From 978c1a14d8b939d8b2dc9cbb9d92ef08643cbfea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olle=20M=C3=A5nsson?= <31876997+ollema@users.noreply.github.com> Date: Thu, 25 Jan 2024 01:48:20 +0100 Subject: [PATCH] Initial release --- .changeset/README.md | 8 ++ .changeset/config.json | 11 ++ .changeset/smooth-islands-look.md | 5 + .github/ci.yml | 36 +++++++ .github/release.yml | 41 ++++++++ package.json | 34 +++--- pnpm-lock.yaml | 73 +++++++++---- src/lib/barcode-scanner.svelte | 112 ++++++++++++++++++++ src/lib/barcode-scanner.ts | 165 ++++++++++++++++++++++++++++++ src/lib/index.ts | 2 +- src/lib/types.ts | 15 +++ src/routes/+layout.svelte | 12 +++ src/routes/+page.svelte | 103 ++++++++++++++++++- 13 files changed, 578 insertions(+), 39 deletions(-) create mode 100644 .changeset/README.md create mode 100644 .changeset/config.json create mode 100644 .changeset/smooth-islands-look.md create mode 100644 .github/ci.yml create mode 100644 .github/release.yml create mode 100644 src/lib/barcode-scanner.svelte create mode 100644 src/lib/barcode-scanner.ts create mode 100644 src/lib/types.ts create mode 100644 src/routes/+layout.svelte diff --git a/.changeset/README.md b/.changeset/README.md new file mode 100644 index 0000000..e5b6d8d --- /dev/null +++ b/.changeset/README.md @@ -0,0 +1,8 @@ +# Changesets + +Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works +with multi-package repos, or single-package repos to help you version and publish your code. You can +find the full documentation for it [in our repository](https://github.com/changesets/changesets) + +We have a quick list of common questions to get you started engaging with this project in +[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) diff --git a/.changeset/config.json b/.changeset/config.json new file mode 100644 index 0000000..91b6a95 --- /dev/null +++ b/.changeset/config.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json", + "changelog": "@changesets/cli/changelog", + "commit": false, + "fixed": [], + "linked": [], + "access": "restricted", + "baseBranch": "main", + "updateInternalDependencies": "patch", + "ignore": [] +} diff --git a/.changeset/smooth-islands-look.md b/.changeset/smooth-islands-look.md new file mode 100644 index 0000000..eecdb6d --- /dev/null +++ b/.changeset/smooth-islands-look.md @@ -0,0 +1,5 @@ +--- +"svelte-barcode-scanner": minor +--- + +Initial release diff --git a/.github/ci.yml b/.github/ci.yml new file mode 100644 index 0000000..6dbefcb --- /dev/null +++ b/.github/ci.yml @@ -0,0 +1,36 @@ +name: CI + +on: + push: + branches: + - main + pull_request: + +# cancel in-progress runs on new commits to same PR (gitub.event.number) +concurrency: + group: ${{ github.workflow }}-${{ github.event.number || github.sha }} + cancel-in-progress: true + +permissions: + contents: read # to fetch code (actions/checkout) + +jobs: + Lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: pnpm/action-setup@v2 + with: + version: 8.6.3 + run_install: true + - run: pnpm run lint + + Check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: pnpm/action-setup@v2 + with: + version: 8.6.3 + run_install: true + - run: pnpm run check diff --git a/.github/release.yml b/.github/release.yml new file mode 100644 index 0000000..311e287 --- /dev/null +++ b/.github/release.yml @@ -0,0 +1,41 @@ +name: Release + +on: + push: + branches: + - main + +concurrency: ${{ github.workflow }}-${{ github.ref }} + +jobs: + release: + permissions: + contents: write # to create release (changesets/action) + pull-requests: write # to create pull request (changesets/action) + name: Release + runs-on: ubuntu-latest + steps: + - name: Checkout Repo + uses: actions/checkout@v3 + with: + # This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits + fetch-depth: 0 + - uses: pnpm/action-setup@v2.2.4 + with: + version: 8.6.3 + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: '20.x' + cache: pnpm + + - run: pnpm install --frozen-lockfile + + - name: Create Release Pull Request or Publish to npm + id: changesets + uses: changesets/action@v1 + with: + publish: pnpm release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/package.json b/package.json index 9ea7c04..b3c3f1c 100644 --- a/package.json +++ b/package.json @@ -30,27 +30,31 @@ }, "devDependencies": { "@changesets/cli": "^2.27.1", - "@sveltejs/adapter-auto": "^3.0.0", - "@sveltejs/kit": "^2.0.0", - "@sveltejs/package": "^2.0.0", - "@sveltejs/vite-plugin-svelte": "^3.0.0", + "@sveltejs/adapter-auto": "^3.1.1", + "@sveltejs/kit": "^2.4.3", + "@sveltejs/package": "^2.2.6", + "@sveltejs/vite-plugin-svelte": "^3.0.1", "@svitejs/changesets-changelog-github-compact": "^1.1.0", - "@types/eslint": "8.56.0", - "@typescript-eslint/eslint-plugin": "^6.0.0", - "@typescript-eslint/parser": "^6.0.0", + "@types/eslint": "8.56.2", + "@typescript-eslint/eslint-plugin": "^6.19.1", + "@typescript-eslint/parser": "^6.19.1", "eslint": "^8.56.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-svelte": "^2.35.1", - "prettier": "^3.1.1", + "prettier": "^3.2.4", "prettier-plugin-svelte": "^3.1.2", - "publint": "^0.1.16", - "svelte": "^4.2.7", - "svelte-check": "^3.6.0", - "tslib": "^2.4.1", - "typescript": "^5.0.0", - "vite": "^5.0.11" + "publint": "^0.2.7", + "svelte": "^4.2.9", + "svelte-check": "^3.6.3", + "tslib": "^2.6.2", + "typescript": "^5.3.3", + "vite": "^5.0.12" }, "svelte": "./dist/index.js", "types": "./dist/index.d.ts", - "type": "module" + "type": "module", + "dependencies": { + "barcode-detector": "^2.2.2", + "rvfc-polyfill": "^1.0.7" + } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1e9fa1f..34c772d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,33 +4,41 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +dependencies: + barcode-detector: + specifier: ^2.2.2 + version: 2.2.2 + rvfc-polyfill: + specifier: ^1.0.7 + version: 1.0.7 + devDependencies: '@changesets/cli': specifier: ^2.27.1 version: 2.27.1 '@sveltejs/adapter-auto': - specifier: ^3.0.0 + specifier: ^3.1.1 version: 3.1.1(@sveltejs/kit@2.4.3) '@sveltejs/kit': - specifier: ^2.0.0 + specifier: ^2.4.3 version: 2.4.3(@sveltejs/vite-plugin-svelte@3.0.1)(svelte@4.2.9)(vite@5.0.12) '@sveltejs/package': - specifier: ^2.0.0 + specifier: ^2.2.6 version: 2.2.6(svelte@4.2.9)(typescript@5.3.3) '@sveltejs/vite-plugin-svelte': - specifier: ^3.0.0 + specifier: ^3.0.1 version: 3.0.1(svelte@4.2.9)(vite@5.0.12) '@svitejs/changesets-changelog-github-compact': specifier: ^1.1.0 version: 1.1.0 '@types/eslint': - specifier: 8.56.0 - version: 8.56.0 + specifier: 8.56.2 + version: 8.56.2 '@typescript-eslint/eslint-plugin': - specifier: ^6.0.0 + specifier: ^6.19.1 version: 6.19.1(@typescript-eslint/parser@6.19.1)(eslint@8.56.0)(typescript@5.3.3) '@typescript-eslint/parser': - specifier: ^6.0.0 + specifier: ^6.19.1 version: 6.19.1(eslint@8.56.0)(typescript@5.3.3) eslint: specifier: ^8.56.0 @@ -42,28 +50,28 @@ devDependencies: specifier: ^2.35.1 version: 2.35.1(eslint@8.56.0)(svelte@4.2.9) prettier: - specifier: ^3.1.1 + specifier: ^3.2.4 version: 3.2.4 prettier-plugin-svelte: specifier: ^3.1.2 version: 3.1.2(prettier@3.2.4)(svelte@4.2.9) publint: - specifier: ^0.1.16 - version: 0.1.16 + specifier: ^0.2.7 + version: 0.2.7 svelte: - specifier: ^4.2.7 + specifier: ^4.2.9 version: 4.2.9 svelte-check: - specifier: ^3.6.0 + specifier: ^3.6.3 version: 3.6.3(postcss@8.4.33)(svelte@4.2.9) tslib: - specifier: ^2.4.1 + specifier: ^2.6.2 version: 2.6.2 typescript: - specifier: ^5.0.0 + specifier: ^5.3.3 version: 5.3.3 vite: - specifier: ^5.0.11 + specifier: ^5.0.12 version: 5.0.12 packages: @@ -848,8 +856,16 @@ packages: resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} dev: true - /@types/eslint@8.56.0: - resolution: {integrity: sha512-FlsN0p4FhuYRjIxpbdXovvHQhtlG05O1GG/RNWvdAxTboR438IOTwmrY/vLA+Xfgg06BTkP045M3vpFwTMv1dg==} + /@types/dom-webcodecs@0.1.11: + resolution: {integrity: sha512-yPEZ3z7EohrmOxbk/QTAa0yonMFkNkjnVXqbGb7D4rMr+F1dGQ8ZUFxXkyLLJuiICPejZ0AZE9Rrk9wUCczx4A==} + dev: false + + /@types/emscripten@1.39.10: + resolution: {integrity: sha512-TB/6hBkYQJxsZHSqyeuO1Jt0AB/bW6G7rHt9g7lML7SOF6lbgcHvw/Lr+69iqN0qxgXLhWKScAon73JNnptuDw==} + dev: false + + /@types/eslint@8.56.2: + resolution: {integrity: sha512-uQDwm1wFHmbBbCZCqAlq6Do9LYwByNZHWzXppSnay9SuwJ+VRbjkbLABer54kcPnMSlG6Fdiy2yaFXm/z9Z5gw==} dependencies: '@types/estree': 1.0.5 '@types/json-schema': 7.0.15 @@ -1145,6 +1161,13 @@ packages: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} dev: true + /barcode-detector@2.2.2: + resolution: {integrity: sha512-JcSekql+EV93evfzF9zBr+Y6aRfkR+QFvgyzbwQ0dbymZXoAI9+WgT7H1E429f+3RKNncHz2CW98VQtaaKpmfQ==} + dependencies: + '@types/dom-webcodecs': 0.1.11 + zxing-wasm: 1.1.3 + dev: false + /better-path-resolve@1.0.0: resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} engines: {node: '>=4'} @@ -2874,8 +2897,8 @@ packages: resolution: {integrity: sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==} dev: true - /publint@0.1.16: - resolution: {integrity: sha512-wJgk7HnXDT5Ap0DjFYbGz78kPkN44iQvDiaq8P63IEEyNU9mYXvaMd2cAyIM6OgqXM/IA3CK6XWIsRq+wjNpgw==} + /publint@0.2.7: + resolution: {integrity: sha512-tLU4ee3110BxWfAmCZggJmCUnYWgPTr0QLnx08sqpLYa8JHRiOudd+CgzdpfU5x5eOaW2WMkpmOrFshRFYK7Mw==} engines: {node: '>=16'} hasBin: true dependencies: @@ -3031,6 +3054,10 @@ packages: queue-microtask: 1.2.3 dev: true + /rvfc-polyfill@1.0.7: + resolution: {integrity: sha512-seBl7J1J3/k0LuzW2T9fG6JIOpni5AbU+/87LA+zTYKgTVhsfShmS8K/yOo1eeEjGJHnAdkVAUUM+PEjN9Mpkw==} + dev: false + /sade@1.8.1: resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} engines: {node: '>=6'} @@ -3813,3 +3840,9 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} dev: true + + /zxing-wasm@1.1.3: + resolution: {integrity: sha512-MYm9k/5YVs4ZOTIFwlRjfFKD0crhefgbnt1+6TEpmKUDFp3E2uwqGSKwQOd2hOIsta/7Usq4hnpNRYTLoljnfA==} + dependencies: + '@types/emscripten': 1.39.10 + dev: false diff --git a/src/lib/barcode-scanner.svelte b/src/lib/barcode-scanner.svelte new file mode 100644 index 0000000..1944abd --- /dev/null +++ b/src/lib/barcode-scanner.svelte @@ -0,0 +1,112 @@ + + +
+ +
+ + diff --git a/src/lib/barcode-scanner.ts b/src/lib/barcode-scanner.ts new file mode 100644 index 0000000..7c493de --- /dev/null +++ b/src/lib/barcode-scanner.ts @@ -0,0 +1,165 @@ +import { BarcodeDetector } from 'barcode-detector/pure'; + +import type { ROI, Track } from './types.js'; + +export async function startCamera( + video: HTMLVideoElement, + stream: MediaStream, + constraints: MediaTrackConstraints +) { + if (window.isSecureContext !== true) { + throw new Error('Camera access requires a secure context'); + } + + if (navigator?.mediaDevices?.getUserMedia === undefined) { + throw new Error('Stream API is not supported'); + } + + stream = await navigator.mediaDevices.getUserMedia({ + video: constraints, + audio: false + }); + + video.srcObject = stream; + video.play(); + + await new Promise((resolve) => { + video.addEventListener('loadeddata', resolve, { once: true }); + }); + + // according to: https://oberhofer.co/mediastreamtrack-and-its-capabilities/#queryingcapabilities + // on some devices, getCapabilities only returns a non-empty object after + // some delay. there is no appropriate event so we have to add a constant timeout + await new Promise((resolve) => { + setTimeout(resolve, 500); + }); + + const [track] = stream.getVideoTracks(); + const capabilities = track.getCapabilities(); + + return { capabilities }; +} + +export function stopCamera(stream: MediaStream) { + for (const track of stream.getTracks()) { + stream.removeTrack(track); + track.stop(); + } +} + +function objectFitVideoTransform(video: HTMLVideoElement) { + const matrix = new DOMMatrix(); + + // center + const videoOffsetCenter = new DOMPoint(video.offsetWidth / 2, video.offsetHeight / 2); + const videoCenter = new DOMPoint(video.videoWidth / 2, video.videoHeight / 2); + matrix.translateSelf(videoOffsetCenter.x - videoCenter.x, videoOffsetCenter.y - videoCenter.y); + + // scale (object-fit: cover) + const scale = Math.max( + video.offsetWidth / video.videoWidth, + video.offsetHeight / video.videoHeight + ); + matrix.scaleSelf(scale, scale, 1, videoCenter.x, videoCenter.y); + + const topLeft = matrix.inverse().transformPoint(new DOMPoint(0, 0)); + const bottomRight = matrix + .inverse() + .transformPoint(new DOMPoint(video.offsetWidth, video.offsetHeight)); + + return { + x: topLeft.x, + y: topLeft.y, + width: bottomRight.x - topLeft.x, + height: bottomRight.y - topLeft.y + }; +} + +export function setupCanvas( + video: HTMLVideoElement, + camera: HTMLCanvasElement, + overlay: HTMLCanvasElement +) { + const ctxCamera = camera.getContext('2d', { + alpha: false, + willReadFrequently: true + }) as CanvasRenderingContext2D; + const ctxOverlay = overlay.getContext('2d', { + alpha: true, + willReadFrequently: true + }) as CanvasRenderingContext2D; + + const { width, height } = objectFitVideoTransform(video); + camera.width = width; + camera.height = height; + overlay.width = width; + overlay.height = height; + + return { ctxCamera, ctxOverlay }; +} + +export function drawVideo( + ctxCamera: CanvasRenderingContext2D, + camera: HTMLCanvasElement, + video: HTMLVideoElement +) { + const { x, y, width, height } = objectFitVideoTransform(video); + ctxCamera.clearRect(0, 0, camera.width, camera.height); + ctxCamera.drawImage(video, x, y, width, height, 0, 0, camera.width, camera.height); +} + +export async function detect( + detector: BarcodeDetector, + ctxCamera: CanvasRenderingContext2D, + camera: HTMLCanvasElement, + ctxOverlay: CanvasRenderingContext2D, + overlay: HTMLCanvasElement, + rois: ROI[], + showROIS: boolean, + track: Track | undefined +) { + ctxOverlay.clearRect(0, 0, overlay.width, overlay.height); + + const allDetections = []; + + for (const roi of rois) { + const imageData = ctxCamera.getImageData( + roi.x * camera.width, + roi.y * camera.height, + roi.width * camera.width, + roi.height * camera.height + ); + + const detections = await detector.detect(imageData); + const adjustedDetections = detections.map((detection) => { + return { + ...detection, + boundingBox: DOMRectReadOnly.fromRect({ + x: detection.boundingBox.x + roi.x * camera.width, + y: detection.boundingBox.y + roi.y * camera.height, + width: detection.boundingBox.width, + height: detection.boundingBox.height + }) + }; + }); + + allDetections.push(...adjustedDetections); + + if (showROIS) { + ctxOverlay.strokeStyle = 'rgba(0, 0, 0, 0.3)'; + ctxOverlay.lineWidth = 3; + ctxOverlay.strokeRect( + roi.x * camera.width, + roi.y * camera.height, + roi.width * camera.width, + roi.height * camera.height + ); + } + + if (track && adjustedDetections.length > 0) { + track(adjustedDetections, ctxOverlay); + } + } + + return allDetections; +} diff --git a/src/lib/index.ts b/src/lib/index.ts index 47d3c46..3c68ac3 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -1 +1 @@ -// Reexport your entry components here +export { default as BarcodeScanner } from './barcode-scanner.svelte'; diff --git a/src/lib/types.ts b/src/lib/types.ts new file mode 100644 index 0000000..93a358a --- /dev/null +++ b/src/lib/types.ts @@ -0,0 +1,15 @@ +import type { DetectedBarcode } from 'barcode-detector/pure'; + +export type ROI = { + x: number; + y: number; + width: number; + height: number; +}; + +export type Events = { + detect: { detections: DetectedBarcode[] }; + init: { capabilities: MediaTrackCapabilities }; +}; + +export type Track = (detections: DetectedBarcode[], ctxOverlay: CanvasRenderingContext2D) => void; diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte new file mode 100644 index 0000000..a16f9e3 --- /dev/null +++ b/src/routes/+layout.svelte @@ -0,0 +1,12 @@ +
+ +
+ + diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 0a45b69..48b125a 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,3 +1,100 @@ -

Welcome to your library project

-

Create your package using @sveltejs/package and preview/showcase your work with SvelteKit

-

Visit kit.svelte.dev to read the documentation

+ + +
+ { + for (const detection of detections) { + ctxOverlay.strokeStyle = 'rgba(255, 0, 0, 0.5)'; + ctxOverlay.lineWidth = 4; + ctxOverlay.strokeRect( + detection.boundingBox.x, + detection.boundingBox.y, + detection.boundingBox.width, + detection.boundingBox.height + ); + } + }} + > + {#if loading} +
Loading...
+ {/if} +
+
+ +