-
-
Notifications
You must be signed in to change notification settings - Fork 827
Add real-time barcode scanning API and iOS backend implementation #4458
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 4 commits
5e990e4
67134a5
edd692c
d555088
fcc8343
bb76ae0
589dfaf
ea52118
57c73c2
8ff7b03
c6e5035
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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. | ||
| 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: | ||||||||||||
|
|
@@ -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 | ||||||||||||
|
|
@@ -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: | ||||||||||||
|
|
@@ -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)``. | ||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is in ReST format, not Markdown
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 | ||||||||||||
|
Comment on lines
+136
to
+137
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. More ReST:
Suggested change
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, | ||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As a convenience, should we also accept
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would you prefer that over
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
|
||||||||||||
| on_detection: Callable | None = None, | ||||||||||||
| continuous: bool = False, | ||||||||||||
|
Comment on lines
+153
to
+156
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||||||||||||
| ) -> 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
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Part of this duplicates the |
||||||||||||
|
|
||||||||||||
| 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 | ||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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() | ||||||||||||
Uh oh!
There was an error while loading. Please reload this page.