30 |
31 | ## License
32 |
33 |
34 |
35 | Published under the [MIT](https://github.com/svecosystem/barqode/blob/main/LICENSE) license. Made by
36 | [@ollema](https://github.com/ollema), [@huntabyte](https://github.com/huntabyte) and
37 | [community](https://github.com/svecosystem/barqode/graphs/contributors) 💛
11 |
12 |
13 |
--------------------------------------------------------------------------------
/docs/src/content/components/barqode-dropzone.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: BarqodeDropzone
3 | description: Click to upload images, drag & drop or use your camera to scan.
4 | section: Components
5 | ---
6 |
7 |
10 |
11 | This component functions as a file input with the `capture` attribute set to `environment` which allows users to take a picture with their camera. You can also drag-and-drop image files from your desktop or images embedded into other web pages anywhere in the area the component occupies. The images are directly scanned and positive results are indicated by the `onDetect` callback.
12 |
13 | ## Demo
14 |
15 |
16 |
17 | ## Usage
18 |
19 | ```svelte
20 |
34 |
35 |
36 |
37 |
Click to upload or drop an image here
38 |
39 |
40 |
41 | Last detected: {result}
42 |
43 |
48 | ```
49 |
50 | ## Props
51 |
52 | ### `formats`
53 |
54 | Type: [`BarcodeFormat[]`](https://github.com/Sec-ant/barcode-detector?tab=readme-ov-file#barcode-detector)
55 |
56 | Default: `["qr_code"]`
57 |
58 | Configure the barcode formats to detect. By default, only QR codes are detected.
59 |
60 | If you want to detect multiple formats, pass an array of formats:
61 |
62 | ```svelte
63 |
64 | ```
65 |
66 | Under the hood, the standard [BarcodeDetector API](https://developer.mozilla.org/en-US/docs/Web/API/BarcodeDetector) is used. Support varies across devices, operating systems and browsers. All components will prefer to use the native implementation if available and otherwise falls back to a polyfill implementation.
67 |
68 | Note that even if the native implementation is available, the component still might use the polyfill. For example, if the native implementation only supports the format `'qr_code'` but the you select the formats `['qr_code', 'aztec']`.
69 |
70 | ### `onDetect`
71 |
72 | Type: `(detectedCodes: DetectedBarcode[]) => void`
73 |
74 | Callback function that is called when a barcode is detected.
75 |
76 | It receives an array of [detected barcodes](https://developer.mozilla.org/en-US/docs/Web/API/BarcodeDetector/detect#return_value), one callback per image.
77 |
78 | If not barcode is detected, the array will be empty.
79 |
80 | ### `onDragover`
81 |
82 | Type: `(isDraggingOver: boolean) => void`
83 |
84 | Callback function that is called when a file is dragged over the drop zone.
85 |
86 | ### `onError`
87 |
88 | Type: `(error: Error) => void`
89 |
90 | Callback function that is called when an error occurs.
91 |
92 | TODO: insert link to errors.
93 |
94 | ### Other props
95 |
96 | The `BarqodeDropzone` component accepts all attributes that a standard `input` element accepts.
97 |
98 | By default, the following attributes are set:
99 |
100 | - `type="file"`. This is required to make the input a file input. You should not change this.
101 | - `name="image"`. This is the name of the file input.
102 | - `accept="image/*"`. This restricts the file types that can be uploaded to images.
103 | - `capture="environment"`. This tells the browser to open the camera when the input is clicked on mobile devices. You can choose between `user` and `environment`, which opens the front and back camera respectively. You can also disable this functionality by setting it to `null`.
104 | - `multiple`. This allows the user to upload multiple files at once. You can disable this by settings this to `false`.
105 |
106 | ## Browser Support
107 |
108 | This component depends on the [File Reader API](https://developer.mozilla.org/en-US/docs/Web/API/FileReader) which is widely supported in modern browsers.
109 |
--------------------------------------------------------------------------------
/docs/src/content/components/barqode-stream.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: BarqodeStream
3 | description: Continuously scans frames from a camera stream.
4 | section: Components
5 | ---
6 |
7 |
11 |
12 | The `BarqodeStream` component continuously scans frames from a camera stream and detects barcodes in real-time.
13 |
14 | ## Demo
15 |
16 |
17 |
18 | ## Usage
19 |
20 | ```svelte
21 |
54 |
55 |
56 |
57 | {#if loading}
58 |
Loading...
59 | {/if}
60 |
61 |
62 |
63 | Last detected: {result}
64 | ```
65 |
66 | ## Props
67 |
68 | ### `constraints`
69 |
70 | Type: [`MediaTrackConstraints`](https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints)
71 |
72 | Default: `{ video: { facingMode: "environment" } }`
73 |
74 | Configure the the various camera options, for example whether to use front or rear camera.
75 |
76 | The object must be of type `MediaTrackConstraints`.
77 |
78 | The object is passed as-is to `getUserMedia`, which is the API call for requesting a camera stream:
79 |
80 | ```js
81 | navigator.mediaDevices.getUserMedia({
82 | audio: false,
83 | video: the_constraint_object_you_provide,
84 | });
85 | ```
86 |
87 | When `constraints` is updated, a new camera stream is requested which triggers the `onCameraOn` callback again. You can catch errors with the `onError` callback. An error can occur when you try to use the front camera on a device that doesn't have one for example.
88 |
89 | ### `formats`
90 |
91 | Type: [`BarcodeFormat[]`](https://github.com/Sec-ant/barcode-detector?tab=readme-ov-file#barcode-detector)
92 |
93 | Default: `["qr_code"]`
94 |
95 | Configure the barcode formats to detect. By default, only QR codes are detected.
96 |
97 | If you want to detect multiple formats, pass an array of formats:
98 |
99 | ```svelte
100 |
101 | ```
102 |
103 |
104 |
105 | Don't select more barcode formats than needed.
106 |
107 | Scanning becomes more expensive the more formats you select.
108 |
109 |
110 |
111 | Under the hood, the standard [BarcodeDetector API](https://developer.mozilla.org/en-US/docs/Web/API/BarcodeDetector) is used. Support varies across devices, operating systems and browsers. All components will prefer to use the native implementation if available and otherwise falls back to a polyfill implementation.
112 |
113 | Note that even if the native implementation is available, the component still might use the polyfill. For example, if the native implementation only supports the format `'qr_code'` but the you select the formats `['qr_code', 'aztec']`.
114 |
115 | ### `paused`
116 |
117 | Type: `boolean` (bindable)
118 |
119 | Default: `false`
120 |
121 | Whether the camera stream is or should be paused. Bindable, which means that you can pause/unpause the stream from outside the component.
122 |
123 | Pausing the stream by setting `paused` to `true` is useful if you want to show some microinteraction after successful scans. When the you set it to `false`, the camera stream will be restarted and the `onCameraOn` callback function will be triggered again.
124 |
125 | ### `torch`
126 |
127 | Type: `boolean`
128 |
129 | Default: `false`
130 |
131 | Turn the camera flashlight on or off.
132 |
133 | This is not consistently supported by all devices and browsers. Support can even vary on the same device with the same browser. For example the rear camera often has a flashlight but the front camera does not.
134 |
135 | We can only tell if flashlight control is supported once the camera is loaded and the `onCameraOn` callback has been called. At the moment, enabling the torch may silently fail on unsupported devices, but in the `onCameraOn` callback payload you can access the `MediaTrackCapabilities` object, from which you can determine if the torch is supported.
136 |
137 | The camera stream must be reloaded when turning the torch on or off. That means the `onCameraOn` event will be emitted again.
138 |
139 | ### `track`
140 |
141 | Type: `(detectedCodes: DetectedBarcode[], ctx: CanvasRenderingContext2D) => void`
142 |
143 | Callback function that can be used to visually highlight detected barcodes.
144 |
145 | A transparent canvas is overlaid on top of the camera stream. The `track` function is used to draw on this canvas.
146 |
147 | It receives an array of [detected barcodes](https://developer.mozilla.org/en-US/docs/Web/API/BarcodeDetector/detect#return_value) and a [CanvasRenderingContext2D](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D) as the second argument.
148 |
149 | Note that when `track` is set the scanning frequency has to be increased. So if you want to go easy on your target device you might not want to enable tracking.
150 |
151 |
152 |
153 | The `track` function is called for every frame. It is important to keep the function as performant as possible.
154 |
155 | This can lead to performance issues on low-end devices and memory leaks if not handled correctly.
156 |
157 |
158 |
159 | ### `onCameraOn`
160 |
161 | Type: `(capabilities: MediaTrackCapabilities) => void`
162 |
163 | Callback function that is called when the camera stream is successfully loaded.
164 |
165 | It might take a while before the component is ready and the scanning process starts. The user has to be asked for camera access permission first and the camera stream has to be loaded.
166 |
167 | If you want to show a loading indicator, you can wait for this callback to be called. It is called as soon as the camera start streaming.
168 |
169 | The callback receives the a promise which resolves with the cameras [`MediaTrackCapabilities`](https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackCapabilities) when everything is ready.
170 |
171 | ### `onError`
172 |
173 | Type: `(error: Error) => void`
174 |
175 | Callback function that is called when an error occurs.
176 |
177 | TODO: insert link to errors.
178 |
179 | ### `onCameraOff`
180 |
181 | Type: `() => void`
182 |
183 | Callback function that is called when the camera stream is stopped.
184 |
185 | This can happen when the camera constraints are modified, for example when switching between front and rear camera or when turning the torch on or off.
186 |
187 | ### `onDetect`
188 |
189 | Type: `(detectedCodes: DetectedBarcode[]) => void`
190 |
191 | Callback function that is called when a barcode is detected.
192 |
193 | It receives an array of [detected barcodes](https://developer.mozilla.org/en-US/docs/Web/API/BarcodeDetector/detect#return_value).
194 |
195 |
196 |
197 | If you scan the same barcode multiple times in a row, `onDetect` is still only called once. When you hold a barcode in the camera, frames are actually decoded multiple times a second but you don't want to be flooded with callbacks that often. That's why the last decoded QR code is always cached and only new results are propagated. However, changing the value of `paused` resets this internal cache.
198 |
199 |
200 |
201 | ## Browser Support
202 |
203 | This component depends on the [Media Capture and Streams API](https://developer.mozilla.org/en-US/docs/Web/API/Media_Capture_and_Streams_API) which is widely supported in modern browsers.
204 |
--------------------------------------------------------------------------------
/docs/src/content/demos/full-demo.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Full demo
3 | description: Kitchen sink demo of the BarqodeStream component.
4 | section: Demos
5 | ---
6 |
7 |
10 |
11 | Modern mobile phones often have a variety of different cameras installed (e.g. front, rear,
12 | wide-angle, infrared, desk-view). The one picked by default is sometimes not the best
13 | choice. For more fine-grained control, you can select a camera by device constraints or by
14 | the device ID.
15 |
16 | Detected codes are visually highlighted in real-time. In this demo you can use the track function dropdown to change the flavor.
17 |
18 | By default only QR-codes are detected but a variety of other barcode formats are also supported.
19 | You can select one or multiple but the more you select the more expensive scanning becomes.
20 |
21 | ## Demo
22 |
23 |
24 |
25 | ## Usage
26 |
27 | ```svelte
28 |
209 |
210 |
211 |
218 |
219 |
220 |
227 |
228 |
229 | {#each Object.keys(barcodeFormats) as option}
230 | {@const barcodeOption = option as BarcodeFormat}
231 |
251 |
252 | Last result: {result}
253 | ```
254 |
--------------------------------------------------------------------------------
/docs/src/content/getting-started.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Getting Started
3 | description: A quick guide to get started using barqode
4 | section: Overview
5 | ---
6 |
7 | The following guide will walk you through installing and setting up the barqode components in your Svelte project.
8 |
9 | ## Installation
10 |
11 | Install the package using your preferred package manager:
12 |
13 | ```bash
14 | npm install barqode
15 | ```
16 |
17 | ## Basic Usage
18 |
19 | The simplest way to use the library is with the `BarqodeStream` component for live scanning:
20 |
21 | ```svelte
22 |
29 |
30 |
31 |
32 |
33 |
34 |
41 | ```
42 |
43 | For detailed information about each component's capabilities and options, refer to their respective API documentation pages.
44 |
--------------------------------------------------------------------------------
/docs/src/content/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Introduction
3 | description: What is Barqode?
4 | section: Overview
5 | ---
6 |
7 | Barqode provides a set of Svelte components for detecting and decoding QR codes and various other barcode formats right in the browser. It's a comprehensive solution that supports both camera streams and static image processing.
8 |
9 | Barqode started out as a port of [`vue-qrcode-reader`](https://github.com/gruhn/vue-qrcode-reader).
10 |
11 | ## Components
12 |
13 | - **`BarqodeStream`** - continuously scans frames from a camera stream.
14 | - **`BarqodeDropzone`** - drag & drop, click to upload or capture images from camera.
15 |
16 | ## Features
17 |
18 | - **Real-time scanning**. Detect codes from live camera stream.
19 | - **Multiple formats**. Support for QR codes and various barcode standards.
20 | - **Visual feedback**. Customizable canvas based tracking of detected codes.
21 | - **Cross-browser**. Works across modern browsers with a [WebAssembly-based polyfill](https://github.com/Sec-ant/barcode-detector) if needed.
22 | - **Camera selection**. Choose between front/rear cameras.
23 | - **Torch control**. Control device flashlight where supported.
24 | - **Responsive**. Components adapt to fill available space.
25 | - **Error handling**. Comprehensive error handling for camera/detection issues.
26 |
27 | ## Usage Example
28 |
29 | ```svelte
30 |
37 |
38 |
39 |
40 |
41 |
42 |
49 | ```
50 |
51 | ## Browser Support
52 |
53 | The components rely primarily on the [Barcode Detection API](https://developer.mozilla.org/en-US/docs/Web/API/Barcode_Detection_API) and [Media Capture and Streams API](https://developer.mozilla.org/en-US/docs/Web/API/Media_Capture_and_Streams_API), with fallbacks where possible.
54 |
55 | While the core scanning functionality uses the native `BarcodeDetector` where available, it falls back to a [WebAssembly-based polyfill](https://github.com/Sec-ant/barcode-detector) to ensure consistent behavior across browsers.
56 |
57 | For a detailed compatibility overview, check each component's documentation.
58 |
--------------------------------------------------------------------------------
/docs/src/lib/components/blueprint.svelte:
--------------------------------------------------------------------------------
1 |
5 |
6 |
9 |
10 | {@render children?.()}
11 |
--------------------------------------------------------------------------------
/docs/src/lib/components/demos/barqode-dropzone.svelte:
--------------------------------------------------------------------------------
1 |
16 |
17 |
18 |
3 |
--------------------------------------------------------------------------------
/docs/src/routes/(landing)/+page.ts:
--------------------------------------------------------------------------------
1 | import { redirect } from "@sveltejs/kit";
2 |
3 | export function load() {
4 | redirect(302, "/docs");
5 | }
6 |
--------------------------------------------------------------------------------
/docs/src/routes/+layout.svelte:
--------------------------------------------------------------------------------
1 |
11 |
12 |
13 | {#if !dev}
14 |
19 | {/if}
20 |
21 |
22 | {@render children?.()}
23 |
--------------------------------------------------------------------------------
/docs/src/routes/api/search.json/+server.ts:
--------------------------------------------------------------------------------
1 | import type { RequestHandler } from "@sveltejs/kit";
2 | import search from "./search.json" assert { type: "json" };
3 |
4 | export const prerender = true;
5 |
6 | export const GET: RequestHandler = () => {
7 | return Response.json(search);
8 | };
9 |
--------------------------------------------------------------------------------
/docs/src/routes/api/search.json/search.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "title": "Getting Started",
4 | "href": "/docs/getting-started",
5 | "description": "A quick guide to get started using barqode",
6 | "content": "The following guide will walk you through installing and setting up the barqode components in your Svelte project. Installation Install the package using your preferred package manager: npm install barqode Basic Usage The simplest way to use the library is with the BarqodeStream component for live scanning: import { BarqodeStream, type DetectedBarcode } from \"barqode\"; function onDetect(detectedCodes: DetectedBarcode[]) { console.log(detectedCodes.map((detectedCode) => detectedCode.rawValue)); } .barqode { width: 100%; max-width: 600px; aspect-ratio: 4/3; } For detailed information about each component's capabilities and options, refer to their respective API documentation pages."
7 | },
8 | {
9 | "title": "Introduction",
10 | "href": "/docs/index",
11 | "description": "What is Barqode?",
12 | "content": "Barqode provides a set of Svelte components for detecting and decoding QR codes and various other barcode formats right in the browser. It's a comprehensive solution that supports both camera streams and static image processing. Barqode started out as a port of $2. Components BarqodeStream** - continuously scans frames from a camera stream. BarqodeDropzone** - drag & drop, click to upload or capture images from camera. Features Real-time scanning**. Detect codes from live camera stream. Multiple formats**. Support for QR codes and various barcode standards. Visual feedback**. Customizable canvas based tracking of detected codes. Cross-browser**. Works across modern browsers with a $2 if needed. Camera selection**. Choose between front/rear cameras. Torch control**. Control device flashlight where supported. Responsive**. Components adapt to fill available space. Error handling**. Comprehensive error handling for camera/detection issues. Usage Example import { BarqodeStream, type DetectedBarcode } from \"barqode\"; function onDetect(detectedCodes: DetectedBarcode[]) { console.log(detectedCodes.map((detectedCode) => detectedCode.rawValue)); } .barqode { width: 100%; max-width: 600px; aspect-ratio: 4/3; } Browser Support The components rely primarily on the $2 and $2, with fallbacks where possible. While the core scanning functionality uses the native BarcodeDetector where available, it falls back to a $2 to ensure consistent behavior across browsers. For a detailed compatibility overview, check each component's documentation."
13 | },
14 | {
15 | "title": "BarqodeDropzone",
16 | "href": "/docs/components/barqode-dropzone",
17 | "description": "Click to upload images, drag & drop or use your camera to scan.",
18 | "content": " import Demo from '$lib/components/demos/barqode-dropzone.svelte'; This component functions as a file input with the capture attribute set to environment which allows users to take a picture with their camera. You can also drag-and-drop image files from your desktop or images embedded into other web pages anywhere in the area the component occupies. The images are directly scanned and positive results are indicated by the onDetect callback. Demo Usage import { BarqodeDropzone, type DetectedBarcode } from \"barqode\"; let result = $state(\"\"); let dragover = $state(false); function onDetect(detectedCodes: DetectedBarcode[]) { result = detectedCodes.map((detectedCode) => detectedCode.rawValue).join(\", \"); } function onDragover(isDraggingOver: boolean) { dragover = isDraggingOver; } Click to upload or drop an image here Last detected: {result} .dragover { border-color: white; } Props formats Type: [BarcodeFormat[]](https://github.com/Sec-ant/barcode-detector?tab=readme-ov-file#barcode-detector) Default: [\"qr_code\"] Configure the barcode formats to detect. By default, only QR codes are detected. If you want to detect multiple formats, pass an array of formats: Under the hood, the standard $2 is used. Support varies across devices, operating systems and browsers. All components will prefer to use the native implementation if available and otherwise falls back to a polyfill implementation. Note that even if the native implementation is available, the component still might use the polyfill. For example, if the native implementation only supports the format 'qr_code' but the you select the formats ['qr_code', 'aztec']. onDetect Type: (detectedCodes: DetectedBarcode[]) => void Callback function that is called when a barcode is detected. It receives an array of $2, one callback per image. If not barcode is detected, the array will be empty. onDragover Type: (isDraggingOver: boolean) => void Callback function that is called when a file is dragged over the drop zone. onError Type: (error: Error) => void Callback function that is called when an error occurs. TODO: insert link to errors. Other props The BarqodeDropzone component accepts all attributes that a standard input element accepts. By default, the following attributes are set: type=\"file\". This is required to make the input a file input. You should not change this. name=\"image\". This is the name of the file input. accept=\"image/*\". This restricts the file types that can be uploaded to images. capture=\"environment\". This tells the browser to open the camera when the input is clicked on mobile devices. You can choose between user and environment, which opens the front and back camera respectively. You can also disable this functionality by setting it to null. multiple. This allows the user to upload multiple files at once. You can disable this by settings this to false. Browser Support This component depends on the $2 which is widely supported in modern browsers."
19 | },
20 | {
21 | "title": "BarqodeStream",
22 | "href": "/docs/components/barqode-stream",
23 | "description": "Continuously scans frames from a camera stream.",
24 | "content": " import Demo from '$lib/components/demos/barqode-stream.svelte'; import { Callout } from '@svecodocs/kit'; The BarqodeStream component continuously scans frames from a camera stream and detects barcodes in real-time. Demo Usage import { BarqodeStream, type DetectedBarcode } from \"barqode\"; let loading = $state(true); let result: string | null = $state(null); function onCameraOn() { loading = false; } function onDetect(detectedCodes: DetectedBarcode[]) { result = detectedCode.map((code) => detectedCode.rawValue).join(\", \"); } function track(detectedCodes: DetectedBarcode[], ctx: CanvasRenderingContext2D) { for (const detectedCode of detectedCodes) { const [firstPoint, ...otherPoints] = detectedCode.cornerPoints; ctx.strokeStyle = \"#2563eb\"; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(firstPoint.x, firstPoint.y); for (const { x, y } of otherPoints) { ctx.lineTo(x, y); } ctx.lineTo(firstPoint.x, firstPoint.y); ctx.closePath(); ctx.stroke(); } } {#if loading} Loading... {/if} Last detected: {result} Props constraints Type: $2 Default: { video: { facingMode: \"environment\" } } Configure the the various camera options, for example whether to use front or rear camera. The object must be of type MediaTrackConstraints. The object is passed as-is to getUserMedia, which is the API call for requesting a camera stream: navigator.mediaDevices.getUserMedia({ audio: false, video: the_constraint_object_you_provide, }); When constraints is updated, a new camera stream is requested which triggers the onCameraOn callback again. You can catch errors with the onError callback. An error can occur when you try to use the front camera on a device that doesn't have one for example. formats Type: [BarcodeFormat[]](https://github.com/Sec-ant/barcode-detector?tab=readme-ov-file#barcode-detector) Default: [\"qr_code\"] Configure the barcode formats to detect. By default, only QR codes are detected. If you want to detect multiple formats, pass an array of formats: Don't select more barcode formats than needed. Scanning becomes more expensive the more formats you select. Under the hood, the standard $2 is used. Support varies across devices, operating systems and browsers. All components will prefer to use the native implementation if available and otherwise falls back to a polyfill implementation. Note that even if the native implementation is available, the component still might use the polyfill. For example, if the native implementation only supports the format 'qr_code' but the you select the formats ['qr_code', 'aztec']. paused Type: boolean (bindable) Default: false Whether the camera stream is or should be paused. Bindable, which means that you can pause/unpause the stream from outside the component. Pausing the stream by setting paused to true is useful if you want to show some microinteraction after successful scans. When the you set it to false, the camera stream will be restarted and the onCameraOn callback function will be triggered again. torch Type: boolean Default: false Turn the camera flashlight on or off. This is not consistently supported by all devices and browsers. Support can even vary on the same device with the same browser. For example the rear camera often has a flashlight but the front camera does not. We can only tell if flashlight control is supported once the camera is loaded and the onCameraOn callback has been called. At the moment, enabling the torch may silently fail on unsupported devices, but in the onCameraOn callback payload you can access the MediaTrackCapabilities object, from which you can determine if the torch is supported. The camera stream must be reloaded when turning the torch on or off. That means the onCameraOn event will be emitted again. track Type: (detectedCodes: DetectedBarcode[], ctx: CanvasRenderingContext2D) => void Callback function that can be used to visually highlight detected barcodes. A transparent canvas is overlaid on top of the camera stream. The track function is used to draw on this canvas. It receives an array of $2 and a $2 as the second argument. Note that when track is set the scanning frequency has to be increased. So if you want to go easy on your target device you might not want to enable tracking. The track function is called for every frame. It is important to keep the function as performant as possible. This can lead to performance issues on low-end devices and memory leaks if not handled correctly. onCameraOn Type: (capabilities: MediaTrackCapabilities) => void Callback function that is called when the camera stream is successfully loaded. It might take a while before the component is ready and the scanning process starts. The user has to be asked for camera access permission first and the camera stream has to be loaded. If you want to show a loading indicator, you can wait for this callback to be called. It is called as soon as the camera start streaming. The callback receives the a promise which resolves with the cameras $2 when everything is ready. onError Type: (error: Error) => void Callback function that is called when an error occurs. TODO: insert link to errors. onCameraOff Type: () => void Callback function that is called when the camera stream is stopped. This can happen when the camera constraints are modified, for example when switching between front and rear camera or when turning the torch on or off. onDetect Type: (detectedCodes: DetectedBarcode[]) => void Callback function that is called when a barcode is detected. It receives an array of $2. If you scan the same barcode multiple times in a row, onDetect is still only called once. When you hold a barcode in the camera, frames are actually decoded multiple times a second but you don't want to be flooded with callbacks that often. That's why the last decoded QR code is always cached and only new results are propagated. However, changing the value of paused resets this internal cache. Browser Support This component depends on the $2 which is widely supported in modern browsers."
25 | },
26 | {
27 | "title": "Full demo",
28 | "href": "/docs/demos/full-demo",
29 | "description": "Kitchen sink demo of the BarqodeStream component.",
30 | "content": " import Demo from '$lib/components/demos/full-demo.svelte'; Modern mobile phones often have a variety of different cameras installed (e.g. front, rear, wide-angle, infrared, desk-view). The one picked by default is sometimes not the best choice. For more fine-grained control, you can select a camera by device constraints or by the device ID. Detected codes are visually highlighted in real-time. In this demo you can use the track function dropdown to change the flavor. By default only QR-codes are detected but a variety of other barcode formats are also supported. You can select one or multiple but the more you select the more expensive scanning becomes. Demo Usage import { BarqodeStream, type BarcodeFormat, type DetectedBarcode } from \"barqode\"; let result = $state(\"\"); let error = $state(\"\"); let selectedConstraints = $state({ facingMode: \"environment\" }); let barcodeFormats: { [key in BarcodeFormat]: boolean; } = $state({ aztec: false, code_128: false, code_39: false, code_93: false, codabar: false, databar: false, databar_expanded: false, databar_limited: false, data_matrix: false, dx_film_edge: false, ean_13: false, ean_8: false, itf: false, maxi_code: false, micro_qr_code: false, pdf417: false, qr_code: true, rm_qr_code: false, upc_a: false, upc_e: false, linear_codes: false, matrix_codes: false, unknown: false, }); // computed value for selected formats let selectedBarcodeFormats: BarcodeFormat[] = $derived( Object.keys(barcodeFormats).filter( (format: string) => barcodeFormats[format] ) as BarcodeFormat[] ); // track function options const trackFunctionOptions = [ { text: \"nothing (default)\", value: undefined }, { text: \"outline\", value: paintOutline }, { text: \"centered text\", value: paintCenterText }, { text: \"bounding box\", value: paintBoundingBox }, ]; let trackFunctionSelected = $state(trackFunctionOptions[1]); // camera constraint options const defaultConstraintOptions: { label: string; constraints: MediaTrackConstraints }[] = [ { label: \"rear camera\", constraints: { facingMode: \"environment\" } }, { label: \"front camera\", constraints: { facingMode: \"user\" } }, ]; let constraintOptions = $state(defaultConstraintOptions); async function onCameraOn() { try { const devices = await navigator.mediaDevices.enumerateDevices(); const videoDevices = devices.filter(({ kind }) => kind === \"videoinput\"); constraintOptions = [ ...defaultConstraintOptions, ...videoDevices.map(({ deviceId, label }) => ({ label: ${label}, constraints: { deviceId }, })), ]; error = \"\"; } catch (e) { console.error(e); } } function onError(err: { name: string; message: string }) { error = [${err.name}]: ; if (err.name === \"NotAllowedError\") { error += \"you need to grant camera access permission\"; } else if (err.name === \"NotFoundError\") { error += \"no camera on this device\"; } else if (err.name === \"NotSupportedError\") { error += \"secure context required (HTTPS, localhost)\"; } else if (err.name === \"NotReadableError\") { error += \"is the camera already in use?\"; } else if (err.name === \"OverconstrainedError\") { error += \"installed cameras are not suitable\"; } else if (err.name === \"StreamApiNotSupportedError\") { error += \"Stream API is not supported in this browser\"; } else { error += err.message; } } function onDetect(detectedCodes: DetectedBarcode[]) { console.log(detectedCodes); result = JSON.stringify(detectedCodes.map((code) => code.rawValue)); } // track functions function paintOutline( detectedCodes: { cornerPoints: { x: number; y: number }[]; boundingBox: DOMRectReadOnly; rawValue: string; format: Exclude; }[], ctx: CanvasRenderingContext2D ) { for (const detectedCode of detectedCodes) { const [firstPoint, ...otherPoints] = detectedCode.cornerPoints; ctx.strokeStyle = \"red\"; ctx.beginPath(); ctx.moveTo(firstPoint.x, firstPoint.y); for (const { x, y } of otherPoints) { ctx.lineTo(x, y); } ctx.lineTo(firstPoint.x, firstPoint.y); ctx.closePath(); ctx.stroke(); } } function paintBoundingBox( detectedCodes: { cornerPoints: { x: number; y: number }[]; boundingBox: DOMRectReadOnly; rawValue: string; format: Exclude; }[], ctx: CanvasRenderingContext2D ) { for (const detectedCode of detectedCodes) { const { boundingBox: { x, y, width, height }, } = detectedCode; ctx.lineWidth = 2; ctx.strokeStyle = \"#007bff\"; ctx.strokeRect(x, y, width, height); } } function paintCenterText( detectedCodes: { cornerPoints: { x: number; y: number }[]; boundingBox: DOMRectReadOnly; rawValue: string; format: Exclude; }[], ctx: CanvasRenderingContext2D ) { for (const detectedCode of detectedCodes) { const { boundingBox, rawValue } = detectedCode; const centerX = boundingBox.x + boundingBox.width / 2; const centerY = boundingBox.y + boundingBox.height / 2; const fontSize = Math.max(12, (50 * boundingBox.width) / ctx.canvas.width); ctx.font = bold ${fontSize}px sans-serif; ctx.textAlign = \"center\"; ctx.lineWidth = 3; ctx.strokeStyle = \"#35495e\"; ctx.strokeText(detectedCode.rawValue, centerX, centerY); ctx.fillStyle = \"#5cb984\"; ctx.fillText(rawValue, centerX, centerY); } } Camera constraints: {#each constraintOptions as option} {option.label} {/each} Track function: {#each trackFunctionOptions as option} {option.text} {/each} Barcode formats: {#each Object.keys(barcodeFormats) as option} {@const barcodeOption = option as BarcodeFormat} {option} {/each} {#if error} {error} {/if} Last result:{result} `"
31 | }
32 | ]
33 |
--------------------------------------------------------------------------------
/docs/static/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/svecosystem/barqode/129d781e31c7b88016e618d4c5ad3635c2770f8a/docs/static/android-chrome-192x192.png
--------------------------------------------------------------------------------
/docs/static/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/svecosystem/barqode/129d781e31c7b88016e618d4c5ad3635c2770f8a/docs/static/android-chrome-512x512.png
--------------------------------------------------------------------------------
/docs/static/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/svecosystem/barqode/129d781e31c7b88016e618d4c5ad3635c2770f8a/docs/static/apple-touch-icon.png
--------------------------------------------------------------------------------
/docs/static/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/svecosystem/barqode/129d781e31c7b88016e618d4c5ad3635c2770f8a/docs/static/favicon-16x16.png
--------------------------------------------------------------------------------
/docs/static/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/svecosystem/barqode/129d781e31c7b88016e618d4c5ad3635c2770f8a/docs/static/favicon-32x32.png
--------------------------------------------------------------------------------
/docs/static/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/svecosystem/barqode/129d781e31c7b88016e618d4c5ad3635c2770f8a/docs/static/favicon.ico
--------------------------------------------------------------------------------
/docs/static/og.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/svecosystem/barqode/129d781e31c7b88016e618d4c5ad3635c2770f8a/docs/static/og.png
--------------------------------------------------------------------------------
/docs/static/site.webmanifest:
--------------------------------------------------------------------------------
1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
--------------------------------------------------------------------------------
/docs/svelte.config.js:
--------------------------------------------------------------------------------
1 | import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
2 | import { mdsx } from "mdsx";
3 | import mdsxConfig from "./mdsx.config.js";
4 | import adapter from "@sveltejs/adapter-cloudflare";
5 |
6 | /** @type {import('@sveltejs/kit').Config} */
7 | const config = {
8 | preprocess: [mdsx(mdsxConfig), vitePreprocess()],
9 | kit: {
10 | alias: {
11 | "$content/*": ".velite/*",
12 | },
13 | adapter: adapter(),
14 | },
15 | extensions: [".svelte", ".md"],
16 | };
17 |
18 | export default config;
19 |
--------------------------------------------------------------------------------
/docs/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./.svelte-kit/tsconfig.json",
3 | "compilerOptions": {
4 | "allowJs": true,
5 | "checkJs": true,
6 | "esModuleInterop": true,
7 | "forceConsistentCasingInFileNames": true,
8 | "resolveJsonModule": true,
9 | "skipLibCheck": true,
10 | "sourceMap": true,
11 | "strict": true,
12 | "moduleResolution": "bundler"
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/docs/velite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig, s } from "velite";
2 |
3 | const baseSchema = s.object({
4 | title: s.string(),
5 | description: s.string(),
6 | path: s.path(),
7 | content: s.markdown(),
8 | navLabel: s.string().optional(),
9 | raw: s.raw(),
10 | toc: s.toc(),
11 | section: s.enum(["Overview", "Components", "Demos"]),
12 | });
13 |
14 | const docSchema = baseSchema.transform((data) => {
15 | return {
16 | ...data,
17 | slug: data.path,
18 | slugFull: `/${data.path}`,
19 | };
20 | });
21 |
22 | export default defineConfig({
23 | root: "./src/content",
24 | collections: {
25 | docs: {
26 | name: "Doc",
27 | pattern: "./**/*.md",
28 | schema: docSchema,
29 | },
30 | },
31 | });
32 |
--------------------------------------------------------------------------------
/docs/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { sveltekit } from "@sveltejs/kit/vite";
2 | import { defineConfig } from "vite";
3 | import tailwindcss from "@tailwindcss/vite";
4 | import { resolve } from "node:path";
5 |
6 | const __dirname = new URL(".", import.meta.url).pathname;
7 |
8 | export default defineConfig({
9 | plugins: [sveltekit(), tailwindcss()],
10 | optimizeDeps: {
11 | exclude: ["barqode"],
12 | },
13 | server: {
14 | fs: {
15 | allow: [resolve(__dirname, "./.velite")],
16 | },
17 | },
18 | });
19 |
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
1 | import eslint from "@eslint/js";
2 | import prettier from "eslint-config-prettier";
3 | import svelte from "eslint-plugin-svelte";
4 | import globals from "globals";
5 | import tseslint from "typescript-eslint";
6 |
7 | export default tseslint.config(
8 | eslint.configs.recommended,
9 | ...tseslint.configs.recommended,
10 | ...svelte.configs["flat/recommended"],
11 | prettier,
12 | ...svelte.configs["flat/prettier"],
13 | {
14 | languageOptions: {
15 | globals: {
16 | ...globals.browser,
17 | ...globals.node,
18 | },
19 | },
20 | },
21 | {
22 | files: ["**/*.svelte"],
23 | languageOptions: {
24 | parserOptions: {
25 | parser: tseslint.parser,
26 | },
27 | },
28 | },
29 | {
30 | rules: {
31 | "@typescript-eslint/no-unused-vars": [
32 | "error",
33 | {
34 | argsIgnorePattern: "^_",
35 | varsIgnorePattern: "^_",
36 | },
37 | ],
38 | "@typescript-eslint/no-unused-expressions": "off",
39 | },
40 | },
41 | {
42 | ignores: [
43 | "build/",
44 | ".svelte-kit/",
45 | "dist/",
46 | ".svelte-kit/**/*",
47 | "docs/.svelte-kit/**/*",
48 | "docs/static/**/*",
49 | "docs/build/**/*",
50 | ".svelte-kit",
51 | "packages/barqode/dist/**/*",
52 | "packages/barqode/.svelte-kit/**/*",
53 | ],
54 | }
55 | );
56 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "root",
3 | "description": "Monorepo for Barqode",
4 | "private": true,
5 | "version": "0.0.0",
6 | "author": "Hunter Johnston ",
7 | "license": "MIT",
8 | "type": "module",
9 | "scripts": {
10 | "build": "pnpm build:packages && pnpm build:docs",
11 | "build:packages": "pnpm -F \"./packages/**\" --parallel build",
12 | "build:docs": "pnpm -F \"./docs/**\" build",
13 | "check": "pnpm build:packages && pnpm -r check",
14 | "ci:publish": "pnpm build:packages && changeset publish",
15 | "dev": "pnpm -F \"./packages/**\" svelte-kit sync && pnpm -r --parallel --reporter append-only --color dev",
16 | "test": "pnpm -r test",
17 | "format": "prettier --write .",
18 | "lint": "prettier --check . && eslint ."
19 | },
20 | "engines": {
21 | "pnpm": ">=9.0.0",
22 | "node": ">=20"
23 | },
24 | "packageManager": "pnpm@9.14.4",
25 | "devDependencies": {
26 | "@changesets/cli": "^2.27.10",
27 | "@eslint/js": "^9.16.0",
28 | "@svitejs/changesets-changelog-github-compact": "^1.2.0",
29 | "@types/node": "^22.10.1",
30 | "@typescript-eslint/eslint-plugin": "^8.17.0",
31 | "@typescript-eslint/scope-manager": "^8.17.0",
32 | "@typescript-eslint/utils": "^8.17.0",
33 | "eslint": "^9.16.0",
34 | "eslint-config-prettier": "^9.1.0",
35 | "eslint-plugin-svelte": "^2.46.1",
36 | "globals": "^15.13.0",
37 | "prettier": "^3.4.1",
38 | "prettier-plugin-svelte": "^3.3.2",
39 | "prettier-plugin-tailwindcss": "^0.6.9",
40 | "svelte": "^5.4.0",
41 | "svelte-eslint-parser": "^0.43.0",
42 | "typescript": "^5.7.2",
43 | "typescript-eslint": "^8.17.0",
44 | "wrangler": "^3.91.0"
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/packages/barqode/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
3 | # Output
4 | .output
5 | .vercel
6 | /.svelte-kit
7 | /build
8 | /dist
9 |
10 | # OS
11 | .DS_Store
12 | Thumbs.db
13 |
14 | # Env
15 | .env
16 | .env.*
17 | !.env.example
18 | !.env.test
19 |
20 | # Vite
21 | vite.config.js.timestamp-*
22 | vite.config.ts.timestamp-*
23 |
--------------------------------------------------------------------------------
/packages/barqode/.npmrc:
--------------------------------------------------------------------------------
1 | engine-strict=true
2 |
--------------------------------------------------------------------------------
/packages/barqode/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # barqode
2 |
3 | ## 0.0.2
4 |
5 | ### Patch Changes
6 |
7 | - expose `*ref` props for access to the underlying elements ([#5](https://github.com/svecosystem/barqode/pull/5))
8 |
9 | ## 0.0.1
10 |
11 | ### Patch Changes
12 |
13 | - Initial release ([#3](https://github.com/svecosystem/barqode/pull/3))
14 |
--------------------------------------------------------------------------------
/packages/barqode/README.md:
--------------------------------------------------------------------------------
1 | # Barqode
2 |
--------------------------------------------------------------------------------
/packages/barqode/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "barqode",
3 | "version": "0.0.2",
4 | "scripts": {
5 | "build": "pnpm package",
6 | "dev": "svelte-package --watch",
7 | "dev:svelte": "vite dev",
8 | "package": "svelte-kit sync && svelte-package && publint",
9 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json"
10 | },
11 | "files": [
12 | "dist",
13 | "!dist/**/*.test.*",
14 | "!dist/**/*.spec.*"
15 | ],
16 | "sideEffects": [
17 | "**/*.css"
18 | ],
19 | "repository": {
20 | "type": "git",
21 | "url": "https://github.com/svecosystem/barqode.git",
22 | "directory": "packages/barqode"
23 | },
24 | "funding": [
25 | "https://github.com/sponsors/huntabyte",
26 | "https://ko-fi.com/ollema"
27 | ],
28 | "svelte": "./dist/index.js",
29 | "types": "./dist/index.d.ts",
30 | "type": "module",
31 | "exports": {
32 | ".": {
33 | "types": "./dist/index.d.ts",
34 | "svelte": "./dist/index.js"
35 | }
36 | },
37 | "peerDependencies": {
38 | "svelte": "^5.0.0"
39 | },
40 | "devDependencies": {
41 | "@sveltejs/adapter-auto": "^3.3.1",
42 | "@sveltejs/kit": "^2.9.0",
43 | "@sveltejs/package": "^2.3.7",
44 | "@sveltejs/vite-plugin-svelte": "^4.0.2",
45 | "publint": "^0.2.12",
46 | "svelte": "^5.4.0",
47 | "svelte-check": "^4.1.0",
48 | "typescript": "^5.7.2",
49 | "vite": "^5.4.11"
50 | },
51 | "license": "MIT",
52 | "contributors": [
53 | {
54 | "name": "Olle",
55 | "url": "https://github.com/ollema"
56 | },
57 | {
58 | "name": "Hunter Johnston",
59 | "url": "https://github.com/huntabyte"
60 | }
61 | ],
62 | "dependencies": {
63 | "barcode-detector": "^2.3.1",
64 | "runed": "^0.16.1",
65 | "webrtc-adapter": "^9.0.1"
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/packages/barqode/src/app.d.ts:
--------------------------------------------------------------------------------
1 | // See https://svelte.dev/docs/kit/types#app.d.ts
2 | // for information about these interfaces
3 | declare global {
4 | namespace App {
5 | // interface Error {}
6 | // interface Locals {}
7 | // interface PageData {}
8 | // interface PageState {}
9 | // interface Platform {}
10 | }
11 | }
12 |
13 | export {};
14 |
--------------------------------------------------------------------------------
/packages/barqode/src/app.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | %sveltekit.head%
8 |
9 |
10 |
219 |
220 |
247 |
--------------------------------------------------------------------------------
/packages/barqode/src/lib/components/types.ts:
--------------------------------------------------------------------------------
1 | import type { BarcodeFormat, DetectedBarcode, Point2D } from "barcode-detector/pure";
2 | import type { Snippet } from "svelte";
3 | import type { HTMLInputAttributes } from "svelte/elements";
4 |
5 | export type { DetectedBarcode, BarcodeFormat, Point2D };
6 |
7 | export type DropzoneProps = {
8 | /**
9 | * The formats of the barcodes to detect.
10 | *
11 | * @default ['qr_code']
12 | *
13 | * @see https://developer.mozilla.org/en-US/docs/Web/API/Barcode_Detection_API#supported_barcode_formats
14 | */
15 | formats?: BarcodeFormat[];
16 |
17 | /**
18 | * A callback function called when the user drops an image file.
19 | *
20 | * @param codes - The detected barcodes.
21 | */
22 | onDetect?: (detectedCodes: DetectedBarcode[]) => void;
23 |
24 | /**
25 | * A callback function called when the user drags an image file over the drop zone.
26 | *
27 | * @param isDraggingOver - Whether the user is dragging an image file over the drop zone.
28 | */
29 | onDragover?: (isDraggingOver: boolean) => void;
30 |
31 | /**
32 | * A callback function called when the user drags an image file out of the drop zone.
33 | *
34 | * @param error - The error that occurred.
35 | */
36 | onError?: (error: Error) => void;
37 |
38 | /**
39 | * Optional prop for content to overlay on top of the drop zone.
40 | */
41 | children?: Snippet;
42 |
43 | /**
44 | * A reference to the underlying `input` element.
45 | *
46 | * @bindable
47 | *
48 | */
49 | inputRef?: HTMLInputElement | null;
50 |
51 | /**
52 | * A reference to the wrapping `div` element.
53 | *
54 | * @bindable
55 | */
56 | wrapperRef?: HTMLElement | null;
57 | } & HTMLInputAttributes;
58 |
59 | export type StreamProps = {
60 | /**
61 | * The MediaTrackConstraints specifying the desired media types and their constraints.
62 | *
63 | * @default { video: { facingMode: 'environment' } }
64 | *
65 | * @see https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints
66 | */
67 | constraints?: MediaTrackConstraints;
68 |
69 | /**
70 | * The formats of the barcodes to detect.
71 | *
72 | * @default ['qr_code']
73 | *
74 | * @see https://developer.mozilla.org/en-US/docs/Web/API/Barcode_Detection_API#supported_barcode_formats
75 | */
76 | formats?: BarcodeFormat[];
77 |
78 | /**
79 | * Whether the camera stream is paused. Bindable.
80 | *
81 | * @default false
82 | *
83 | * Can also be set to `true` to pause the camera stream, which is useful when you want to show
84 | * some microinteraction after successful scans. When the you set it to `false`, the camera
85 | * stream will be restarted and the `onCameraOn` callback function will be triggered again.
86 | */
87 | paused?: boolean;
88 |
89 | /**
90 | * Whether the torch is enabled.
91 | *
92 | * @default false
93 | *
94 | * Not consistently supported across devices.
95 | */
96 | torch?: boolean;
97 |
98 | /**
99 | * Function to visually highlight the detected barcodes.
100 | *
101 | * @param codes - The detected barcodes with their adjusted corner points.
102 | * @param ctx - The canvas rendering context.
103 | */
104 | track?: (detectedCodes: DetectedBarcode[], ctx: CanvasRenderingContext2D) => void;
105 |
106 | /**
107 | * A callback function called when the camera stream is turned on.
108 | *
109 | * @param capabilities - The MediaTrackCapabilities of the camera stream.
110 | */
111 | onCameraOn?: (capabilities: MediaTrackCapabilities) => void;
112 |
113 | /**
114 | * A callback function called when the camera stream encounters an error.
115 | *
116 | * @param error - The error that occurred.
117 | */
118 | onError?: (error: Error) => void;
119 |
120 | /**
121 | * A callback function called when the camera stream is turned off.
122 | */
123 | onCameraOff?: () => void;
124 |
125 | /**
126 | * A callback function called a detection is made.
127 | *
128 | * Note: if you scan the same barcode code multiple times in a row, onDetect is still called once.
129 | * When you hold a barcode in the camera, frames are actually decoded multiple times a second
130 | * but you don't want to be flooded with onDetect callbacks that often.
131 | * That's why the last decoded barcode is always cached and only new results are propagated.
132 | * However changing the value of `paused` resets this internal cache.
133 | */
134 | onDetect?: (detectedCodes: DetectedBarcode[]) => void;
135 |
136 | /**
137 | * Optional prop for content to overlay on top of the camera stream.
138 | */
139 | children?: Snippet;
140 |
141 | /**
142 | * A reference to the underlying `video` element.
143 | *
144 | * @bindable
145 | *
146 | */
147 | videoRef?: HTMLVideoElement | null;
148 |
149 | /**
150 | * A reference to the wrapping `div` element.
151 | *
152 | * @bindable
153 | */
154 | wrapperRef?: HTMLElement | null;
155 |
156 | /**
157 | * A reference to the pause frame `canvas` element.
158 | *
159 | * @bindable
160 | */
161 | pauseRef?: HTMLCanvasElement | null;
162 |
163 | /**
164 | * A reference to the tracking frame `canvas` element.
165 | *
166 | * @bindable
167 | */
168 | trackingRef?: HTMLCanvasElement | null;
169 | };
170 |
--------------------------------------------------------------------------------
/packages/barqode/src/lib/index.ts:
--------------------------------------------------------------------------------
1 | export { default as BarqodeDropzone } from "./components/barqode-dropzone.svelte";
2 | export { default as BarqodeStream } from "./components/barqode-stream.svelte";
3 |
4 | export type {
5 | DetectedBarcode,
6 | BarcodeFormat,
7 | DropzoneProps,
8 | StreamProps,
9 | } from "./components/types.js";
10 |
--------------------------------------------------------------------------------
/packages/barqode/src/lib/internal/callforth.ts:
--------------------------------------------------------------------------------
1 | // based on vue-qrcode-reader by Niklas Gruhn
2 | // see https://github.com/gruhn/vue-qrcode-reader
3 |
4 | /**
5 | * Creates a Promise that resolves when a specified event is triggered on the given EventTarget.
6 | *
7 | * @param eventTarget - The target to listen for events on.
8 | * @param successEvent - The name of the event that will resolve the Promise.
9 | * @param errorEvent - The name of the event that will reject the Promise. Defaults to 'error'.
10 | *
11 | * @returns A Promise that resolves with the event object when the successEvent is
12 | * triggered, or rejects with the event object when the errorEvent is triggered.
13 | */
14 | export function eventOn(
15 | eventTarget: EventTarget,
16 | successEvent: string,
17 | errorEvent = "error"
18 | ): Promise {
19 | let $resolve: (value: Event) => void;
20 | let $reject: (reason?: Event) => void;
21 |
22 | const promise = new Promise(
23 | (resolve: (value: Event) => void, reject: (reason?: Event) => void) => {
24 | $resolve = resolve;
25 | $reject = reject;
26 |
27 | eventTarget.addEventListener(successEvent, $resolve);
28 | eventTarget.addEventListener(errorEvent, $reject);
29 | }
30 | );
31 |
32 | promise.finally(() => {
33 | eventTarget.removeEventListener(successEvent, $resolve);
34 | eventTarget.removeEventListener(errorEvent, $reject);
35 | });
36 |
37 | return promise;
38 | }
39 |
40 | /**
41 | * Creates a promise that resolves after a specified number of milliseconds.
42 | *
43 | * @param ms - The number of milliseconds to wait before the promise resolves.
44 | *
45 | * @returns A promise that resolves after the specified delay.
46 | */
47 | export function sleep(ms: number) {
48 | return new Promise((resolve: (value: unknown) => void) => setTimeout(resolve, ms));
49 | }
50 |
--------------------------------------------------------------------------------
/packages/barqode/src/lib/internal/camera.ts:
--------------------------------------------------------------------------------
1 | // based on vue-qrcode-reader by Niklas Gruhn
2 | // see https://github.com/gruhn/vue-qrcode-reader
3 |
4 | import {
5 | StreamApiNotSupportedError,
6 | InsecureContextError,
7 | StreamLoadTimeoutError,
8 | } from "./errors.js";
9 | import { eventOn, sleep } from "./callforth.js";
10 | import shimGetUserMedia from "./shim-get-user-media.js";
11 | import { assertNever } from "./util.js";
12 |
13 | type StartTaskResult = {
14 | type: "start";
15 | data: {
16 | video: HTMLVideoElement;
17 | stream: MediaStream;
18 | capabilities: Partial;
19 | constraints: MediaTrackConstraints;
20 | isTorchOn: boolean;
21 | };
22 | };
23 |
24 | type StopTaskResult = {
25 | type: "stop";
26 | data: null;
27 | };
28 |
29 | type FailedTask = {
30 | type: "failed";
31 | error: Error;
32 | };
33 |
34 | type TaskResult = StartTaskResult | StopTaskResult | FailedTask;
35 |
36 | let taskQueue: Promise = Promise.resolve({ type: "stop", data: null });
37 |
38 | type CreateObjectURLCompat = (obj: MediaSource | Blob | MediaStream) => string;
39 |
40 | /**
41 | * Starts the camera with the given constraints and attaches the stream to the provided video element.
42 | *
43 | * @param video - The HTML video element to which the camera stream will be attached.
44 | * @param constraints - The media track constraints to apply when starting the camera.
45 | * @param torch - A boolean indicating whether the torch (flashlight) should be enabled if supported.
46 | *
47 | * @returns A promise that resolves to a `StartTaskResult` object containing details about the started camera stream.
48 | *
49 | * @throws InsecureContextError - If the page is not loaded in a secure context (HTTPS).
50 | * @throws StreamApiNotSupportedError - If the Stream API is not supported by the browser.
51 | * @throws StreamLoadTimeoutError - If the video element fails to load the camera stream within a 6-second timeout.
52 | */
53 | async function runStartTask(
54 | video: HTMLVideoElement,
55 | constraints: MediaTrackConstraints,
56 | torch: boolean
57 | ): Promise {
58 | console.debug("[barqode] starting camera with constraints: ", JSON.stringify(constraints));
59 |
60 | // at least in Chrome `navigator.mediaDevices` is undefined when the page is
61 | // loaded using HTTP rather than HTTPS. thus `STREAM_API_NOT_SUPPORTED` is
62 | // initialized with `false` although the API might actually be supported.
63 | // so although `getUserMedia` already should have a built-in mechanism to
64 | // detect insecure context (by throwing `NotAllowedError`), we have to do a
65 | // manual check before even calling `getUserMedia`.
66 | if (window.isSecureContext !== true) {
67 | throw new InsecureContextError();
68 | }
69 |
70 | if (navigator?.mediaDevices?.getUserMedia === undefined) {
71 | throw new StreamApiNotSupportedError();
72 | }
73 |
74 | // this is a browser API only shim. tt patches the global window object which
75 | // is not available during SSR. So we lazily apply this shim at runtime.
76 | shimGetUserMedia();
77 |
78 | console.debug("[barqode] calling getUserMedia");
79 | const stream = await navigator.mediaDevices.getUserMedia({
80 | audio: false,
81 | video: constraints,
82 | });
83 |
84 | if (video.srcObject !== undefined) {
85 | video.srcObject = stream;
86 | } else if (video.mozSrcObject !== undefined) {
87 | video.mozSrcObject = stream;
88 | } else if (window.URL.createObjectURL) {
89 | video.src = (window.URL.createObjectURL as CreateObjectURLCompat)(stream);
90 | } else if (window.webkitURL) {
91 | video.src = (window.webkitURL.createObjectURL as CreateObjectURLCompat)(stream);
92 | } else {
93 | video.src = stream.id;
94 | }
95 |
96 | // in the WeChat browser on iOS, 'loadeddata' event won't get fired unless video is explicitly triggered by play()
97 | video.play();
98 |
99 | console.debug("[barqode] waiting for video element to load");
100 | await Promise.race([
101 | eventOn(video, "loadeddata"),
102 |
103 | // on iOS devices in PWA mode, BarqodeStream works initially, but after killing and restarting the PWA,
104 | // all video elements fail to load camera streams and never emit the `loadeddata` event.
105 | // looks like this is related to a WebKit issue (see #298). no workarounds at the moment.
106 | // to at least detect this situation, we throw an error if the event has not been emitted after a 6 second timeout.
107 | sleep(6_000).then(() => {
108 | throw new StreamLoadTimeoutError();
109 | }),
110 | ]);
111 | console.debug("[barqode] video element loaded");
112 |
113 | // according to: https://oberhofer.co/mediastreamtrack-and-its-capabilities/#queryingcapabilities
114 | // on some devices, getCapabilities only returns a non-empty object after some delay.
115 | // there is no appropriate event so we have to add a constant timeout
116 | await sleep(500);
117 |
118 | const [track] = stream.getVideoTracks();
119 |
120 | const capabilities: Partial = track?.getCapabilities?.() ?? {};
121 |
122 | let isTorchOn = false;
123 | // @ts-expect-error torch is not in the MediaTrackConstraints type but it should be?
124 | if (torch && capabilities.torch) {
125 | // @ts-expect-error torch is not in the MediaTrackConstraints type but it should be?
126 | await track.applyConstraints({ advanced: [{ torch: true }] });
127 | isTorchOn = true;
128 | }
129 |
130 | console.debug("[barqode] camera ready");
131 |
132 | return {
133 | type: "start",
134 | data: {
135 | video,
136 | stream,
137 | capabilities,
138 | constraints,
139 | isTorchOn,
140 | },
141 | };
142 | }
143 |
144 | /**
145 | * Starts the camera with the given video element and settings.
146 | *
147 | * @param video - The HTML video element to which the camera stream will be attached.
148 | * @param options.constraints - The media track constraints to apply when starting the camera.
149 | * @param options.torch - A boolean indicating whether the torch (flashlight) should be enabled if supported.
150 | * @param options.restart - A boolean indicating whether to restart the camera even if no settings changed. Defaults to false.
151 | *
152 | * @returns A promise that resolves to a `MediaTrackCapabilities` object containing the camera capabilities.
153 | *
154 | * @throws Error - If something goes wrong with the camera task queue.
155 | */
156 | export async function start(
157 | video: HTMLVideoElement,
158 | {
159 | constraints,
160 | torch,
161 | restart = false,
162 | }: {
163 | constraints: MediaTrackConstraints;
164 | torch: boolean;
165 | restart?: boolean;
166 | }
167 | ): Promise> {
168 | // update the task queue synchronously
169 | taskQueue = taskQueue
170 | .then((prevTaskResult) => {
171 | if (prevTaskResult.type === "start") {
172 | // previous task is a start task
173 | // we'll check if we can reuse the previous result
174 | const {
175 | data: {
176 | video: prevVideo,
177 | stream: prevStream,
178 | constraints: prevConstraints,
179 | isTorchOn: prevIsTorchOn,
180 | },
181 | } = prevTaskResult;
182 | // TODO: should we keep this object comparison
183 | // this code only checks object sameness not equality
184 | // deep comparison requires snapshots and value by value check
185 | // which seem too much
186 | if (
187 | !restart &&
188 | video === prevVideo &&
189 | constraints === prevConstraints &&
190 | torch === prevIsTorchOn
191 | ) {
192 | // things didn't change, reuse the previous result
193 | return prevTaskResult;
194 | }
195 | // something changed, restart (stop then start)
196 | return runStopTask(prevVideo, prevStream, prevIsTorchOn).then(() =>
197 | runStartTask(video, constraints, torch)
198 | );
199 | } else if (prevTaskResult.type === "stop" || prevTaskResult.type === "failed") {
200 | // previous task is a stop/error task
201 | // we can safely start
202 | return runStartTask(video, constraints, torch);
203 | }
204 |
205 | assertNever(prevTaskResult);
206 | })
207 | .catch((error: Error) => {
208 | console.debug(`[barqode] starting camera failed with "${error}"`);
209 | return { type: "failed", error };
210 | });
211 |
212 | // await the task queue asynchronously
213 | const taskResult = await taskQueue;
214 |
215 | if (taskResult.type === "stop") {
216 | // we just synchronously updated the task above
217 | // to make the latest task a start task
218 | // so this case shouldn't happen
219 | throw new Error("Something went wrong with the camera task queue (start task).");
220 | } else if (taskResult.type === "failed") {
221 | throw taskResult.error;
222 | } else if (taskResult.type === "start") {
223 | // return the data we want
224 | return taskResult.data.capabilities;
225 | }
226 |
227 | assertNever(taskResult);
228 | }
229 |
230 | /**
231 | * Stops the camera stream and cleans up associated resources.
232 | *
233 | * @param video - The HTML video element displaying the camera stream.
234 | * @param stream - The MediaStream object representing the active camera stream.
235 | * @param isTorchOn - A boolean indicating whether the torch is currently enabled.
236 | *
237 | * @returns A promise that resolves to a `StopTaskResult` when the camera is fully stopped.
238 | */
239 | async function runStopTask(
240 | video: HTMLVideoElement,
241 | stream: MediaStream,
242 | isTorchOn: boolean
243 | ): Promise {
244 | console.debug("[barqode] stopping camera");
245 |
246 | video.src = "";
247 | video.srcObject = null;
248 | video.load();
249 |
250 | // wait for load() to emit error
251 | // because src and srcObject are empty
252 | await eventOn(video, "error");
253 |
254 | for (const track of stream.getTracks()) {
255 | // @ts-expect-error torch is not in the MediaTrackConstraints type but it should be?
256 | isTorchOn ?? (await track.applyConstraints({ advanced: [{ torch: false }] }));
257 | stream.removeTrack(track);
258 | track.stop();
259 | }
260 |
261 | return {
262 | type: "stop",
263 | data: null,
264 | };
265 | }
266 |
267 | /**
268 | * Stops any active camera stream and ensures proper cleanup.
269 | *
270 | * @returns A promise that resolves when the camera is fully stopped.
271 | *
272 | * @throws Error - If something goes wrong with the camera task queue.
273 | */
274 | export async function stop() {
275 | // update the task queue synchronously
276 | taskQueue = taskQueue.then((prevTaskResult) => {
277 | if (prevTaskResult.type === "stop" || prevTaskResult.type === "failed") {
278 | // previous task is a stop task
279 | // no need to stop again
280 | return prevTaskResult;
281 | }
282 | const {
283 | data: { video, stream, isTorchOn },
284 | } = prevTaskResult;
285 | return runStopTask(video, stream, isTorchOn);
286 | });
287 | // await the task queue asynchronously
288 | const taskResult = await taskQueue;
289 | if (taskResult.type === "start") {
290 | // we just synchronously updated the task above
291 | // to make the latest task a stop task
292 | // so this case shouldn't happen
293 | throw new Error("Something went wrong with the camera task queue (stop task).");
294 | }
295 | }
296 |
--------------------------------------------------------------------------------
/packages/barqode/src/lib/internal/errors.ts:
--------------------------------------------------------------------------------
1 | export class DropImageFetchError extends Error {
2 | constructor() {
3 | super("can't process cross-origin image");
4 |
5 | this.name = "DropImageFetchError";
6 | }
7 | }
8 |
9 | export class StreamApiNotSupportedError extends Error {
10 | constructor() {
11 | super("this browser has no Stream API support");
12 |
13 | this.name = "StreamApiNotSupportedError";
14 | }
15 | }
16 |
17 | export class InsecureContextError extends Error {
18 | constructor() {
19 | super(
20 | "camera access is only permitted in secure context. Use HTTPS or localhost rather than HTTP."
21 | );
22 |
23 | this.name = "InsecureContextError";
24 | }
25 | }
26 |
27 | export class StreamLoadTimeoutError extends Error {
28 | constructor() {
29 | super(
30 | "loading camera stream timed out after 6 seconds. If you are on iOS in PWA mode, this is a known issue (see https://github.com/gruhn/vue-qrcode-reader/issues/298)"
31 | );
32 |
33 | this.name = "StreamLoadTimeoutError";
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/packages/barqode/src/lib/internal/global.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | interface HTMLVideoElement {
4 | mozSrcObject?: HTMLVideoElement["srcObject"];
5 | }
6 |
--------------------------------------------------------------------------------
/packages/barqode/src/lib/internal/scanner.ts:
--------------------------------------------------------------------------------
1 | // based on vue-qrcode-reader by Niklas Gruhn
2 | // see https://github.com/gruhn/vue-qrcode-reader
3 |
4 | import { type DetectedBarcode, type BarcodeFormat, BarcodeDetector } from "barcode-detector/pure";
5 | import { eventOn } from "./callforth.js";
6 | import { DropImageFetchError } from "./errors.js";
7 |
8 | declare global {
9 | interface Window {
10 | BarcodeDetector?: typeof BarcodeDetector;
11 | }
12 | }
13 |
14 | /**
15 | * Singleton `BarcodeDetector` instance used by `BarqodeStream`. This is firstly to avoid
16 | * the overhead of creating a new instances for scanning each frame. And secondly, the
17 | * instances can seamlessly be replaced in the middle of the scanning process, if the
18 | * `formats` prop of `BarqodeStream` is changed.
19 | *
20 | * This instance is not used by `BarqodeCapture` and `BarqodeDropzone`, because it may not
21 | * have the right `formats` configured. For these components we create one-off `BarcodeDetector`
22 | * instances because it does not happen so frequently anyway (see: `processFile`/`processUrl`).
23 | */
24 | let barcodeDetector: BarcodeDetector;
25 |
26 | /**
27 | * Constructs a `BarcodeDetector` instance, given a list of targeted barcode formats.
28 | * Preferably, we want to use the native `BarcodeDetector` implementation if supported.
29 | * Otherwise, we fall back to the polyfill implementation.
30 | *
31 | * Note, that we can't just monkey patch the polyfill on load, i.e.
32 | *
33 | * window.BarcodeDetector ??= BarcodeDetector
34 | *
35 | * for two reasons. Firstly, this is not SSR compatible, because `window` is not available
36 | * during SSR. Secondly, even if the native implementation is available, we still might
37 | * want to use the polyfill. For example, if the native implementation only supports the
38 | * format `"qr_code"` but the user wants to scan `["qr_code", "aztec"]` (see #450).
39 | */
40 | async function createBarcodeDetector(formats: BarcodeFormat[]): Promise {
41 | if (window.BarcodeDetector === undefined) {
42 | console.debug("[barqode] native BarcodeDetector not supported. Will use polyfill.");
43 | return new BarcodeDetector({ formats });
44 | }
45 |
46 | const allSupportedFormats = await window.BarcodeDetector.getSupportedFormats();
47 | const unsupportedFormats = formats.filter((format) => !allSupportedFormats.includes(format));
48 |
49 | if (unsupportedFormats.length > 0) {
50 | console.debug(
51 | `[barqode] native BarcodeDetector does not support formats ${JSON.stringify(unsupportedFormats)}. Will use polyfill.`
52 | );
53 | return new BarcodeDetector({ formats });
54 | }
55 |
56 | console.debug("[barqode] will use native BarcodeDetector.");
57 | return new window.BarcodeDetector({ formats });
58 | }
59 |
60 | /**
61 | * Update the set of targeted barcode formats. In particular, this function
62 | * can be called during scanning and the camera stream doesn't have to be
63 | * interrupted.
64 | */
65 | export async function setScanningFormats(formats: BarcodeFormat[]) {
66 | barcodeDetector = await createBarcodeDetector(formats);
67 | }
68 |
69 | type ScanHandler = (_: DetectedBarcode[]) => void;
70 |
71 | type KeepScanningOptions = {
72 | detectHandler: ScanHandler;
73 | locateHandler: ScanHandler;
74 | minDelay: number;
75 | formats: BarcodeFormat[];
76 | };
77 |
78 | /**
79 | * Continuously extracts frames from camera stream and tries to read
80 | * potentially pictured QR codes.
81 | */
82 | export async function keepScanning(
83 | videoElement: HTMLVideoElement,
84 | { detectHandler, locateHandler, minDelay, formats }: KeepScanningOptions
85 | ) {
86 | console.debug("[barqode] start scanning");
87 | await setScanningFormats(formats);
88 |
89 | const processFrame =
90 | (state: { lastScanned: number; contentBefore: string[]; lastScanHadContent: boolean }) =>
91 | async (timeNow: number) => {
92 | if (videoElement.readyState === 0) {
93 | console.debug("[barqode] stop scanning: video element readyState is 0");
94 | } else {
95 | const { lastScanned, contentBefore, lastScanHadContent } = state;
96 |
97 | // Scanning is expensive and we don't need to scan camera frames with
98 | // the maximum possible frequency. In particular when visual tracking
99 | // is disabled. So we skip scanning a frame if `minDelay` has not passed
100 | // yet. Notice that this approach is different from doing a `setTimeout`
101 | // after each scan. With `setTimeout`, delay and scanning are sequential:
102 | //
103 | // |-- scan --|---- minDelay ----|-- scan --|---- minDelay ----|
104 | //
105 | // Instead we do it concurrently:
106 | //
107 | // |---- minDelay ----|---- minDelay ----|---- minDelay ----|
108 | // |-- scan --| |-- scan --| |-- scan --|
109 | //
110 | // Let's say `minDelay` is 40ms, then we scan every 40ms as long as
111 | // scanning itself does not take more than 40ms. In particular when
112 | // visual tracking is enabled, that means we can repaint the tracking
113 | // canvas every 40ms. So we paint
114 | //
115 | // 1000ms / 40ms = 25fps (frames per second)
116 | //
117 | // 24fps is the minimum frame-rate that is perceived as a continuous
118 | // animation. We target 25fps just because 24 doesn't divide 1000ms
119 | // evenly.
120 | if (timeNow - lastScanned < minDelay) {
121 | window.requestAnimationFrame(processFrame(state));
122 | } else {
123 | const detectedCodes = await barcodeDetector.detect(videoElement);
124 |
125 | // Only emit a detect event, if at least one of the detected codes has
126 | // not been seen before. Otherwise we spam tons of detect events while
127 | // a QR code is in view of the camera. To avoid that we store the previous
128 | // detection in `contentBefore`.
129 | //
130 | // Implicitly we also don't emit a `detect` event if `detectedCodes` is an
131 | // empty array.
132 | const anyNewCodesDetected = detectedCodes.some((code) => {
133 | return !contentBefore.includes(code.rawValue);
134 | });
135 |
136 | if (anyNewCodesDetected) {
137 | detectHandler(detectedCodes);
138 | }
139 |
140 | const currentScanHasContent = detectedCodes.length > 0;
141 |
142 | // In contrast to the QR code content, the location changes all the time.
143 | // So we call the locate handler on every detection to repaint the tracking
144 | // canvas.
145 | if (currentScanHasContent) {
146 | locateHandler(detectedCodes);
147 | }
148 |
149 | // Additionally, we need to clear the tracking canvas once when no QR code
150 | // is in view of the camera anymore. Technically this can be merged with the
151 | // previous if-statement but this way it's more explicit.
152 | if (!currentScanHasContent && lastScanHadContent) {
153 | locateHandler(detectedCodes);
154 | }
155 |
156 | const newState = {
157 | lastScanned: timeNow,
158 | lastScanHadContent: currentScanHasContent,
159 |
160 | // It can happen that a QR code is constantly in view of the camera but
161 | // maybe a scanned frame is a bit blurry and we detect nothing but in the
162 | // next frame we detect the code again. We also want to avoid emitting
163 | // a `detect` event in such a case. So we don't reset `contentBefore`,
164 | // if we detect nothing, only if we detect something new.
165 | contentBefore: anyNewCodesDetected
166 | ? detectedCodes.map((code) => code.rawValue)
167 | : contentBefore,
168 | };
169 |
170 | window.requestAnimationFrame(processFrame(newState));
171 | }
172 | }
173 | };
174 |
175 | processFrame({
176 | lastScanned: performance.now(),
177 | contentBefore: [],
178 | lastScanHadContent: false,
179 | })(performance.now());
180 | }
181 |
182 | async function imageElementFromUrl(url: string) {
183 | if (url.startsWith("http") && url.includes(location.host) === false) {
184 | throw new DropImageFetchError();
185 | }
186 |
187 | const image = document.createElement("img");
188 | image.src = url;
189 |
190 | await eventOn(image, "load");
191 |
192 | return image;
193 | }
194 |
195 | export async function processFile(
196 | file: File,
197 | formats: BarcodeFormat[] = ["qr_code"]
198 | ): Promise {
199 | // To scan files/urls we use one-off `BarcodeDetector` instances,
200 | // since we don't scan as often as camera frames. Note, that we
201 | // always use the polyfill. This is because (at the time of writing)
202 | // some browser/OS combinations don't support `Blob`/`File` inputs
203 | // into the `detect` function.
204 | const barcodeDetector = new BarcodeDetector({ formats });
205 |
206 | return await barcodeDetector.detect(file);
207 | }
208 |
209 | export async function processUrl(
210 | url: string,
211 | formats: BarcodeFormat[] = ["qr_code"]
212 | ): Promise {
213 | // To scan files/urls we use one-off `BarcodeDetector` instances,
214 | // since we don't scan as often as camera frames. Note, that we
215 | // always use the polyfill. This is because (at the time of writing)
216 | // some browser/OS combinations don't support `Blob`/`File` inputs
217 | // into the `detect` function.
218 | const barcodeDetector = new BarcodeDetector({ formats });
219 |
220 | const image = await imageElementFromUrl(url);
221 |
222 | return await barcodeDetector.detect(image);
223 | }
224 |
--------------------------------------------------------------------------------
/packages/barqode/src/lib/internal/shim-get-user-media.ts:
--------------------------------------------------------------------------------
1 | // based on vue-qrcode-reader by Niklas Gruhn
2 | // see https://github.com/gruhn/vue-qrcode-reader
3 |
4 | // @ts-expect-error no types available
5 | import { shimGetUserMedia as chromeShim } from "webrtc-adapter/dist/chrome/getusermedia";
6 | // @ts-expect-error no types available
7 | import { shimGetUserMedia as firefoxShim } from "webrtc-adapter/dist/firefox/getusermedia";
8 | // @ts-expect-error no types available
9 | import { shimGetUserMedia as safariShim } from "webrtc-adapter/dist/safari/safari_shim";
10 | // @ts-expect-error no types available
11 | import { detectBrowser } from "webrtc-adapter/dist/utils";
12 |
13 | import { StreamApiNotSupportedError } from "./errors.js";
14 | import { idempotent } from "./util.js";
15 |
16 | export default idempotent(() => {
17 | const browserDetails = detectBrowser(window);
18 |
19 | switch (browserDetails.browser) {
20 | case "chrome":
21 | chromeShim(window, browserDetails);
22 | break;
23 | case "firefox":
24 | firefoxShim(window, browserDetails);
25 | break;
26 | case "safari":
27 | safariShim(window, browserDetails);
28 | break;
29 | default:
30 | throw new StreamApiNotSupportedError();
31 | }
32 | });
33 |
--------------------------------------------------------------------------------
/packages/barqode/src/lib/internal/util.ts:
--------------------------------------------------------------------------------
1 | // based on vue-qrcode-reader by Niklas Gruhn
2 | // see https://github.com/gruhn/vue-qrcode-reader
3 |
4 | /**
5 | * Creates a function that ensures the given action is only executed once.
6 | * Subsequent calls to the returned function will return the result of the first call.
7 | *
8 | * @template T - The return type of the action.
9 | * @template U - The type of the arguments passed to the action.
10 | * @param {function(U[]): T} action - The action to be executed idempotently.
11 | * @returns {function(...U[]): T | undefined} A function that, when called, will execute the action only once and return the result.
12 | */
13 | export const idempotent = (action: (x: U[]) => T) => {
14 | let called = false;
15 | let result: T | undefined = undefined;
16 |
17 | return (...args: U[]) => {
18 | if (called) {
19 | return result;
20 | } else {
21 | result = action(args);
22 | called = true;
23 |
24 | return result;
25 | }
26 | };
27 | };
28 |
29 | /**
30 | * Asserts that a given value is of type `never`, indicating that this code path should be unreachable.
31 | * Throws an error if called, signaling a logic error in the code.
32 | *
33 | * @param _witness - The value that should be of type `never`.
34 | * @throws {Error} Always throws an error to indicate unreachable code.
35 | */
36 | export function assertNever(_witness: never): never {
37 | throw new Error("this code should be unreachable");
38 | }
39 |
--------------------------------------------------------------------------------
/packages/barqode/svelte.config.js:
--------------------------------------------------------------------------------
1 | import adapter from "@sveltejs/adapter-auto";
2 | import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
3 |
4 | /** @type {import('@sveltejs/kit').Config} */
5 | const config = {
6 | // Consult https://svelte.dev/docs/kit/integrations
7 | // for more information about preprocessors
8 | preprocess: vitePreprocess(),
9 |
10 | kit: {
11 | // adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.
12 | // If your environment is not supported, or you settled on a specific environment, switch out the adapter.
13 | // See https://svelte.dev/docs/kit/adapters for more information about adapters.
14 | adapter: adapter(),
15 | },
16 | };
17 |
18 | export default config;
19 |
--------------------------------------------------------------------------------
/packages/barqode/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./.svelte-kit/tsconfig.json",
3 | "compilerOptions": {
4 | "allowJs": true,
5 | "checkJs": true,
6 | "esModuleInterop": true,
7 | "forceConsistentCasingInFileNames": true,
8 | "resolveJsonModule": true,
9 | "skipLibCheck": true,
10 | "sourceMap": true,
11 | "strict": true,
12 | "module": "NodeNext",
13 | "moduleResolution": "NodeNext"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/packages/barqode/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { sveltekit } from "@sveltejs/kit/vite";
2 | import { defineConfig } from "vite";
3 |
4 | export default defineConfig({
5 | plugins: [sveltekit()],
6 | });
7 |
--------------------------------------------------------------------------------
/pnpm-workspace.yaml:
--------------------------------------------------------------------------------
1 | packages:
2 | - "packages/*"
3 | - "docs"
4 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./docs/tsconfig.json"
3 | }
4 |
--------------------------------------------------------------------------------