Getting photos via mobile upload #2715
-
QuestionI am trying to create an element for taking a photo with a phones camera and have come up with this code: from nicegui import ui
async def get_file():
image = await ui.run_javascript('return document.getElementById("file-upload").files[0];')
print(image)
ui_camera_input = ui.input(label="HELLO", on_change=lambda: get_file())
ui_camera_input.props('type="file" accept="image/* capture="camera" for="file-upload"')
ui_camera_input.classes('w-full')
ui.run() Now this works 100% of the time when uploading photos with either a computer or through the phone's files however when taking the photo with the phones camera the awaited javascript times out even when set to 50 seconds. Any help on how to get this to work correctly would be great! Tried on Pixel 8, Chrome Firefox and Edge |
Beta Was this translation helpful? Give feedback.
Replies: 3 comments 18 replies
-
Hi @snowbollaanm, It looks like async def get_file():
print(await ui.run_javascript('return document.getElementById("file-upload").files'))
ui.input(on_change=get_file).props('type="file" accept="image/* capture="camera" for="file-upload"') Output:
But I'm not quite sure how to fix it. Does anyone else have an idea? |
Beta Was this translation helpful? Give feedback.
-
Alright, after a lot of tinkering I have managed to come up with a solution that seems to work reliably. The problem from what I can tell has been caused by two main issues:
The solution I ended up coming to is this: import asyncio
from nicegui import ui
@ui.page('/')
async def page():
ui.add_body_html('''
<canvas id="canvas" style="display:none;"></canvas>
<script>
window.addEventListener("DOMContentLoaded", function() {
document.getElementById('file-upload').addEventListener('change', function(event) {
const file = event.target.files[0];
if (!file.type.match('image.*')) {
alert('Please select an image file.');
return;
}
const reader = new FileReader();
reader.onload = function(readerEvent) {
const img = new Image();
img.onload = function() {
const maxWidth = 1024;
const maxHeight = 768;
let width = img.width;
let height = img.height;
if (width > height) {
if (width > maxWidth) {
height *= maxWidth / width;
width = maxWidth;
}
} else {
if (height > maxHeight) {
width *= maxHeight / height;
height = maxHeight;
}
}
const canvas = document.getElementById('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0, width, height);
};
img.src = readerEvent.target.result;
};
reader.readAsDataURL(file);
});
}, false);
</script>
''')
async def get_picture():
await asyncio.sleep(0.5)
file = await ui.run_javascript('var canvas = document.getElementById("canvas"); var imageURL = canvas.toDataURL(); return imageURL')
with ui.dialog(value=True).classes('w-full'):
ui.image(source=file).classes('w-full')
ui_camera_input = ui.input(on_change=lambda: get_picture())
ui_camera_input.props('type="file" accept="image/* capture="camera" for="file-upload"')
ui_camera_input.classes('w-full').style('display: none;')
ui.html(r'''
<label for="file-upload" class="custom-file-upload q-icon text-blue-5 notranslate material-icons q-pa-sm" style="cursor: pointer; font-size: 64px; background-color: rgba(66, 165, 245, 0.3); border-radius: 10px;" aria-hidden="true" role="presentation" rounded="true">
add_a_photo
</label>
''')
ui.run(reconnect_timeout=20) What this does is when a file is uploaded it downsizes the image into a canvas of 1024 x 768 and then getting the canvas image data as a base64 string that I can later turn into a jpg file. It seems when trying to create higher resolution images e.g. 2048 x 1536 that its still breaking when trying to return the canvas data. What are your thoughts @falkoschindler @rodja |
Beta Was this translation helpful? Give feedback.
-
Hopefully final addition to this! Class: from typing import Callable, Optional, Any
import asyncio
from nicegui import ui
class Camera():
def __init__(
self,
icon: str,
icon_color: str,
background_color: str,
for_id: Optional[str] = 'file-upload',
canvas_id: Optional[str] = 'canvas',
on_change: Optional[Callable[..., Any]] = None,
compression: Optional[float] = 0.8,
):
"""Camera Input
:param icon: The icon to be used for the camera
:param icon_color: The color of the icon
:param background_color: The color of the background, must be in rgba format
:param for_id: The id of the input being used, this should be set if you're using this more than once on a page (default: file-upload)
:param canvas_id: The id for the canvas, this should be set if you're using more than 1 canvas on the page (default: canvas)
:param on_change: callback to execute when the value changes
:param compression: Compression ammount used on uploaded images, change this if no image is being returned usually caused by large file sizes (default: 0.8)
"""
self.compression = compression
self.canvas_id = canvas_id
ui.add_body_html(f'''
<canvas id="{canvas_id}" style="display:none;"></canvas>
<script>
window.addEventListener("DOMContentLoaded", function() {{
document.getElementById('{for_id}').addEventListener('change', function(event) {{
const file = event.target.files[0];
if (!file.type.match('image.*')) {{
alert('Please select an image file.');
return;
}}
const reader = new FileReader();
reader.onload = function(readerEvent) {{
const img = new Image();
img.onload = function() {{
const maxWidth = 2048;
const maxHeight = 1536;
let width = img.width;
let height = img.height;
if (width > height) {{
if (width > maxWidth) {{
height *= maxWidth / width;
width = maxWidth;
}}
}} else {{
if (height > maxHeight) {{
width *= maxHeight / height;
height = maxHeight;
}}
}}
const canvas = document.getElementById('{canvas_id}');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0, width, height);
}};
img.src = readerEvent.target.result;
}};
reader.readAsDataURL(file);
}});
}}, false);
</script>
''')
self._camera_input = ui.input(on_change=on_change)
self._camera_input.props(f'type="file" accept="image/*" capture="camera" for="{for_id}"')
self._camera_input.classes('w-full').style('display: none;')
ui.html(rf'''
<label for="{for_id}" class="custom-{for_id} q-icon text-{icon_color} notranslate material-icons q-pa-sm" style="cursor: pointer; font-size: 64px; background-color: {background_color}; border-radius: 10px;" aria-hidden="true" role="presentation" rounded="true">
{icon}
</label>
''')
async def get_image(self):
await asyncio.sleep(0.5)
file = await ui.run_javascript(f'var canvas = document.getElementById("{self.canvas_id}"); var imageURL = canvas.toDataURL("image/jpeg", {self.compression}); return imageURL', timeout=15)
return file Usage: @ui.page('/')
async def content():
async def generate_image():
await asyncio.sleep(0.1)
image = await test_camera.get_image()
ui.image(image).classes('w-1/5')
test_camera = Camera(
icon='photo_camera', icon_color='blue-5',
background_color='rgba(66, 165, 245, 0.3)', for_id='file-upload',
canvas_id='canvas', on_change=lambda: generate_image()
)
ui.run() |
Beta Was this translation helpful? Give feedback.
Hopefully final addition to this!
I've created a class that can be called to add this "camera" to the page. Note that it will only open a camera on a mobile device otherwise on a computer it will open a file explorer. Thanks!
Class: