From 312f2f64607fa5e1eedbb6b3365ce8398def7b58 Mon Sep 17 00:00:00 2001 From: jedi Date: Mon, 17 Aug 2026 12:10:42 +0200 Subject: [PATCH 1/2] stash --- frontend/package-lock.json | 227 +++++++- frontend/package.json | 1 + frontend/src/main.js | 2 + frontend/src/router.js | 3 +- frontend/src/views/Print.vue | 380 ++++++++++++ frontend/vendor/libweblabel.mjs | 2 + frontend/vendor/libweblabel.wasm | Bin 0 -> 74937 bytes frontend/vendor/weblabel.mjs | 958 +++++++++++++++++++++++++++++++ 8 files changed, 1567 insertions(+), 6 deletions(-) create mode 100644 frontend/src/views/Print.vue create mode 100644 frontend/vendor/libweblabel.mjs create mode 100644 frontend/vendor/libweblabel.wasm create mode 100644 frontend/vendor/weblabel.mjs diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 7de8ccb..704e559 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -569,6 +569,11 @@ "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", "dev": true }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" + }, "chai": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz", @@ -609,11 +614,68 @@ "readdirp": "~3.6.0" } }, + "cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + } + } + }, "color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "requires": { "color-name": "~1.1.4" } @@ -621,8 +683,7 @@ "color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "combined-stream": { "version": "1.0.8", @@ -725,6 +786,11 @@ "ms": "^2.1.3" } }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==" + }, "decimal.js": { "version": "10.4.3", "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", @@ -746,6 +812,11 @@ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", "dev": true }, + "dijkstrajs": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==" + }, "dns-query": { "version": "0.11.2", "resolved": "https://registry.npmjs.org/dns-query/-/dns-query-0.11.2.tgz", @@ -855,6 +926,15 @@ "to-regex-range": "^5.0.1" } }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, "foreground-child": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", @@ -883,6 +963,11 @@ "dev": true, "optional": true }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" + }, "get-func-name": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", @@ -992,8 +1077,7 @@ "is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" }, "is-glob": { "version": "4.0.3", @@ -1104,6 +1188,14 @@ "integrity": "sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==", "dev": true }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "requires": { + "p-locate": "^4.1.0" + } + }, "lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", @@ -1230,6 +1322,29 @@ "yocto-queue": "^1.0.0" } }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "requires": { + "p-limit": "^2.2.0" + }, + "dependencies": { + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "requires": { + "p-try": "^2.0.0" + } + } + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" + }, "package-json-from-dist": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", @@ -1245,6 +1360,11 @@ "entities": "^4.4.0" } }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" + }, "path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -1295,6 +1415,11 @@ "pathe": "^1.1.2" } }, + "pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==" + }, "popper.js": { "version": "1.16.1", "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz", @@ -1353,6 +1478,16 @@ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true }, + "qrcode": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", + "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", + "requires": { + "dijkstrajs": "^1.0.1", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + } + }, "querystringify": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", @@ -1374,6 +1509,16 @@ "picomatch": "^2.2.1" } }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==" + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" + }, "requires-port": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", @@ -1427,6 +1572,11 @@ "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "dev": true }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" + }, "shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -1823,6 +1973,11 @@ "isexe": "^2.0.0" } }, + "which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==" + }, "why-is-node-running": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", @@ -1916,6 +2071,68 @@ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", "dev": true }, + "y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==" + }, + "yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "requires": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "requires": { + "ansi-regex": "^5.0.1" + } + } + } + }, + "yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + }, "yocto-queue": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.1.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index bff1287..d4918b3 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -13,6 +13,7 @@ "dns-query": "^0.11.2", "js-nacl": "^1.4.0", "moment": "^2.29.4", + "qrcode": "^1.5.4", "vue": "^3.2.47", "vue-multiselect": "^2.1.7", "vue-router": "^4.1.6", diff --git a/frontend/src/main.js b/frontend/src/main.js index 1d95c89..80ea4ec 100644 --- a/frontend/src/main.js +++ b/frontend/src/main.js @@ -10,6 +10,8 @@ import store from './store'; import _nacl from 'js-nacl'; + + const app = createApp(App).use(store).use(BootstrapIconsPlugin); _nacl.instantiate((nacl) => { diff --git a/frontend/src/router.js b/frontend/src/router.js index ab9f4c6..30a99b2 100644 --- a/frontend/src/router.js +++ b/frontend/src/router.js @@ -20,6 +20,7 @@ import StorageLocationEdit from '@/views/StorageLocationEdit.vue'; import Admin from '@/views/Admin.vue'; import Swatch from '@/views/Swatch.vue'; import Files from '@/views/Files.vue'; +import Print from "@/views/Print.vue"; import Workflows from '@/views/Workflows.vue'; import WorkflowDetail from '@/views/WorkflowDetail.vue'; @@ -67,7 +68,7 @@ const routes = [{path: '/', component: Dashboard, meta: {requiresAuth: true}}, { }, {path: '/admin', component: Admin, meta: {requiresAuth: true} -}, {path: '/swatch', component: Swatch, meta: {requiresAuth: true}}, { +}, {path: '/swatch', component: Swatch, meta: {requiresAuth: true}}, {path: '/print', component: Print, meta: {requiresAuth: true}}, { path: '/search/:query', component: Search, meta: {requiresAuth: true}, diff --git a/frontend/src/views/Print.vue b/frontend/src/views/Print.vue new file mode 100644 index 0000000..1b4724f --- /dev/null +++ b/frontend/src/views/Print.vue @@ -0,0 +1,380 @@ + + + + + diff --git a/frontend/vendor/libweblabel.mjs b/frontend/vendor/libweblabel.mjs new file mode 100644 index 0000000..a2b7eee --- /dev/null +++ b/frontend/vendor/libweblabel.mjs @@ -0,0 +1,2 @@ +async function createPrinterBlob(moduleArg={}){var Module=moduleArg;var ENVIRONMENT_IS_WEB=!!globalThis.window;var ENVIRONMENT_IS_WORKER=!!globalThis.WorkerGlobalScope;var ENVIRONMENT_IS_NODE=globalThis.process?.versions?.node&&globalThis.process?.type!="renderer";if(ENVIRONMENT_IS_NODE){const{createRequire}=await import("node:module");var require=createRequire(import.meta.url)}var programArgs=[];var thisProgram="./this.program";var quit_=(status,toThrow)=>{throw toThrow};var _scriptName=import.meta.url;var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}return scriptDirectory+path}var readAsync,readBinary;if(ENVIRONMENT_IS_NODE){var fs=require("node:fs");if(_scriptName.startsWith("file:")){scriptDirectory=require("node:path").dirname(require("node:url").fileURLToPath(_scriptName))+"/"}readBinary=filename=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename);return ret};readAsync=async(filename,binary=true)=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename,binary?undefined:"utf8");return ret};if(process.argv.length>1){thisProgram=process.argv[1].replace(/\\/g,"/")}programArgs=process.argv.slice(2);quit_=(status,toThrow)=>{process.exitCode=status;throw toThrow}}else if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){try{scriptDirectory=new URL(".",_scriptName).href}catch{}{if(ENVIRONMENT_IS_WORKER){readBinary=url=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.responseType="arraybuffer";xhr.send(null);return new Uint8Array(xhr.response)}}readAsync=async url=>{var response=await fetch(url,{credentials:"same-origin"});if(response.ok){return response.arrayBuffer()}throw new Error(response.status+" : "+response.url)}}}else{}var out=console.log.bind(console);var err=console.error.bind(console);var wasmBinary;var ABORT=false;var EXITSTATUS;var isFileURI=filename=>filename.startsWith("file://");class EmscriptenEH{}class EmscriptenSjLj extends EmscriptenEH{}var runtimeInitialized=false;function getMemoryBuffer(){return wasmMemory.buffer}function updateMemoryViews(){if(HEAP8?.buffer?.resizable)return;var b=getMemoryBuffer();HEAP8=new Int8Array(b);Module["HEAPU8"]=HEAPU8=new Uint8Array(b);HEAP32=new Int32Array(b);HEAPU32=new Uint32Array(b)}function preRun(){var preRun=Module["preRun"];if(preRun){if(typeof preRun=="function")preRun=[preRun];onPreRuns.push(...preRun)}callRuntimeCallbacks(onPreRuns)}function initRuntime(){runtimeInitialized=true;wasmExports["__wasm_call_ctors"]()}function postRun(){var postRun=Module["postRun"];if(postRun){if(typeof postRun=="function")postRun=[postRun];onPostRuns.push(...postRun)}callRuntimeCallbacks(onPostRuns)}function abort(what){Module["onAbort"]?.(what);what=`Aborted(${what})`;err(what);ABORT=true;what+=". Build with -sASSERTIONS for more info.";var e=new WebAssembly.RuntimeError(what);throw e}var wasmBinaryFile;function findWasmBinary(){if(Module["locateFile"]){return locateFile("libweblabel.wasm")}return new URL("libweblabel.wasm",import.meta.url).href}function getBinarySync(file){if(readBinary){return readBinary(file)}throw"both async and sync fetching of the wasm failed"}async function getWasmBinary(binaryFile){if(!wasmBinary){try{var response=await readAsync(binaryFile);return new Uint8Array(response)}catch{}}return getBinarySync(binaryFile)}async function instantiateArrayBuffer(binaryFile,imports){try{var binary=await getWasmBinary(binaryFile);var instance=await WebAssembly.instantiate(binary,imports);return instance}catch(reason){err(`failed to asynchronously prepare wasm: ${reason}`);abort(reason)}}async function instantiateAsync(binary,binaryFile,imports){if(!binary&&!ENVIRONMENT_IS_NODE){try{var response=fetch(binaryFile,{credentials:"same-origin"});var instantiationResult=await WebAssembly.instantiateStreaming(response,imports);return instantiationResult}catch(reason){err(`wasm streaming compile failed: ${reason}`);err("falling back to ArrayBuffer instantiation")}}return instantiateArrayBuffer(binaryFile,imports)}function getWasmImports(){var imports={env:wasmImports,wasi_snapshot_preview1:wasmImports};return imports}async function createWasm(){function receiveInstance(instance){wasmExports=instance.exports;wasmExports=Asyncify.instrumentWasmExports(wasmExports);assignWasmExports(wasmExports);updateMemoryViews();return wasmExports}function receiveInstantiationResult(result){return receiveInstance(result["instance"])}var info=getWasmImports();var instantiateWasm=Module["instantiateWasm"];if(instantiateWasm){return new Promise(resolve=>{instantiateWasm(info,inst=>resolve(receiveInstance(inst)))})}wasmBinaryFile??=findWasmBinary();var result=await instantiateAsync(wasmBinary,wasmBinaryFile,info);var exports=receiveInstantiationResult(result);return exports}class ExitStatus{name="ExitStatus";constructor(status){this.message=`Program terminated with exit(${status})`;this.status=status}}var HEAP8;var callRuntimeCallbacks=callbacks=>{while(callbacks.length>0){callbacks.shift()(Module)}};var onPostRuns=[];var onPreRuns=[];var dynCalls={};var dynCallLegacy=(sig,ptr,args)=>{sig=sig.replace(/p/g,"i");var f=dynCalls[sig];return f(ptr,...args)};var noExitRuntime=true;var stackRestore=val=>__emscripten_stack_restore(val);var stackSave=()=>_emscripten_stack_get_current();var getHeapMax=()=>2147483648;var alignMemory=(size,alignment)=>Math.ceil(size/alignment)*alignment;var growMemory=size=>{var oldHeapSize=wasmMemory.buffer.byteLength;var pages=(size-oldHeapSize+65535)/65536|0;try{wasmMemory.grow(pages);updateMemoryViews();return 1}catch(e){}};var HEAPU8;var _emscripten_resize_heap=requestedSize=>{var oldSize=HEAPU8.length;requestedSize>>>=0;var maxHeapSize=getHeapMax();if(requestedSize>maxHeapSize){return false}for(var cutDown=1;cutDown<=4;cutDown*=2){var overGrownHeapSize=oldSize*(1+.2/cutDown);overGrownHeapSize=Math.min(overGrownHeapSize,requestedSize+100663296);var newSize=Math.min(maxHeapSize,alignMemory(Math.max(requestedSize,overGrownHeapSize),65536));var replacement=growMemory(newSize);if(replacement){return true}}return false};var _fd_close=fd=>52;var INT53_MAX=9007199254740992;var INT53_MIN=-9007199254740992;var bigintToI53Checked=num=>numINT53_MAX?NaN:Number(num);function _fd_seek(fd,offset,whence,newOffset){offset=bigintToI53Checked(offset);return 70}var printCharBuffers=[null,[],[]];var UTF8Decoder=globalThis.TextDecoder&&new TextDecoder;var findStringEnd=(heapOrArray,idx,maxBytesToRead,ignoreNul)=>{var maxIdx=idx+maxBytesToRead;if(ignoreNul)return maxIdx;while(heapOrArray[idx]&&!(idx>=maxIdx))++idx;return idx};var UTF8ArrayToString=(heapOrArray,idx=0,maxBytesToRead,ignoreNul)=>{var endPtr=findStringEnd(heapOrArray,idx,maxBytesToRead,ignoreNul);if(endPtr-idx>16&&heapOrArray.buffer&&UTF8Decoder){return UTF8Decoder.decode(heapOrArray.subarray(idx,endPtr))}var str="";while(idx>10,56320|ch&1023)}}return str};var printChar=(stream,curr)=>{var buffer=printCharBuffers[stream];if(!curr||curr===10){(stream===1?out:err)(UTF8ArrayToString(buffer));buffer.length=0}else{buffer.push(curr)}};var UTF8ToString=(ptr,maxBytesToRead,ignoreNul)=>ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead,ignoreNul):"";var HEAPU32;var _fd_write=(fd,iov,iovcnt,pnum)=>{var num=0;for(var i=0;i>2];var len=HEAPU32[iov+4>>2];iov+=8;for(var j=0;j>2]=num;return 0};var runAndAbortIfError=func=>{try{return func()}catch(e){abort(e)}};var handleException=e=>{if(e instanceof ExitStatus||e=="unwind"){return EXITSTATUS}quit_(1,e)};var runtimeKeepaliveCounter=0;var keepRuntimeAlive=()=>noExitRuntime||runtimeKeepaliveCounter>0;var _proc_exit=code=>{EXITSTATUS=code;if(!keepRuntimeAlive()){Module["onExit"]?.(code);ABORT=true}quit_(code,new ExitStatus(code))};var exitJS=(status,implicit)=>{EXITSTATUS=status;_proc_exit(status)};var _exit=exitJS;var maybeExit=()=>{if(!keepRuntimeAlive()){try{_exit(EXITSTATUS)}catch(e){handleException(e)}}};var callUserCallback=func=>{if(ABORT){return}try{return func()}catch(e){handleException(e)}finally{maybeExit()}};var runtimeKeepalivePush=()=>{runtimeKeepaliveCounter+=1};var runtimeKeepalivePop=()=>{runtimeKeepaliveCounter-=1};var HEAP32;var Asyncify={instrumentWasmImports(imports){var importPattern=/^(invoke_.*|__asyncjs__.*)$/;for(let[x,original]of Object.entries(imports)){if(typeof original=="function"){let isAsyncifyImport=original.isAsync||importPattern.test(x)}}},instrumentFunction(original){var wrapper=(...args)=>{Asyncify.exportCallStack.push(original);try{return original(...args)}finally{if(!ABORT){var top=Asyncify.exportCallStack.pop();Asyncify.maybeStopUnwind()}}};Asyncify.funcWrappers.set(original,wrapper);return wrapper},instrumentWasmExports(exports){var ret={};for(let[x,original]of Object.entries(exports)){if(typeof original=="function"){var wrapper=Asyncify.instrumentFunction(original);ret[x]=wrapper}else{ret[x]=original}}return ret},State:{Normal:0,Unwinding:1,Rewinding:2,Disabled:3},state:0,StackSize:16384,currData:null,handleSleepReturnValue:0,exportCallStack:[],callstackFuncToId:new Map,callStackIdToFunc:new Map,funcWrappers:new Map,callStackId:0,asyncPromiseHandlers:null,sleepCallbacks:[],getCallStackId(func){if(!Asyncify.callstackFuncToId.has(func)){var id=Asyncify.callStackId++;Asyncify.callstackFuncToId.set(func,id);Asyncify.callStackIdToFunc.set(id,func)}return Asyncify.callstackFuncToId.get(func)},maybeStopUnwind(){if(Asyncify.currData&&Asyncify.state===Asyncify.State.Unwinding&&!Asyncify.exportCallStack.length){Asyncify.state=Asyncify.State.Normal;runAndAbortIfError(_asyncify_stop_unwind);if(typeof Fibers!="undefined"){Fibers.trampoline()}}},whenDone(){return new Promise((resolve,reject)=>{Asyncify.asyncPromiseHandlers={resolve,reject}})},allocateData(){var ptr=_malloc(12+Asyncify.StackSize);Asyncify.setDataHeader(ptr,ptr+12,Asyncify.StackSize);Asyncify.setDataRewindFunc(ptr);return ptr},setDataHeader(ptr,stack,stackSize){HEAPU32[ptr>>2]=stack;HEAPU32[ptr+4>>2]=stack+stackSize},setDataRewindFunc(ptr){var bottomOfCallStack=Asyncify.exportCallStack[0];var rewindId=Asyncify.getCallStackId(bottomOfCallStack);HEAP32[ptr+8>>2]=rewindId},getDataRewindFunc(ptr){var id=HEAP32[ptr+8>>2];var func=Asyncify.callStackIdToFunc.get(id);return func},doRewind(ptr){var original=Asyncify.getDataRewindFunc(ptr);var func=Asyncify.funcWrappers.get(original);return callUserCallback(func)},handleSleep(startAsync){if(ABORT)return;if(Asyncify.state===Asyncify.State.Normal){var reachedCallback=false;var reachedAfterCallback=false;startAsync((handleSleepReturnValue=0)=>{if(ABORT)return;Asyncify.handleSleepReturnValue=handleSleepReturnValue;reachedCallback=true;if(!reachedAfterCallback){return}Asyncify.state=Asyncify.State.Rewinding;runAndAbortIfError(()=>_asyncify_start_rewind(Asyncify.currData));if(typeof MainLoop!="undefined"&&MainLoop.func){MainLoop.resume()}var asyncWasmReturnValue,isError=false;try{asyncWasmReturnValue=Asyncify.doRewind(Asyncify.currData)}catch(err){asyncWasmReturnValue=err;isError=true}var handled=false;if(!Asyncify.currData){var asyncPromiseHandlers=Asyncify.asyncPromiseHandlers;if(asyncPromiseHandlers){Asyncify.asyncPromiseHandlers=null;(isError?asyncPromiseHandlers.reject:asyncPromiseHandlers.resolve)(asyncWasmReturnValue);handled=true}}if(isError&&!handled){throw asyncWasmReturnValue}});reachedAfterCallback=true;if(!reachedCallback){Asyncify.state=Asyncify.State.Unwinding;Asyncify.currData=Asyncify.allocateData();if(typeof MainLoop!="undefined"&&MainLoop.func){MainLoop.pause()}runAndAbortIfError(()=>_asyncify_start_unwind(Asyncify.currData))}}else if(Asyncify.state===Asyncify.State.Rewinding){Asyncify.state=Asyncify.State.Normal;runAndAbortIfError(_asyncify_stop_rewind);_free(Asyncify.currData);Asyncify.currData=null;Asyncify.sleepCallbacks.forEach(callUserCallback)}else{abort(`invalid state: ${Asyncify.state}`)}return Asyncify.handleSleepReturnValue},handleAsync:startAsync=>Asyncify.handleSleep(async wakeUp=>{wakeUp(await startAsync())})};var getCFunc=ident=>{var func=Module["_"+ident];return func};var writeArrayToMemory=(array,buffer)=>{HEAP8.set(array,buffer)};var lengthBytesUTF8=str=>{var len=0;for(var i=0;i=55296&&c<=57343){len+=4;++i}else{len+=3}}return len};var stringToUTF8Array=(str,heap,outIdx,maxBytesToWrite)=>{if(!(maxBytesToWrite>0))return 0;var startIdx=outIdx;var endIdx=outIdx+maxBytesToWrite-1;for(var i=0;i=endIdx)break;heap[outIdx++]=u}else if(u<=2047){if(outIdx+1>=endIdx)break;heap[outIdx++]=192|u>>6;heap[outIdx++]=128|u&63}else if(u<=65535){if(outIdx+2>=endIdx)break;heap[outIdx++]=224|u>>12;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}else{if(outIdx+3>=endIdx)break;heap[outIdx++]=240|u>>18;heap[outIdx++]=128|u>>12&63;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63;i++}}heap[outIdx]=0;return outIdx-startIdx};var stringToUTF8=(str,outPtr,maxBytesToWrite)=>stringToUTF8Array(str,HEAPU8,outPtr,maxBytesToWrite);var stackAlloc=sz=>__emscripten_stack_alloc(sz);var stringToUTF8OnStack=str=>{var size=lengthBytesUTF8(str)+1;var ret=stackAlloc(size);stringToUTF8(str,ret,size);return ret};var ccall=(ident,returnType,argTypes,args,opts)=>{var toC={string:str=>{var ret=0;if(str!==null&&str!==undefined&&str!==0){ret=stringToUTF8OnStack(str)}return ret},array:arr=>{var ret=stackAlloc(arr.length);writeArrayToMemory(arr,ret);return ret}};function convertReturnValue(ret){if(returnType==="string"){return UTF8ToString(ret)}if(returnType==="boolean")return Boolean(ret);return ret}var func=getCFunc(ident);var cArgs=[];var stack=0;if(args){for(var i=0;i{var numericArgs=!argTypes||argTypes.every(type=>type==="number"||type==="boolean");var numericRet=returnType!=="string";if(numericRet&&numericArgs&&!opts){return getCFunc(ident)}return(...args)=>ccall(ident,returnType,argTypes,args,opts)};{if(Module["noExitRuntime"])noExitRuntime=Module["noExitRuntime"];if(Module["print"])out=Module["print"];if(Module["printErr"])err=Module["printErr"];if(Module["arguments"])programArgs=Module["arguments"];if(Module["thisProgram"])thisProgram=Module["thisProgram"];var preInit=Module["preInit"];if(preInit){if(typeof preInit=="function")Module["preInit"]=preInit=[preInit];while(preInit.length>0){preInit.shift()()}}}Module["ccall"]=ccall;Module["cwrap"]=cwrap;Module["UTF8ToString"]=UTF8ToString;function __asyncjs__pwusb_list(out,max){return Asyncify.handleAsync(async()=>{const devices=await Module.usbBackend.list(max);const view=new DataView(HEAPU8.buffer);let n=0;for(const d of devices){if(n>=max)break;const base=out+n*6;view.setUint16(base+0,d.vendorId,true);view.setUint16(base+2,d.productId,true);view.setUint8(base+4,d.busNumber||0);view.setUint8(base+5,d.deviceAddress||0);++n}return n})}function __asyncjs__pwusb_open(index){return Asyncify.handleAsync(async()=>await Module.usbBackend.open(index))}function __asyncjs__pwusb_close(handle){return Asyncify.handleAsync(async()=>{await Module.usbBackend.close(handle)})}function __asyncjs__pwusb_claim(handle,interface_number){return Asyncify.handleAsync(async()=>await Module.usbBackend.claim(handle,interface_number))}function __asyncjs__pwusb_release(handle,interface_number){return Asyncify.handleAsync(async()=>await Module.usbBackend.release(handle,interface_number))}function __asyncjs__pwusb_out(handle,endpoint,data,len){return Asyncify.handleAsync(async()=>{const bytes=HEAPU8.slice(data,data+len);return await Module.usbBackend.transferOut(handle,endpoint,bytes)})}function __asyncjs__pwusb_in(handle,endpoint,data,len){return Asyncify.handleAsync(async()=>{const result=await Module.usbBackend.transferIn(handle,endpoint,len);if(typeof result==="number"){return result}const n=Math.min(result.length,len);HEAPU8.set(result.subarray(0,n),data);return n})}function __asyncjs__pwusb_sleep_ms(ms){return Asyncify.handleAsync(async()=>{await new Promise(resolve=>setTimeout(resolve,ms))})}var _pw_manifest,_pw_ptouch_manifest,_pw_ql570_manifest,_pw_ptouch_devices,_pw_ql570_devices,_pw_ptouch_capabilities,_pw_ql570_capabilities,_pw_devices,_pw_capabilities,_pw_driver_for,_pw_ptouch_supports,_pw_ql570_supports,_pw_driver_count,_pw_supports,_pw_abi_version,_pw_last_error,_pw_open,_pw_close,_pw_status_json,_pw_print,_pw_ptouch_last_error,_pw_ptouch_status_json,_pw_ptouch_abi_version,_pw_ptouch_open,_pw_ptouch_close,_pw_ptouch_print,_pw_ql570_last_error,_pw_ql570_status_json,_pw_ql570_abi_version,_pw_ql570_open,_pw_ql570_close,_pw_ql570_print,_malloc,_free,__emscripten_stack_restore,__emscripten_stack_alloc,_emscripten_stack_get_current,dynCall_i,dynCall_ii,dynCall_iii,dynCall_iiii,dynCall_iiiiiii,dynCall_jiji,dynCall_iidiiiii,dynCall_vii,_asyncify_start_unwind,_asyncify_stop_unwind,_asyncify_start_rewind,_asyncify_stop_rewind,memory,__indirect_function_table,wasmMemory;function assignWasmExports(wasmExports){_pw_manifest=Module["_pw_manifest"]=wasmExports["pw_manifest"];_pw_ptouch_manifest=Module["_pw_ptouch_manifest"]=wasmExports["pw_ptouch_manifest"];_pw_ql570_manifest=Module["_pw_ql570_manifest"]=wasmExports["pw_ql570_manifest"];_pw_ptouch_devices=Module["_pw_ptouch_devices"]=wasmExports["pw_ptouch_devices"];_pw_ql570_devices=Module["_pw_ql570_devices"]=wasmExports["pw_ql570_devices"];_pw_ptouch_capabilities=Module["_pw_ptouch_capabilities"]=wasmExports["pw_ptouch_capabilities"];_pw_ql570_capabilities=Module["_pw_ql570_capabilities"]=wasmExports["pw_ql570_capabilities"];_pw_devices=Module["_pw_devices"]=wasmExports["pw_devices"];_pw_capabilities=Module["_pw_capabilities"]=wasmExports["pw_capabilities"];_pw_driver_for=Module["_pw_driver_for"]=wasmExports["pw_driver_for"];_pw_ptouch_supports=Module["_pw_ptouch_supports"]=wasmExports["pw_ptouch_supports"];_pw_ql570_supports=Module["_pw_ql570_supports"]=wasmExports["pw_ql570_supports"];_pw_driver_count=Module["_pw_driver_count"]=wasmExports["pw_driver_count"];_pw_supports=Module["_pw_supports"]=wasmExports["pw_supports"];_pw_abi_version=Module["_pw_abi_version"]=wasmExports["pw_abi_version"];_pw_last_error=Module["_pw_last_error"]=wasmExports["pw_last_error"];_pw_open=Module["_pw_open"]=wasmExports["pw_open"];_pw_close=Module["_pw_close"]=wasmExports["pw_close"];_pw_status_json=Module["_pw_status_json"]=wasmExports["pw_status_json"];_pw_print=Module["_pw_print"]=wasmExports["pw_print"];_pw_ptouch_last_error=Module["_pw_ptouch_last_error"]=wasmExports["pw_ptouch_last_error"];_pw_ptouch_status_json=Module["_pw_ptouch_status_json"]=wasmExports["pw_ptouch_status_json"];_pw_ptouch_abi_version=Module["_pw_ptouch_abi_version"]=wasmExports["pw_ptouch_abi_version"];_pw_ptouch_open=Module["_pw_ptouch_open"]=wasmExports["pw_ptouch_open"];_pw_ptouch_close=Module["_pw_ptouch_close"]=wasmExports["pw_ptouch_close"];_pw_ptouch_print=Module["_pw_ptouch_print"]=wasmExports["pw_ptouch_print"];_pw_ql570_last_error=Module["_pw_ql570_last_error"]=wasmExports["pw_ql570_last_error"];_pw_ql570_status_json=Module["_pw_ql570_status_json"]=wasmExports["pw_ql570_status_json"];_pw_ql570_abi_version=Module["_pw_ql570_abi_version"]=wasmExports["pw_ql570_abi_version"];_pw_ql570_open=Module["_pw_ql570_open"]=wasmExports["pw_ql570_open"];_pw_ql570_close=Module["_pw_ql570_close"]=wasmExports["pw_ql570_close"];_pw_ql570_print=Module["_pw_ql570_print"]=wasmExports["pw_ql570_print"];_malloc=Module["_malloc"]=wasmExports["malloc"];_free=Module["_free"]=wasmExports["free"];__emscripten_stack_restore=wasmExports["_emscripten_stack_restore"];__emscripten_stack_alloc=wasmExports["_emscripten_stack_alloc"];_emscripten_stack_get_current=wasmExports["emscripten_stack_get_current"];dynCall_i=dynCalls["i"]=wasmExports["dynCall_i"];dynCall_ii=dynCalls["ii"]=wasmExports["dynCall_ii"];dynCall_iii=dynCalls["iii"]=wasmExports["dynCall_iii"];dynCall_iiii=dynCalls["iiii"]=wasmExports["dynCall_iiii"];dynCall_iiiiiii=dynCalls["iiiiiii"]=wasmExports["dynCall_iiiiiii"];dynCall_jiji=dynCalls["jiji"]=wasmExports["dynCall_jiji"];dynCall_iidiiiii=dynCalls["iidiiiii"]=wasmExports["dynCall_iidiiiii"];dynCall_vii=dynCalls["vii"]=wasmExports["dynCall_vii"];_asyncify_start_unwind=wasmExports["asyncify_start_unwind"];_asyncify_stop_unwind=wasmExports["asyncify_stop_unwind"];_asyncify_start_rewind=wasmExports["asyncify_start_rewind"];_asyncify_stop_rewind=wasmExports["asyncify_stop_rewind"];memory=wasmMemory=wasmExports["memory"];__indirect_function_table=wasmExports["__indirect_function_table"]}var wasmImports={__asyncjs__pwusb_claim,__asyncjs__pwusb_close,__asyncjs__pwusb_in,__asyncjs__pwusb_list,__asyncjs__pwusb_open,__asyncjs__pwusb_out,__asyncjs__pwusb_release,__asyncjs__pwusb_sleep_ms,emscripten_resize_heap:_emscripten_resize_heap,fd_close:_fd_close,fd_seek:_fd_seek,fd_write:_fd_write};async function run(){preRun();var setStatus=Module["setStatus"];if(setStatus){setStatus("Running...");await new Promise(resolve=>setTimeout(resolve,1));setTimeout(setStatus,1,"")}if(ABORT)return;initRuntime();Module["onRuntimeInitialized"]?.();postRun()}var wasmExports;wasmExports=await createWasm();await run(); +;return Module}export default createPrinterBlob; diff --git a/frontend/vendor/libweblabel.wasm b/frontend/vendor/libweblabel.wasm new file mode 100644 index 0000000000000000000000000000000000000000..7ba37e170ffac8ff1de11c4c07788b626ad7c2d3 GIT binary patch literal 74937 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 literal 0 HcmV?d00001 diff --git a/frontend/vendor/weblabel.mjs b/frontend/vendor/weblabel.mjs new file mode 100644 index 0000000..79c65f1 --- /dev/null +++ b/frontend/vendor/weblabel.mjs @@ -0,0 +1,958 @@ +/* + libweblabel — generated bundle, do not edit. + + Built by tools/bundle.py from src/web/index.js and the modules it re-exports: + src/web/lib/blob.js + src/web/lib/bitmap.js + src/web/lib/text.js + src/web/lib/pattern.js + src/web/lib/ruler.js + + Edit those and rebuild. The wasm driver blobs are separate files loaded at + runtime; see dist/blobs/. +*/ + +/* ---- src/web/lib/blob.js ------------------------------------------------- */ + +/* + Loader for USB printer blobs (ABI v1). + + Deliberately knows nothing about P-touch printers, Brother, or libptouch. + It loads a wasm blob, reads the device table out of the blob's manifest and + calls the six pw_* entry points. A blob for a different vendor's printers, + built against the same shim and exporting the same ABI, is driven by this + file unchanged - and so is a future version of this driver that supports + more printers, because the device table travels in the manifest rather than + being duplicated here. + + Two classes, and the difference is smaller than it looks: + + PrinterBlob any blob, holding one driver or many. + MultiPrinterBlob the same, seen as the set of drivers inside it: which + one owns a given printer, what that one alone can do, + and which of its exports to call. A merged blob + (dist/blobs/libweblabel.mjs) is what makes those + questions interesting; PrinterBlob drives it either + way. + + The ABI is described in docs/ABI.md. +*/ + +/* ------------------------------------------------------------------------ + WebUSB backend + + The JS half of the shim's contract. Also vendor-neutral: it moves bytes and + has no idea what they mean. + ------------------------------------------------------------------------ */ + +export class WebUsbBackend { + constructor({ transferTimeoutMs = 5000 } = {}) { + this.transferTimeoutMs = transferTimeoutMs; + this.devices = []; /* the devices the blob is allowed to see */ + this.handles = new Map(); + this.nextHandle = 1; + } + + /* Scope the blob to specific devices. Drivers typically open the first + match in the list, so passing exactly the device the user picked in the + chooser is what makes the selection stick. */ + setDevices(devices) { + this.devices = devices; + } + + async list() { + return this.devices.map((d, index) => ({ + vendorId: d.vendorId, + productId: d.productId, + busNumber: 0, /* WebUSB does not expose bus topology */ + deviceAddress: index, + })); + } + + async open(index) { + const device = this.devices[index]; + if (!device) { + return -1; + } + try { + if (!device.opened) { + await device.open(); + } + if (device.configuration === null) { + await device.selectConfiguration(1); + } + } catch (e) { + this.lastError = e; + return -1; + } + const handle = this.nextHandle++; + this.handles.set(handle, device); + return handle; + } + + async close(handle) { + const device = this.handles.get(handle); + this.handles.delete(handle); + if (device && device.opened) { + try { + await device.close(); + } catch { /* the device may already be gone */ } + } + } + + async claim(handle, interfaceNumber) { + const device = this.handles.get(handle); + if (!device) return -1; + try { + await device.claimInterface(interfaceNumber); + return 0; + } catch (e) { + /* A driver only learns that the claim failed, not why. WebUSB's + own message is the useful part, and on Linux the overwhelmingly + likely cause is a bound kernel driver - usblp binds anything of + printer class - which the browser cannot detach. */ + let detail = `claimInterface(${interfaceNumber}) failed: ${e.message}`; + if (typeof navigator !== "undefined" && /Linux/.test(navigator.userAgent || "")) { + detail += ". On Linux this usually means a kernel driver holds the " + + "interface (usblp claims printer-class devices) and the browser " + + "cannot detach it. Unbind it: " + + "echo -n | sudo tee /sys/bus/usb/drivers/usblp/unbind " + + "— see drivers/ptouch/README.md for the persistent udev rule"; + } + this.lastError = new Error(detail); + return -1; + } + } + + /* Hand the most recent underlying failure to the caller, once. */ + takeLastError() { + const e = this.lastError; + this.lastError = null; + return e ? e.message : null; + } + + async release(handle, interfaceNumber) { + const device = this.handles.get(handle); + if (!device) return -1; + try { + await device.releaseInterface(interfaceNumber); + return 0; + } catch (e) { + this.lastError = e; + return -1; + } + } + + async transferOut(handle, endpoint, bytes) { + const device = this.handles.get(handle); + if (!device) return -1; + try { + const result = await device.transferOut(endpoint, bytes); + if (result.status !== "ok") { + this.lastError = new Error(`transferOut: ${result.status}`); + return -1; + } + return result.bytesWritten; + } catch (e) { + this.lastError = e; + return -1; + } + } + + async transferIn(handle, endpoint, length) { + const device = this.handles.get(handle); + if (!device) return -1; + /* WebUSB has no per-transfer timeout and a pending transferIn cannot be + cancelled, so race it against a timer. -7 is LIBUSB_ERROR_TIMEOUT to + the shim, which is what the driver's own timeout handling expects. */ + const TIMEOUT = Symbol("timeout"); + let timer; + try { + const result = await Promise.race([ + device.transferIn(endpoint, length), + new Promise((resolve) => { + timer = setTimeout(() => resolve(TIMEOUT), this.transferTimeoutMs); + }), + ]); + if (result === TIMEOUT) { + /* The abandoned transfer would otherwise deliver its data into + the next read. Clearing the endpoint discards it. */ + try { await device.clearHalt("in", endpoint); } catch { /* best effort */ } + this.lastError = new Error(`transferIn timed out after ${this.transferTimeoutMs} ms`); + return -7; + } + if (result.status !== "ok") { + this.lastError = new Error(`transferIn: ${result.status}`); + return -1; + } + return new Uint8Array(result.data.buffer, result.data.byteOffset, result.data.byteLength); + } catch (e) { + this.lastError = e; + return -1; + } finally { + clearTimeout(timer); + } + } +} + +/* ------------------------------------------------------------------------ + Blob + ------------------------------------------------------------------------ */ + +const MAX_LOG = 500; + +export class PrinterBlob { + constructor(module, backend) { + this._module = module; + this._backend = backend; + this._queue = Promise.resolve(); + this.log = []; + this.manifest = null; + this.abiVersion = 0; + this.openDevice = null; /* {vendorId, productId} while open */ + } + + /* + url location of the blob's .mjs glue, resolved against baseUrl + backend anything implementing the backend contract; defaults to WebUSB + baseUrl base for resolving url (defaults to the document / this module) + */ + static async load(url, { backend, baseUrl } = {}) { + const base = baseUrl + || (typeof document !== "undefined" ? document.baseURI : import.meta.url); + const href = new URL(url, base).href; + + const usbBackend = backend || new WebUsbBackend(); + /* `new this`, so a subclass reaching this through super.load() gets an + instance of itself. */ + const blob = new this(null, usbBackend); + + const factory = (await import(/* @vite-ignore */ href)).default; + blob._module = await factory({ + usbBackend, + print: (text) => blob._record("out", text), + printErr: (text) => blob._record("err", text), + }); + + blob.abiVersion = await blob._call("pw_abi_version", "number", [], []); + if (blob.abiVersion !== 1) { + throw new Error(`unsupported blob ABI version ${blob.abiVersion}, expected 1`); + } + const manifest = await blob._call("pw_manifest", "string", [], []); + blob.manifest = JSON.parse(manifest); + return blob; + } + + _record(stream, text) { + this.log.push({ stream, text }); + if (this.log.length > MAX_LOG) { + this.log.shift(); + } + } + + /* Asyncify unwinds a single wasm stack at a time, so every export call is + serialized. Without this, two overlapping calls corrupt each other. */ + _call(name, returnType, argTypes, args) { + const run = () => this._module.ccall(name, returnType, argTypes, args, { async: true }); + const result = this._queue.then(run, run); + this._queue = result.then(() => undefined, () => undefined); + return result; + } + + /* + Call an export by name, through the same queue everything else uses. + + The methods below cover the ABI; this is for the exports they do not, + which is how a caller uses the per-driver symbols that + MultiPrinterBlob.symbolsFor() names. Going around it and calling + Module.ccall() directly is what re-entering a suspended blob looks like. + */ + callExport(name, returnType = "number", argTypes = [], args = []) { + return this._call(name, returnType, argTypes, args); + } + + /* The driver's own explanation, plus whatever the backend knows about the + host-level cause. The driver only sees "the claim failed"; the backend + is the layer that saw the actual exception. */ + async _lastError() { + const fromDriver = await this._call("pw_last_error", "string", [], []); + const fromBackend = typeof this._backend.takeLastError === "function" + ? this._backend.takeLastError() : null; + return [fromDriver, fromBackend].filter(Boolean).join(" — ") || "unknown error"; + } + + /* + Device filters for navigator.usb.requestDevice(), straight from the + blob. Deduplicated by USB id: a merged blob's table is several drivers' + tables at once, and two of them may know the same printer. + */ + usbFilters() { + const seen = new Set(); + const filters = []; + for (const d of this.manifest.devices) { + const key = `${d.vendorId}:${d.productId}`; + if (!seen.has(key)) { + seen.add(key); + filters.push({ vendorId: d.vendorId, productId: d.productId }); + } + } + return filters; + } + + /** What the blob knows about these USB ids, or null if it does not know them. */ + findDevice(vendorId, productId) { + return this.manifest.devices.find( + (d) => d.vendorId === vendorId && d.productId === productId) || null; + } + + /** Restrict the blob's view of the bus to these WebUSB devices. */ + setDevices(devices) { + this._backend.setDevices(devices); + } + + async open(vendorId, productId, { timeoutSeconds = 1 } = {}) { + const rc = await this._call("pw_open", "number", + ["number", "number", "number"], [vendorId, productId, timeoutSeconds]); + if (rc !== 0) { + throw new Error(await this._lastError()); + } + this.openDevice = { vendorId, productId }; + } + + async status({ timeoutSeconds = 1 } = {}) { + const json = await this._call("pw_status_json", "string", + ["number"], [timeoutSeconds]); + const parsed = JSON.parse(json); + if (parsed === null) { + throw new Error(await this._lastError()); + } + return parsed; + } + + async close() { + await this._call("pw_close", "number", [], []); + this.openDevice = null; + } + + /** Does the blob advertise this capability? */ + can(capability) { + return (this.manifest.capabilities || []).includes(capability); + } + + /* + Print a bitmap: one byte per pixel, row-major, non-zero = a printed dot. + width runs along the tape, height across it. The caller is responsible + for keeping height within the print area the status reports. + */ + async printBitmap({ data, width, height }, + { chain = false, precut = false, copies = 1 } = {}) { + if (!this.can("print")) { + throw new Error("this blob does not support printing"); + } + if (data.length !== width * height) { + throw new Error(`bitmap is ${data.length} bytes, expected ${width * height}`); + } + /* Staged into the wasm heap so the blob can read it directly. Freed + even if the print throws, and after the call has fully finished - + the pointer is live for the whole suspended-stack duration. */ + const ptr = this._module._malloc(data.length); + if (!ptr) { + throw new Error(`could not allocate ${data.length} bytes in the blob`); + } + try { + this._module.HEAPU8.set(data, ptr); + const rc = await this._call("pw_print", "number", + ["number", "number", "number", "number", "number", "number"], + [ptr, width, height, chain ? 1 : 0, precut ? 1 : 0, copies]); + if (rc !== 0) { + throw new Error(await this._lastError()); + } + } finally { + this._module._free(ptr); + } + } +} + +/* ------------------------------------------------------------------------ + Blobs seen as a set of drivers + + dist/blobs/libweblabel.mjs is every driver this build produced, in one + module. It forwards the plain ABI to whichever driver owns the printer that + was opened, so PrinterBlob drives it without knowing that. + + What this class covers is the part only the driver set can answer: which + driver a given printer belongs to, what that driver alone can do, and which + of its exports to call for it. A blob holding one driver is a set of one, so + this works on those too and a page never has to choose between the two + classes. See "Merged blobs" in docs/ABI.md. + ------------------------------------------------------------------------ */ + +/* The ABI, as the JavaScript name for each export and the bare C name it is + built from. A merged blob exports every one of these twice: once plain, and + once per driver as pw__. */ +const ABI_EXPORTS = { + abiVersion: "abi_version", + manifest: "manifest", + devices: "devices", + capabilities: "capabilities", + supports: "supports", + open: "open", + close: "close", + statusJson: "status_json", + print: "print", + lastError: "last_error", +}; + +export class MultiPrinterBlob extends PrinterBlob { + /* + Does this module hold several drivers? + + The merged blob reports them under "drivers" and reaches each one + through prefixed exports; a single-driver blob has only the plain ones. + It is the one thing that changes the answers below. + */ + get merged() { + return Array.isArray(this.manifest.drivers); + } + + /* The drivers inside this blob, each one's own manifest verbatim. A + single-driver blob's manifest has that shape already, so it is a set of + one rather than a special case. */ + get drivers() { + return this.manifest.drivers || [this.manifest]; + } + + /** Their names, in the order the build linked them. */ + driverNames() { + return this.drivers.map((d) => d.driver.name); + } + + /** The manifest of the driver with this name, or null. */ + driver(name) { + return this.drivers.find((d) => d.driver.name === name) || null; + } + + /* + The driver that owns these USB ids, or null if none does. + + Answered from the merged device table, where every entry names the + driver it came from. pw_driver_for() inside the blob answers the same + question from the same tables; this is the cheap synchronous way to ask + it. + */ + driverFor(vendorId, productId) { + const device = this.findDevice(vendorId, productId); + return device ? this.driver(device.driver) : null; + } + + /* + Which exports to call for this printer. + + const { driver, exports } = blob.symbolsFor(0x04f9, 0x2074); + await blob.callExport(exports.open, "number", + ["number", "number", "number"], [0x04f9, 0x2074, 1]); + + Going through the plain ABI - blob.open(), blob.status(), and the rest + - does the same dispatch inside the blob and is what most callers want. + This is for a caller that has a reason to address one driver directly: + reading a second driver's manifest while a printer is open, say, or + driving two of them without letting either one's dispatch state decide + which is current. + + On a single-driver blob the prefix is empty and these are the plain ABI + names, which is the truth there: the one driver is the dispatch. + + Returns null when no driver in this blob knows the device. + */ + symbolsFor(vendorId, productId) { + const driver = this.driverFor(vendorId, productId); + if (!driver) { + return null; + } + const name = driver.driver.name; + const prefix = this.merged ? `pw_${name}_` : "pw_"; + const exports = {}; + for (const [key, bare] of Object.entries(ABI_EXPORTS)) { + exports[key] = prefix + bare; + } + return { driver: name, prefix, exports, capabilities: driver.capabilities }; + } + + /* + Does a capability apply? + + With no device, the answer is the merged one: some driver in this blob + can do it. With a device - or with one open - it is that device's + driver alone, which is the honest answer for a control that is about to + act on that printer. "chain" is the case that matters: the P-touch + driver honours it and the QL driver does not. + */ + can(capability, device = this.openDevice) { + if (!device) { + return super.can(capability); + } + const driver = this.driverFor(device.vendorId, device.productId); + return Boolean(driver && (driver.capabilities || []).includes(capability)); + } +} + +/* ---- src/web/lib/bitmap.js ----------------------------------------------- */ + +/* + Canvas ⇄ the one-byte-per-pixel bitmap the blob ABI takes. + + A printhead has one state per dot: a pin either fires or it does not. Canvas + drawing is anti-aliased, so everything drawn there is greyscale until it is + thresholded here. Nothing in this file knows what is being printed. +*/ + +/* Canvas RGBA to the bitmap: one byte per pixel, row-major, non-zero = a dot. */ +export function canvasToBitmap(canvas) { + const { width, height } = canvas; + const rgba = canvas.getContext("2d", { willReadFrequently: true }) + .getImageData(0, 0, width, height).data; + const data = new Uint8Array(width * height); + for (let i = 0, p = 0; p < data.length; i += 4, ++p) { + /* Rounded, because the coefficients do not sum to exactly 1 in binary + floating point: without this, a pixel at exactly mid grey flips on + rounding noise instead of landing consistently on one side. */ + const luminance = Math.round(0.299 * rgba[i] + 0.587 * rgba[i + 1] + 0.114 * rgba[i + 2]); + /* Transparent counts as blank; anything darker than mid grey prints. */ + data[p] = (rgba[i + 3] > 127 && luminance < 128) ? 1 : 0; + } + return { data, width, height }; +} + +/* + Paint a bitmap back onto its canvas, so the preview shows exactly the dots + that will be printed, jaggies and all. canvasToBitmap() is idempotent over + this: re-reading the canvas afterwards yields the same bytes. +*/ +export function bitmapToCanvas(canvas, bitmap) { + const ctx = canvas.getContext("2d", { willReadFrequently: true }); + const img = ctx.createImageData(bitmap.width, bitmap.height); + for (let p = 0, i = 0; p < bitmap.data.length; ++p, i += 4) { + const value = bitmap.data[p] ? 0 : 255; + img.data[i] = img.data[i + 1] = img.data[i + 2] = value; + img.data[i + 3] = 255; + } + ctx.putImageData(img, 0, 0); +} + +/** How many dots the printhead will fire for this bitmap. */ +export function countDots(bitmap) { + let dots = 0; + for (const b of bitmap.data) { + dots += b; + } + return dots; +} + +/* ---- src/web/lib/text.js ------------------------------------------------- */ + +/* + Text laid out the way `ptouch-print --text` does it. + + A port of render_text() and its helpers from ptouch-print.c, which cannot be + linked into a blob: that file is libgd and argp all the way down. The CLI + picks a font size that makes the tallest line fill its share of the tape, + builds an image exactly as wide as the widest line, and distributes the lines + down the print area. Everything below mirrors that, including the integer + arithmetic, so the same input yields the same layout. + + Verified against the real tool: for the cases in the test suite this picks + the same point size and produces the same label width as + `ptouch-print --force-tape-width N --text ... --write-png`. + + One deliberate difference: gd is called through gdImageStringFT_180dpi(), + which hardcodes 180 dpi, so the CLI renders text at half the intended + physical size on a 360 dpi model. Here the printer's real dpi is used. + + gd's brect maps onto canvas TextMetrics like this: + brect[1] descent below the baseline actualBoundingBoxDescent + brect[5] -ascent above the baseline -actualBoundingBoxAscent + brect[0] left edge of the ink -actualBoundingBoxLeft + brect[2] right edge of the ink actualBoundingBoxRight + so height = brect[1]-brect[5], needed_width = brect[2]-brect[0] and + offset_x = -brect[0] all carry over directly. +*/ + +export const MAX_LINES = 4; /* as in ptouch-print.c */ + +/* + The threshold is 50%, matching what the CLI does. ptouch-print.c renders with + a *negative* colour (`-black`), which tells libgd to disable anti-aliasing + and use FreeType's monochrome rasterizer - verified: the same string rendered + with `black` produces 9 distinct palette entries, with `-black` exactly 2. So + the CLI's text is natively 1-bit and its coverage rule is "is the pixel centre + inside the outline", which 50% approximates. + + The one thing that cannot be reproduced is FreeType's dropout control, which + deliberately keeps hairlines that would otherwise fall between pixel centres. + Lowering TEXT_COVERAGE thickens stems if that ever bites. + + What stops small text turning to mush is not the threshold but the size: the + text is scaled to the print area rather than to a fixed pixel size, so stems + are several dots wide. fillText()'s maxWidth argument is never used - it + condenses glyphs horizontally to fit, which destroys legibility far faster + than omitting the element does. +*/ +const TEXT_COVERAGE = 0.5; + +/* DejaVu Sans is the CLI's default font, so labels come out looking the same + either way. */ +const TEXT_FONT_STACK = '"DejaVu Sans", "Liberation Sans", Arial, Helvetica, sans-serif'; +const TEXT_WEIGHT = "normal"; + +/* Verbatim from find_fontsize(): measuring against a fixed set of ascenders and + descenders keeps every line the same height whatever it contains. */ +const COMMON_CHARS = "!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnoprqstuvwxyz{|}~"; + +let measureCtx = null; + +const textFont = (size) => `${TEXT_WEIGHT} ${size}px ${TEXT_FONT_STACK}`; + +/* Ink box rather than advance width, so text can be packed tightly. Already + rounded to integers, which is what FreeType reports, so the arithmetic below + stays integer as it is in C. */ +function ftMetrics(text, fontPx) { + if (!measureCtx) { + measureCtx = document.createElement("canvas").getContext("2d"); + } + measureCtx.font = textFont(fontPx); + const m = measureCtx.measureText(text); + const left = Math.ceil(m.actualBoundingBoxLeft ?? 0); + const right = Math.ceil(m.actualBoundingBoxRight ?? m.width); + const ascent = Math.ceil(m.actualBoundingBoxAscent ?? fontPx * 0.8); + const descent = Math.ceil(m.actualBoundingBoxDescent ?? fontPx * 0.2); + return { left, width: left + right, ascent, descent, height: ascent + descent }; +} + +const ptToPx = (pt, dpi) => pt * dpi / 72; + +/* find_fontsize(): the largest whole point size whose line height still fits. */ +function findFontSizePt(wantPx, text, dpi) { + const combined = text + COMMON_CHARS; + let save = 0; + for (let pt = 4; pt <= 500; ++pt) { + if (ftMetrics(combined, ptToPx(pt, dpi)).height <= wantPx) { + save = pt; + } else { + break; + } + } + return save === 0 ? -1 : save; +} + +/* get_baselineoffset(): how much further below the baseline this text reaches + than a letter that sits on it. 'z' is the CLI's reference glyph. */ +function baselineOffset(text, fontPx) { + return ftMetrics(text, fontPx).descent - ftMetrics("z", fontPx).descent; +} + +/* The CLI's --text splits on a literal \n as well as a real newline. */ +export function parseTextLines(input) { + return input.split(/\\n|\n/).slice(0, MAX_LINES); +} + +/* render_text(): the placement of every line, or a throw carrying the same + message the CLI prints. printWidth is the print area of the mounted tape. */ +export function layoutTextLabel(lines, printWidth, + { align = "l", fontSizePt = 0, fontMargin = 0, dpi }) { + let fsz = fontSizePt; + if (fsz <= 0) { + const wantPx = Math.floor((printWidth - 2 * fontMargin) / lines.length); + for (const line of lines) { + const candidate = findFontSizePt(wantPx, line, dpi); + if (candidate < 0) { + throw new Error("could not estimate needed font size"); + } + if (fsz === 0 || candidate < fsz) { + fsz = candidate; + } + } + } + + const fontPx = ptToPx(fsz, dpi); + const metrics = lines.map((line) => ftMetrics(line, fontPx)); + const width = Math.max(...metrics.map((m) => m.width)); + const maxHeight = Math.max(...metrics.map((m) => m.height)); + + if (maxHeight * lines.length > printWidth) { + throw new Error(`Font size ${fsz} too large for ${lines.length} lines`); + } + + const unusedPx = printWidth - maxHeight * lines.length; + const placements = lines.map((line, i) => { + const ofs = baselineOffset(line, fontPx); + let pos = i * Math.floor(printWidth / lines.length) + maxHeight - ofs; + pos += Math.floor(Math.floor(unusedPx / lines.length) / 2); + let alignOfs = 0; + if (align === "c") { + alignOfs = Math.floor((width - metrics[i].width) / 2); + } else if (align === "r") { + alignOfs = width - metrics[i].width; + } + return { text: line, x: metrics[i].left + alignOfs, baseline: pos }; + }); + + return { fontSizePt: fsz, fontPx, width, maxHeight, placements }; +} + +/* + Render the whole text block into one buffer and threshold it in a single + pass, the way the CLI fills one gd image. The result is already 1-bit, so + whoever composes the label can blit it 1:1 and keep it that way. +*/ +export function renderTextLabel(lines, printWidth, opts) { + const layout = layoutTextLabel(lines, printWidth, opts); + + const canvas = document.createElement("canvas"); + canvas.width = Math.max(1, layout.width); + canvas.height = printWidth; + const ctx = canvas.getContext("2d", { willReadFrequently: true }); + ctx.fillStyle = "#fff"; + ctx.fillRect(0, 0, canvas.width, canvas.height); + ctx.font = textFont(layout.fontPx); + ctx.textBaseline = "alphabetic"; + ctx.fillStyle = "#000"; + for (const p of layout.placements) { + ctx.fillText(p.text, p.x, p.baseline); + } + + /* Opaque white background, so coverage is luminance rather than alpha. */ + const img = ctx.getImageData(0, 0, canvas.width, canvas.height); + const cutoff = Math.round((1 - TEXT_COVERAGE) * 255); + for (let i = 0; i < img.data.length; i += 4) { + const luminance = Math.round(0.299 * img.data[i] + 0.587 * img.data[i + 1] + + 0.114 * img.data[i + 2]); + const value = luminance <= cutoff ? 0 : 255; + img.data[i] = img.data[i + 1] = img.data[i + 2] = value; + img.data[i + 3] = 255; + } + ctx.putImageData(img, 0, 0); + return { canvas, layout }; +} + +/* ---- src/web/lib/pattern.js ---------------------------------------------- */ + +/* + A test pattern for a thermal printhead. + + Everything is laid out left to right and simply omitted when the tape runs + out. Nothing is ever scaled down to fit - a squeezed element is a misleading + test. +*/ + +/* + ctx the label canvas, one pixel per printhead pin + w, h the area to fill; h is the print area of the mounted tape + textBlock an already-thresholded canvas to blit at the left, or null +*/ +export function drawTestPattern(ctx, w, h, textBlock) { + ctx.fillStyle = "#fff"; + ctx.fillRect(0, 0, w, h); + ctx.fillStyle = "#000"; + ctx.strokeStyle = "#000"; + ctx.lineWidth = 1; + /* Crisp 1px strokes need half-pixel coordinates. */ + const line = (x1, y1, x2, y2) => { + ctx.beginPath(); + ctx.moveTo(x1 + 0.5, y1 + 0.5); + ctx.lineTo(x2 + 0.5, y2 + 0.5); + ctx.stroke(); + }; + + /* A frame on the outermost pins: if the print comes out clipped or off + centre, this is the thing that shows it. */ + ctx.strokeRect(0.5, 0.5, w - 1, h - 1); + + const gap = 6; + const margin = 5; + let x = margin; + const room = (need) => x + need <= w - margin; + + /* The text block comes first, rendered by the same --text layout used in + text-only mode, so the pattern shows real label text next to the geometry + rather than a second, differently-produced caption. Already 1-bit, so a + 1:1 blit is all that is needed. */ + if (textBlock && room(textBlock.width)) { + ctx.drawImage(textBlock, x, 0); + x += textBlock.width + gap; + } + + /* Vertical bars 1..4 px wide - horizontal (along-tape) resolution. */ + if (room(4 + 3 * 3 + 4)) { + for (let bar = 1; bar <= 4; ++bar) { + ctx.fillRect(x, 4, bar, h - 8); + x += bar + 3; + } + x += gap - 3; + } + + /* Horizontal rules at top, middle and bottom - vertical alignment. */ + const rules = 22; + if (room(rules)) { + line(x, 4, x + rules, 4); + line(x, (h >> 1), x + rules, (h >> 1)); + line(x, h - 5, x + rules, h - 5); + x += rules + gap; + } + + /* Solid block, then a hollow one. */ + const box = Math.max(6, Math.min(24, h - 16)); + const boxTop = (h - box) >> 1; + if (room(box)) { + ctx.fillRect(x, boxTop, box, box); + x += box + gap; + } + if (room(box)) { + ctx.strokeRect(x + 0.5, boxTop + 0.5, box - 1, box - 1); + x += box + gap; + } + + /* Circle - diagonal edges and anti-aliasing, which thresholding must cope + with. */ + const r = Math.max(3, Math.min(box, h - 12) / 2); + if (room(2 * r)) { + ctx.beginPath(); + ctx.arc(x + r, h / 2, r, 0, Math.PI * 2); + ctx.stroke(); + x += 2 * r + gap; + } + + /* An X. */ + const d = Math.min(24, h - 12); + const top = (h - d) >> 1; + if (room(d)) { + line(x, top, x + d, top + d); + line(x, top + d, x + d, top); + x += d + gap; + } + + /* 2px checkerboard. */ + const check = Math.min(24, h - 12); + if (room(check)) { + for (let cy = 0; cy < check; cy += 2) { + for (let cx = 0; cx < check; cx += 2) { + if (((cx + cy) / 2) % 2 === 0) { + ctx.fillRect(x + cx, top + cy, 2, 2); + } + } + } + x += check + gap; + } +} + +/* ---- src/web/lib/ruler.js ------------------------------------------------ */ + +/* + Millimetre rulers for the label preview. + + They live on their own canvases, deliberately not on the label canvas: that + one is the print bitmap, so anything drawn there would come out on the tape. + + One bitmap pixel is one printhead pin, so millimetres follow from the + printer's own dpi: 1 mm = dpi/25.4 pixels, which is 7.09 px at 180 dpi and + 14.17 px at 360 dpi. +*/ + +export const RULER_H = 20; /* height of the along-tape ruler, CSS px */ +export const RULER_W = 26; /* width of the across-tape ruler, CSS px */ + +/* Tick spacing in mm, coarsened as the preview shrinks: minor ticks need room + to stay distinguishable, numbered ticks need room not to collide. */ +function tickSteps(screenPxPerMm) { + const candidates = [[1, 5], [2, 10], [5, 25], [10, 50], [20, 100], [50, 250]]; + for (const [minor, major] of candidates) { + if (minor * screenPxPerMm >= 4 && major * screenPxPerMm >= 26) { + return { minor, major }; + } + } + return { minor: 100, major: 500 }; +} + +/* + canvas the ruler's own canvas + lengthPx the ruled span in bitmap pixels + axis "x" along the tape (below the label), "y" across it (beside it) + zoom screen pixels per bitmap pixel + dpi the printer's resolution, which is what makes a millimetre a + millimetre +*/ +export function drawRuler(canvas, { lengthPx, axis, zoom, dpi }) { + const dpr = window.devicePixelRatio || 1; + const pxPerMm = dpi / 25.4; + const span = lengthPx * zoom; /* CSS px along the ruled axis */ + const horizontal = axis === "x"; + const cssW = horizontal ? span : RULER_W; + const cssH = horizontal ? RULER_H : span; + + /* Backed at device resolution so the tick labels stay sharp; the label + canvas next to it stays 1:1 because it is pixel data, not a drawing. */ + canvas.width = Math.round(cssW * dpr); + canvas.height = Math.round(cssH * dpr); + canvas.style.width = `${cssW}px`; + canvas.style.height = `${cssH}px`; + + const ctx = canvas.getContext("2d"); + ctx.setTransform(dpr, 0, 0, dpr, 0, 0); + ctx.clearRect(0, 0, cssW, cssH); + + const ink = getComputedStyle(document.body).color; + ctx.strokeStyle = ink; + ctx.fillStyle = ink; + ctx.globalAlpha = 0.65; + ctx.lineWidth = 1; + ctx.font = "9px system-ui, sans-serif"; + + /* The rule itself, along the edge shared with the preview. */ + ctx.beginPath(); + if (horizontal) { + ctx.moveTo(0, 0.5); + ctx.lineTo(span, 0.5); + } else { + ctx.moveTo(0.5, 0); + ctx.lineTo(0.5, span); + } + ctx.stroke(); + + const totalMm = lengthPx / pxPerMm; + const step = tickSteps(pxPerMm * zoom); + for (let mm = 0; mm <= totalMm; mm += step.minor) { + const at = Math.round(mm * pxPerMm * zoom) + 0.5; + const major = mm % step.major === 0; + const tick = major ? 8 : 4; + + ctx.globalAlpha = major ? 0.75 : 0.45; + ctx.beginPath(); + if (horizontal) { + ctx.moveTo(at, 0); + ctx.lineTo(at, tick); + } else { + ctx.moveTo(0, at); + ctx.lineTo(tick, at); + } + ctx.stroke(); + + if (!major) { + continue; + } + ctx.globalAlpha = 0.75; + const text = String(mm); + if (horizontal) { + /* Skip a number that would run past the end of the tape. */ + if (at + ctx.measureText(text).width + 2 > span) { + continue; + } + ctx.textBaseline = "top"; + ctx.fillText(text, at + 2, tick + 1); + } else { + if (at + 4 > span) { + continue; + } + ctx.textBaseline = "middle"; + ctx.fillText(text, tick + 2, at + 4); + } + } +} From 9803f23b8d996d06cfdf17ef18291be2a131f6b8 Mon Sep 17 00:00:00 2001 From: jedi Date: Mon, 17 Aug 2026 12:37:18 +0200 Subject: [PATCH 2/2] stash --- deploy/prod/.gitignore | 1 + deploy/prod/playbook.yml | 69 ++++++++++++++++++++++++++--- frontend/src/components/Sidebar.vue | 6 +++ 3 files changed, 69 insertions(+), 7 deletions(-) diff --git a/deploy/prod/.gitignore b/deploy/prod/.gitignore index cb88020..7e17447 100644 --- a/deploy/prod/.gitignore +++ b/deploy/prod/.gitignore @@ -1,2 +1,3 @@ .secrets/ inventory.yml +.frontend-build/ diff --git a/deploy/prod/playbook.yml b/deploy/prod/playbook.yml index 7096962..d46c406 100644 --- a/deploy/prod/playbook.yml +++ b/deploy/prod/playbook.yml @@ -41,6 +41,13 @@ toolshed_backend_port: 8000 toolshed_wiki_dist_dir: /var/www/toolshed-wiki toolshed_local_dir: /var/www/toolshed-local + # The frontend build runs on the controller (see "Build frontend builder + # docker image (controller)" below) rather than the target host, so its + # scratch checkout and build output live here instead of under + # toolshed_src_dir/toolshed_dist_dir. Keyed by inventory_hostname so + # concurrent deploys to different hosts never collide. + toolshed_frontend_build_src_dir: "{{ playbook_dir }}/.frontend-build/{{ inventory_hostname }}/src" + toolshed_frontend_build_dist_dir: "{{ playbook_dir }}/.frontend-build/{{ inventory_hostname }}/dist" # Django's collectstatic output (admin/drf-yasg assets etc.), exported # from the built backend image so nginx can serve it directly instead of # proxying to gunicorn for every asset request. @@ -248,11 +255,14 @@ {% endif %} tasks: - - name: Install docker.io and nginx + - name: Install docker.io, nginx and rsync ansible.builtin.apt: name: - docker.io - nginx + # rsync is what the frontend dist sync (further down) relies on - + # it's the ansible.posix.synchronize module's transport. + - rsync state: present update_cache: true @@ -319,12 +329,18 @@ # until reloaded. notify: reload nginx + # Owned by ansible_user rather than www-data up front: the frontend dist + # sync below pushes files over a plain rsync-over-ssh connection as + # ansible_user (synchronize shells out to the local rsync binary, which + # opens its own ssh session - it doesn't go through Ansible's become), + # so that account needs write access here first. "Fix ownership of + # exported frontend build" resets this to www-data (via become) right + # after the sync completes. - name: Create frontend static output directory ansible.builtin.file: path: "{{ toolshed_dist_dir }}" state: directory - owner: www-data - group: www-data + owner: "{{ ansible_user }}" mode: "0755" - name: Create backend static output directory @@ -430,17 +446,56 @@ enabled: true state: started - - name: Build frontend builder docker image + # The next few tasks build the frontend on the controller instead of the + # target host: `npm run build` pulls in bootstrap+jquery+vue+moment+ + # js-nacl+qrcode, and esbuild's rendering/minification pass for that + # bundle needs more memory than small/memory-constrained target hosts + # (e.g. LXC containers without usable swap) reliably have. Only the + # resulting static dist/ is shipped to the target - the docker image + # itself never runs there. This assumes docker is already usable on the + # controller (not managed by this playbook, since "Install docker.io, + # nginx and rsync" above targets the remote host only). + - name: Checkout toolshed source (controller, for frontend build) + ansible.builtin.git: + repo: "{{ toolshed_repo_url | mandatory('toolshed_repo_url must be set as a host_var for ' ~ inventory_hostname) }}" + dest: "{{ toolshed_frontend_build_src_dir }}" + version: "{{ toolshed_version | default('stable') }}" + force: true + recursive: false + delegate_to: localhost + become: false + + - name: Build frontend builder docker image (controller) ansible.builtin.command: cmd: >- docker build -t {{ toolshed_frontend_image }}:{{ toolshed_image_tag }} - -f {{ toolshed_src_dir }}/deploy/prod/Dockerfile.frontend {{ toolshed_src_dir }}/frontend + -f {{ toolshed_frontend_build_src_dir }}/deploy/prod/Dockerfile.frontend {{ toolshed_frontend_build_src_dir }}/frontend changed_when: true + delegate_to: localhost + become: false - - name: Run frontend builder once to export the static build + - name: Create local frontend dist scratch directory (controller) + ansible.builtin.file: + path: "{{ toolshed_frontend_build_dist_dir }}" + state: directory + mode: "0755" + delegate_to: localhost + become: false + + - name: Run frontend builder once to export the static build (controller) ansible.builtin.command: - cmd: docker run --rm -v {{ toolshed_dist_dir }}:/output {{ toolshed_frontend_image }}:{{ toolshed_image_tag }} + cmd: docker run --rm -v {{ toolshed_frontend_build_dist_dir }}:/output {{ toolshed_frontend_image }}:{{ toolshed_image_tag }} changed_when: true + delegate_to: localhost + become: false + + - name: Sync built frontend dist to the target host + ansible.posix.synchronize: + src: "{{ toolshed_frontend_build_dist_dir }}/" + dest: "{{ toolshed_dist_dir }}/" + delete: true + delegate_to: localhost + become: false - name: Fix ownership of exported frontend build ansible.builtin.file: diff --git a/frontend/src/components/Sidebar.vue b/frontend/src/components/Sidebar.vue index c55f002..de5fc9a 100644 --- a/frontend/src/components/Sidebar.vue +++ b/frontend/src/components/Sidebar.vue @@ -51,6 +51,12 @@ Swatch +