Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions android/src/toga_android/hardware/camera.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,3 +120,12 @@ def photo_taken(code, data):
self.interface.app._impl.start_activity(intent, on_complete=photo_taken)
else:
raise PermissionError("App does not have permission to take photos")

def is_scanning(self):
raise NotImplementedError("Barcode scanning is not yet implemented on Android")

def start_scanning(self, future, device, code_types, continuous):
raise NotImplementedError("Barcode scanning is not yet implemented on Android")

def stop_scanning(self):
raise NotImplementedError("Barcode scanning is not yet implemented on Android")
1 change: 1 addition & 0 deletions changes/camera-scanning.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The Camera API gained `start_scanning()`, `stop_scanning()`, and `is_scanning()` methods for real-time barcode and QR code scanning, along with an `on_detection` callback and a `BarcodeFormat` enum. The iOS backend implements scanning using `AVCaptureSession` with `AVCaptureMetadataOutput`, supporting QR codes, Code 128, EAN-13, EAN-8, PDF417, Aztec, and Data Matrix formats. The macOS and Android backends raise `NotImplementedError` for scanning operations.
Comment thread
phildini marked this conversation as resolved.
Outdated
9 changes: 9 additions & 0 deletions cocoa/src/toga_cocoa/hardware/camera.py
Original file line number Diff line number Diff line change
Expand Up @@ -322,3 +322,12 @@ def take_photo(self, result, device, flash):
window.show()
else:
raise PermissionError("App does not have permission to take photos")

def is_scanning(self):
raise NotImplementedError("Barcode scanning is not yet implemented on macOS")
Comment thread
phildini marked this conversation as resolved.
Outdated

def start_scanning(self, future, device, code_types, continuous):
raise NotImplementedError("Barcode scanning is not yet implemented on macOS")

def stop_scanning(self):
raise NotImplementedError("Barcode scanning is not yet implemented on macOS")
15 changes: 15 additions & 0 deletions core/src/toga/constants/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,21 @@ class FillRule(Enum):
##########################################################################


class BarcodeFormat(Enum):
"""The types of barcodes that can be detected during scanning."""

QR = auto()
CODE128 = auto()
EAN13 = auto()
EAN8 = auto()
PDF417 = auto()
AZTEC = auto()
DATA_MATRIX = auto()

def __str__(self) -> str:
return self.name.title()


class FlashMode(Enum):
"""The flash mode to use when capturing photos or videos."""

Expand Down
91 changes: 89 additions & 2 deletions core/src/toga/hardware/camera.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
from __future__ import annotations

from collections.abc import Callable
from typing import TYPE_CHECKING, Any

from toga.constants import FlashMode
from toga.handlers import AsyncResult, PermissionResult
from toga.constants import BarcodeFormat, FlashMode
from toga.handlers import AsyncResult, PermissionResult, wrapped_handler
from toga.platform import get_factory

if TYPE_CHECKING:
Expand All @@ -15,6 +16,10 @@ class PhotoResult(AsyncResult):
RESULT_TYPE = "photo"


class ScanResult(AsyncResult):
RESULT_TYPE = "scan"


class CameraDevice:
def __init__(self, impl: Any):
self._impl = impl
Expand Down Expand Up @@ -49,6 +54,7 @@ def __init__(self, app: App):
self.factory = get_factory()
self._app = app
self._impl = self.factory.Camera(self)
self._on_detection = wrapped_handler(self, None)

@property
def app(self) -> App:
Expand Down Expand Up @@ -119,3 +125,84 @@ def take_photo(
photo = PhotoResult(None)
self._impl.take_photo(photo, device=device, flash=flash)
return photo

@property
def on_detection(self) -> Callable:
"""A handler to invoke when a barcode is detected during scanning.

The callback receives the camera as the first argument, and the detected content
as a keyword argument: ``on_detection(camera, content=content)``.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is in ReST format, not Markdown

Suggested change
as a keyword argument: ``on_detection(camera, content=content)``.
as a keyword argument: `on_detection(camera, content=content)`.


If scanning was started with ``continuous=True``, the callback will be invoked
each time a barcode is detected. If ``continuous=False`` (the default), the
Comment on lines +136 to +137

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More ReST:

Suggested change
If scanning was started with ``continuous=True``, the callback will be invoked
each time a barcode is detected. If ``continuous=False`` (the default), the
If scanning was started with `continuous=True`, the callback will be invoked
each time a barcode is detected. If `continuous=False` (the default), the

This looks to be a recurring theme for the rest of this file.

callback is invoked once before scanning stops automatically.
"""
return self._on_detection

@on_detection.setter
def on_detection(self, handler: Callable | None) -> None:
self._on_detection = wrapped_handler(self, handler)

@property
def is_scanning(self) -> bool:
"""Is the camera currently scanning for barcodes?"""
return self._impl.is_scanning()

def start_scanning(
self,
device: CameraDevice | None = None,
code_types: list[BarcodeFormat] | None = None,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a convenience, should we also accept BarcodeFormat here? I would have expected the most common use case is "scan for QR Code", rather than "Scan for QR code or aztec code or Code128 barcode or ...".

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you prefer that over code_types?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wasn't suggesting changing the argument name - I was only suggesting adding the typing and argument handling shim so that start_scanning(QR) is a legal usage, equivalent to start_scanning([QR]).

start_scanning(code_types=QR) is a little weird because of inconsistent pluralization... but if code_types is the first argument, then you won't need to ever write code_types, and maybe that isn't as obvious as a weirdness?

on_detection: Callable | None = None,
continuous: bool = False,
Comment on lines +153 to +156

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We haven't been consistent about this elsewhere in Toga, but when a method has optional arguments with no obvious order, making them keyword-only tends to make the calling code more readable, especially when they're simple types like numbers or booleans. In this case, I'd say that applies to on_detection and continuous.

) -> ScanResult:
"""Start scanning for barcodes (including QR codes) in real-time.

Displays a live camera preview that scans for supported barcode types. When a
barcode is detected, the ``on_detection`` callback is invoked.

If ``continuous`` is ``False`` (the default), scanning stops automatically after
the first detection, and the returned ``ScanResult`` resolves with the detected
content string. If ``continuous`` is ``True``, scanning continues until
:meth:`stop_scanning` is called, and the ``ScanResult`` resolves with ``None``.
Comment on lines +163 to +166

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Part of this duplicates the continuous docstring, and the rest should be moved to the :returns: docstring.


If the platform requires permission to access the camera, and the user hasn't
previously provided that permission, this will cause permission to be requested.

**This is an asynchronous method**. If you invoke this method in synchronous
context, it will start the scanning process, but will return *immediately*.
The return value can be awaited in an asynchronous context, but cannot be used
directly.

:param device: The camera device to use for scanning. If ``None``, the default
camera will be used.
:param code_types: The types of barcodes to scan for. If ``None``, all supported
types will be detected.
:param on_detection: A handler to invoke when a barcode is detected. This can
also be set via the :attr:`on_detection` property.
:param continuous: If ``False`` (default), scanning stops after the first
detection. If ``True``, scanning continues until :meth:`stop_scanning` is
called.
:returns: An asynchronous result; when awaited, returns the detected content
string if a barcode was found, or ``None`` if scanning was cancelled.
:raises PermissionError: if the app does not have permission to use the camera.
"""
if on_detection is not None:
self.on_detection = on_detection

if code_types is None:
code_types = list(BarcodeFormat)

result = ScanResult(None)
self._impl.start_scanning(
result, device=device, code_types=code_types, continuous=continuous

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a code style thing, once we get to a point where Ruff is breaking code onto a new line to get space for the arguments, we tend to go directly to "1 argument per line", rather than the intermediate "all args on one standalone line" format:

Suggested change
result, device=device, code_types=code_types, continuous=continuous
result,
device=device,
code_types=code_types,
continuous=continuous,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One argument per line might be more readable when the arguments are complex expressions, but forcing it for simple situations like this doesn't seem to accomplish anything except reducing the amount of code you can see on screen at once.

)
return result

def stop_scanning(self) -> None:
"""Stop scanning for barcodes.

If the camera is currently scanning, the scan preview will be dismissed and the
pending :class:`ScanResult` from :meth:`start_scanning` will resolve with
``None``.
"""
self._impl.stop_scanning()
Loading
Loading