diff --git a/deploy/prod/.gitignore b/deploy/prod/.gitignore index 7e17447..cb88020 100644 --- a/deploy/prod/.gitignore +++ b/deploy/prod/.gitignore @@ -1,3 +1,2 @@ .secrets/ inventory.yml -.frontend-build/ diff --git a/deploy/prod/playbook.yml b/deploy/prod/playbook.yml index d46c406..7096962 100644 --- a/deploy/prod/playbook.yml +++ b/deploy/prod/playbook.yml @@ -41,13 +41,6 @@ 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. @@ -255,14 +248,11 @@ {% endif %} tasks: - - name: Install docker.io, nginx and rsync + - name: Install docker.io and nginx 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 @@ -329,18 +319,12 @@ # 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: "{{ ansible_user }}" + owner: www-data + group: www-data mode: "0755" - name: Create backend static output directory @@ -446,56 +430,17 @@ enabled: true state: started - # 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) + - name: Build frontend builder docker image ansible.builtin.command: cmd: >- docker build -t {{ toolshed_frontend_image }}:{{ toolshed_image_tag }} - -f {{ toolshed_frontend_build_src_dir }}/deploy/prod/Dockerfile.frontend {{ toolshed_frontend_build_src_dir }}/frontend + -f {{ toolshed_src_dir }}/deploy/prod/Dockerfile.frontend {{ toolshed_src_dir }}/frontend changed_when: true - delegate_to: localhost - become: false - - 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) + - name: Run frontend builder once to export the static build ansible.builtin.command: - cmd: docker run --rm -v {{ toolshed_frontend_build_dist_dir }}:/output {{ toolshed_frontend_image }}:{{ toolshed_image_tag }} + cmd: docker run --rm -v {{ toolshed_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/package-lock.json b/frontend/package-lock.json index 704e559..7de8ccb 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -569,11 +569,6 @@ "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", @@ -614,68 +609,11 @@ "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" } @@ -683,7 +621,8 @@ "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==" + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true }, "combined-stream": { "version": "1.0.8", @@ -786,11 +725,6 @@ "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", @@ -812,11 +746,6 @@ "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", @@ -926,15 +855,6 @@ "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", @@ -963,11 +883,6 @@ "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", @@ -1077,7 +992,8 @@ "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==" + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true }, "is-glob": { "version": "4.0.3", @@ -1188,14 +1104,6 @@ "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", @@ -1322,29 +1230,6 @@ "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", @@ -1360,11 +1245,6 @@ "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", @@ -1415,11 +1295,6 @@ "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", @@ -1478,16 +1353,6 @@ "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", @@ -1509,16 +1374,6 @@ "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", @@ -1572,11 +1427,6 @@ "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", @@ -1973,11 +1823,6 @@ "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", @@ -2071,68 +1916,6 @@ "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 d4918b3..bff1287 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -13,7 +13,6 @@ "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/components/Sidebar.vue b/frontend/src/components/Sidebar.vue index de5fc9a..c55f002 100644 --- a/frontend/src/components/Sidebar.vue +++ b/frontend/src/components/Sidebar.vue @@ -51,12 +51,6 @@ Swatch - diff --git a/frontend/src/main.js b/frontend/src/main.js index 80ea4ec..1d95c89 100644 --- a/frontend/src/main.js +++ b/frontend/src/main.js @@ -10,8 +10,6 @@ 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 30a99b2..ab9f4c6 100644 --- a/frontend/src/router.js +++ b/frontend/src/router.js @@ -20,7 +20,6 @@ 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'; @@ -68,7 +67,7 @@ const routes = [{path: '/', component: Dashboard, meta: {requiresAuth: true}}, { }, {path: '/admin', component: Admin, meta: {requiresAuth: true} -}, {path: '/swatch', component: Swatch, meta: {requiresAuth: true}}, {path: '/print', component: Print, meta: {requiresAuth: true}}, { +}, {path: '/swatch', component: Swatch, 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 deleted file mode 100644 index 1b4724f..0000000 --- a/frontend/src/views/Print.vue +++ /dev/null @@ -1,380 +0,0 @@ - - - - - diff --git a/frontend/vendor/libweblabel.mjs b/frontend/vendor/libweblabel.mjs deleted file mode 100644 index a2b7eee..0000000 --- a/frontend/vendor/libweblabel.mjs +++ /dev/null @@ -1,2 +0,0 @@ -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 deleted file mode 100644 index 7ba37e1..0000000 Binary files a/frontend/vendor/libweblabel.wasm and /dev/null differ diff --git a/frontend/vendor/weblabel.mjs b/frontend/vendor/weblabel.mjs deleted file mode 100644 index 79c65f1..0000000 --- a/frontend/vendor/weblabel.mjs +++ /dev/null @@ -1,958 +0,0 @@ -/* - 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); - } - } -}