From d3e43bea9858b29a90b4dc89f9ffb2402bc0cd4b Mon Sep 17 00:00:00 2001 From: Albrecht Degering Date: Wed, 2 Sep 2026 11:52:44 +0200 Subject: [PATCH] Release Label Tools 0.2.0 --- .gitea/workflows/verify.yml | 39 ++ CHANGELOG.md | 5 + README.md | 20 +- SECURITY.md | 3 +- SOURCE.md | 2 +- THIRD_PARTY_NOTICES.md | 3 +- docs/ARCHITECTURE.md | 20 +- docs/PRIVACY-SECURITY.md | 10 +- package-lock.json | 40 +- package.json | 10 +- playwright.config.ts | 22 +- public/CHANGELOG.md | 5 + public/LICENSES/npm-runtime-licenses.txt | 6 +- public/README.md | 20 +- public/SECURITY.md | 3 +- public/SOURCE.md | 2 +- public/THIRD_PARTY_NOTICES.md | 3 +- public/docs/ARCHITECTURE.md | 20 +- public/docs/PRIVACY-SECURITY.md | 10 +- public/sw.js | 2 +- public/toolbox-app.json | 19 +- src/components/DesignerPanel.tsx | 556 +++++++++++++++++++++++ src/components/HelpDialog.tsx | 3 +- src/components/Workbench.tsx | 27 +- src/labels/export.ts | 2 +- src/labels/pdf.ts | 257 +++++++++-- src/labels/render.ts | 134 ++++++ src/labels/template.ts | 313 +++++++++++++ src/labels/types.ts | 68 ++- src/styles.css | 55 +++ src/toolbox/manifest.source.json | 40 +- src/version.ts | 2 +- tests/browser/app.spec.ts | 8 +- tests/browser/responsive.spec.ts | 18 + tests/labels/pdf.test.ts | 52 +++ tests/labels/render.test.ts | 14 + tests/labels/template.test.ts | 39 ++ 37 files changed, 1733 insertions(+), 119 deletions(-) create mode 100644 .gitea/workflows/verify.yml create mode 100644 src/components/DesignerPanel.tsx create mode 100644 src/labels/template.ts create mode 100644 tests/browser/responsive.spec.ts create mode 100644 tests/labels/pdf.test.ts create mode 100644 tests/labels/template.test.ts diff --git a/.gitea/workflows/verify.yml b/.gitea/workflows/verify.yml new file mode 100644 index 0000000..b84a7cb --- /dev/null +++ b/.gitea/workflows/verify.yml @@ -0,0 +1,39 @@ +name: Verify + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +concurrency: + group: verify-${{ gitea.repository }}-${{ gitea.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + verify: + runs-on: ubuntu-latest + timeout-minutes: 45 + env: + CI: "true" + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "22" + cache: npm + - name: Select declared npm version + run: npm install --global npm@11.17.0 + - name: Install dependencies + run: npm ci + - name: Audit runtime dependencies + run: npm audit --omit=dev --audit-level=moderate + - name: Check, test, and build + run: npm run check + - name: Install browser engines + run: npx playwright install --with-deps chromium firefox webkit + - name: Browser tests + run: npm run test:browser diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e9c898..0850604 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 0.2.0 - 2026-09-02 + +- Add a visual label designer, richer vector templates, deterministic + multi-page SVG/ZIP output and vector PDF export with calibrated geometry. + ## 0.1.0 - 2026-09-01 - Add bounded CSV/JSON data merge, flattening and field mapping. diff --git a/README.md b/README.md index 5904f67..d66dd43 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,17 @@ # Label Tools +Label Tools combines bounded CSV/JSON data merge with calibrated label stocks +and a reusable visual template designer. Text, image, barcode and shape +elements can be positioned by drag, keyboard or exact mm/in/pt fields; template +JSON round-trips locally. Bleed, sheet bounds, barcode quiet zones and likely +scan-size problems are checked before print. Physical-size SVG and vector PDF +exports retain paths and text, while local raster images remain embedded image +objects. + Label Tools is a production-oriented, local-first browser studio for merging CSV/JSON records into printable label, badge and asset-tag sheets. -Version 0.1.0 includes: +Version 0.2.0 includes: - bounded CSV and JSON parsing with nested JSON field flattening; - explicit field mapping plus local serial and cryptographically random values; @@ -16,7 +24,7 @@ Version 0.1.0 includes: - start-slot selection, X/Y offset and independent scale calibration; - optional cut guides and registration marks; - paginated print preview and explicit print calibration guidance; -- vector SVG, deterministic multi-page SVG/report ZIP, and 144 DPI PDF export. +- vector SVG, deterministic multi-page SVG/report ZIP, and vector PDF export. Data and assets remain in browser memory. There is no telemetry, remote API, font download, CDN asset or automatic request. @@ -42,9 +50,11 @@ the actual sheet, print the registration/cut marks on plain paper, choose stock. Printer feed and unprintable margins are outside browser control. SVG uses one of four system font stacks and does not embed fonts. Results can -therefore vary by operating system. PDF export intentionally rasterizes each -generated SVG page at 144 DPI, preserving the preview appearance on the same -device but not creating an archival or press-quality font-embedded PDF. +therefore vary by operating system. PDF export maps application-owned SVG +geometry into pdf-lib vector paths, rectangles and embedded Helvetica text. +Characters outside the built-in WinAnsi repertoire are replaced visibly, and +unsupported WebP image objects remain available in SVG but are omitted from +PDF. The result is print-accurate in physical dimensions, not an archival PDF. ## Limits diff --git a/SECURITY.md b/SECURITY.md index 0977b69..c8df721 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -7,7 +7,8 @@ Label Tools keeps data in browser memory and accepts only bounded CSV/JSON and PNG/JPEG/WebP assets. Merge text and SVG attributes are escaped. Generated barcode markup comes from a pinned local encoder and is checked for active or linked elements. Remote image URLs, user-provided SVG and arbitrary templates -are not accepted in v0.1. +containing markup or code are not accepted in v0.2. Imported template JSON is +restricted to the bounded declarative v1 element schema. Generated labels can contain sensitive or guessable identifiers. Review the preview and warnings before export, and do not treat serial/random values as an diff --git a/SOURCE.md b/SOURCE.md index 25f0708..68bb0a0 100644 --- a/SOURCE.md +++ b/SOURCE.md @@ -1,7 +1,7 @@ # Source identity - Project: Label Tools -- Version: 0.1.0 +- Version: 0.2.0 - Canonical repository: https://git.add-ideas.de/lotobo/label-tools - Licence: GPL-3.0-or-later - Toolbox app id: `de.add-ideas.label-tools` diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index 07cbfd5..2ead298 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -5,7 +5,8 @@ never loaded from a CDN. Their licence texts are copied into the production artifact at build time. - React and React DOM — MIT -- add·ideas Toolbox Contract, Shell React and Helpers — GPL-3.0-or-later +- add·ideas Toolbox Contract and Shell React 0.3.0 — Apache-2.0 +- add·ideas Toolbox Helpers 0.2.0 — GPL-3.0-or-later - bwip-js — MIT - fflate — MIT - pdf-lib and its runtime dependencies — MIT diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 7339606..b25fc4a 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -1,15 +1,29 @@ # Architecture +The sheet engine keeps all physical geometry in millimetres and converts only +at the editor boundary. Alongside the fixed address, badge and asset layouts, +`labels/template.ts` validates a reusable v1 element model for draggable text, +image, barcode and shape elements. Merge expressions are fixed `{{field}}` +lookups; they never evaluate code. Bounds include an explicit bleed allowance, +and barcode elements report format-specific quiet-zone and minimum-size risks. + +SVG pages use physical dimensions and application-owned escaped markup. PDF +export walks that generated markup into `pdf-lib` vector paths, rectangles and +embedded standard-font text; local PNG/JPEG assets remain image objects. No +page-wide rasterization is used. Unsupported WebP embedding remains visible in +SVG and is omitted from PDF rather than fetched or executed. + Label Tools separates untrusted input from rendering: 1. `labels/data.ts` uses toolbox helpers for bounded CSV/JSON parsing and the browser cryptographic RNG for optional virtual fields. 2. `labels/stocks.ts` stores canonical millimetre geometry and validates every preset/custom grid before rendering. -3. `labels/render.ts` maps scalar fields into three fixed layouts, escapes XML, - embeds validated local raster data, and places reviewed bwip-js output. +3. `labels/render.ts` maps scalar fields into three fixed layouts or the + validated designer model, escapes XML, embeds validated local raster data, + and places reviewed bwip-js output. 4. `labels/export.ts` exports exact SVG pages, deterministic fflate ZIPs, or - locally rasterized pdf-lib documents. + vector pdf-lib documents assembled from application-owned SVG markup. The React workbench retains the last valid data model after parse errors and limits interactive preview to the first 12 pages. Complete SVG export remains diff --git a/docs/PRIVACY-SECURITY.md b/docs/PRIVACY-SECURITY.md index 98a037a..3f6f921 100644 --- a/docs/PRIVACY-SECURITY.md +++ b/docs/PRIVACY-SECURITY.md @@ -7,10 +7,12 @@ inside the symbol; they are never requested. CSV/JSON structure, field lengths, record counts, image bytes/pixels, grid dimensions, calibration and output pages are bounded. Object URLs are revoked. -User strings are normalized for label display and XML-escaped. Only built-in -templates are supported, and only locally decoded PNG/JPEG/WebP is embedded. +User strings are normalized for label display and XML-escaped. Imported +templates use a bounded, declarative v1 schema with no script or remote-resource +field, and only locally decoded PNG/JPEG/WebP is embedded. Printing crosses a physical boundary the browser cannot verify: installed font metrics, device drivers, page scaling, feed skew and stock tolerances. Use the -calibration controls and test on plain paper. PDF is a 144 DPI raster export and -does not embed or preserve font semantics. +calibration controls and test on plain paper. PDF uses vector geometry and +embedded standard fonts, but it does not preserve the selected system-font +metrics or provide archival guarantees. diff --git a/package-lock.json b/package-lock.json index b17a7c1..1370203 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,17 @@ { "name": "label-tools", - "version": "0.1.0", + "version": "0.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "label-tools", - "version": "0.1.0", + "version": "0.2.0", "license": "GPL-3.0-or-later", "dependencies": { - "@add-ideas/toolbox-contract": "0.2.3", - "@add-ideas/toolbox-helpers": "0.1.0", - "@add-ideas/toolbox-shell-react": "0.2.3", + "@add-ideas/toolbox-contract": "0.3.0", + "@add-ideas/toolbox-helpers": "0.2.0", + "@add-ideas/toolbox-shell-react": "0.3.0", "bwip-js": "4.11.4", "fflate": "0.8.3", "pdf-lib": "1.17.1", @@ -19,7 +19,7 @@ "react-dom": "19.2.8" }, "devDependencies": { - "@add-ideas/toolbox-testkit": "0.2.3", + "@add-ideas/toolbox-testkit": "0.3.0", "@eslint/js": "10.0.1", "@playwright/test": "1.62.1", "@testing-library/jest-dom": "6.9.1", @@ -45,24 +45,24 @@ } }, "node_modules/@add-ideas/toolbox-contract": { - "version": "0.2.3", - "resolved": "https://git.add-ideas.de/api/packages/lotobo/npm/%40add-ideas%2Ftoolbox-contract/-/0.2.3/toolbox-contract-0.2.3.tgz", - "integrity": "sha512-T0PVSuMT40GjTDfQJhEEY3ZawQq8zz1/ry95JdKI6W39CdLacaRXdGnEpDCMHt+jUbf1Jz7Nat/M5dFCgKVM9A==", + "version": "0.3.0", + "resolved": "https://git.add-ideas.de/api/packages/lotobo/npm/%40add-ideas%2Ftoolbox-contract/-/0.3.0/toolbox-contract-0.3.0.tgz", + "integrity": "sha512-dKrK7BjOFwqJaBfJuhKxZKIld4sH0AKjEn6a0yLnbdMUFY+fFv4VSLGV2tNSBD016gumc2iNqOjUj/ld7x4rtA==", "license": "Apache-2.0" }, "node_modules/@add-ideas/toolbox-helpers": { - "version": "0.1.0", - "resolved": "https://git.add-ideas.de/api/packages/lotobo/npm/%40add-ideas%2Ftoolbox-helpers/-/0.1.0/toolbox-helpers-0.1.0.tgz", - "integrity": "sha512-UKl1Oxekedf8D2df86VrnVA53AcMhrnh6iUPXY+k8frirBXotb0yd8SGT+IF/3hcqYwcYe/v9WVFuSgKtIYVnw==", + "version": "0.2.0", + "resolved": "https://git.add-ideas.de/api/packages/lotobo/npm/%40add-ideas%2Ftoolbox-helpers/-/0.2.0/toolbox-helpers-0.2.0.tgz", + "integrity": "sha512-SdOqkw+P+3J3fa5iVkzb5P15rVepB001GNV21Oh8w0CZcVL+YRltgD/s+MVcTyrNijWQf3E5vtQON/3N2LLyKg==", "license": "GPL-3.0-or-later" }, "node_modules/@add-ideas/toolbox-shell-react": { - "version": "0.2.3", - "resolved": "https://git.add-ideas.de/api/packages/lotobo/npm/%40add-ideas%2Ftoolbox-shell-react/-/0.2.3/toolbox-shell-react-0.2.3.tgz", - "integrity": "sha512-DT5lQDH48BFkFcmFLZnQh7+Cm73JzBPcmp5WzUXypfkUXpEyDYHzaXgmW4kZ0edSwh4RK4sPmx+JPtK0X4aKCQ==", + "version": "0.3.0", + "resolved": "https://git.add-ideas.de/api/packages/lotobo/npm/%40add-ideas%2Ftoolbox-shell-react/-/0.3.0/toolbox-shell-react-0.3.0.tgz", + "integrity": "sha512-74p6JzAOG0YCAKdlc1hLofV4ZIko7vb448S75cIiM88PKm93EHl5VD7g8YVyfM56Ui97UY9dmy+Whiq4sGzpsg==", "license": "Apache-2.0", "dependencies": { - "@add-ideas/toolbox-contract": "0.2.3" + "@add-ideas/toolbox-contract": "0.3.0" }, "peerDependencies": { "react": ">=18 <20", @@ -70,13 +70,13 @@ } }, "node_modules/@add-ideas/toolbox-testkit": { - "version": "0.2.3", - "resolved": "https://git.add-ideas.de/api/packages/lotobo/npm/%40add-ideas%2Ftoolbox-testkit/-/0.2.3/toolbox-testkit-0.2.3.tgz", - "integrity": "sha512-sq1MwhKWfFKen+N+124hl74qQimRSvmQ9sOU7jdcI+2qCKZ67+2B8rWyezeV80uTFu4Jv6deHksfYQ/tKNV6XQ==", + "version": "0.3.0", + "resolved": "https://git.add-ideas.de/api/packages/lotobo/npm/%40add-ideas%2Ftoolbox-testkit/-/0.3.0/toolbox-testkit-0.3.0.tgz", + "integrity": "sha512-4Fk+oSvZFspOMIXr8Xy040nhAaBsIQAzsGyXWSpjn3+k3yBKq7nB1r5zCHhsXzfdLzvPDAx2KcmSNOhM330D9w==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@add-ideas/toolbox-contract": "0.2.3" + "@add-ideas/toolbox-contract": "0.3.0" }, "bin": { "toolbox-check": "dist/cli.js" diff --git a/package.json b/package.json index e9654a1..65d6f39 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "label-tools", - "version": "0.1.0", + "version": "0.2.0", "description": "Merge local data into printable labels and badges in the browser.", "license": "GPL-3.0-or-later", "author": "Albrecht Degering", @@ -39,9 +39,9 @@ "release:artifact": "npm run check && npm run test:browser && npm run package:release -- --force" }, "dependencies": { - "@add-ideas/toolbox-contract": "0.2.3", - "@add-ideas/toolbox-helpers": "0.1.0", - "@add-ideas/toolbox-shell-react": "0.2.3", + "@add-ideas/toolbox-contract": "0.3.0", + "@add-ideas/toolbox-helpers": "0.2.0", + "@add-ideas/toolbox-shell-react": "0.3.0", "bwip-js": "4.11.4", "fflate": "0.8.3", "pdf-lib": "1.17.1", @@ -49,7 +49,7 @@ "react-dom": "19.2.8" }, "devDependencies": { - "@add-ideas/toolbox-testkit": "0.2.3", + "@add-ideas/toolbox-testkit": "0.3.0", "@eslint/js": "10.0.1", "@playwright/test": "1.62.1", "@testing-library/jest-dom": "6.9.1", diff --git a/playwright.config.ts b/playwright.config.ts index 266bb16..a46071c 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -15,7 +15,25 @@ export default defineConfig({ timeout: 180_000, }, projects: [ - { name: "chromium", use: { ...devices["Desktop Chrome"] } }, - { name: "firefox", use: { ...devices["Desktop Firefox"] } }, + { + name: "chromium", + testIgnore: /responsive\.spec\.ts/, + use: { ...devices["Desktop Chrome"] }, + }, + { + name: "firefox", + testIgnore: /responsive\.spec\.ts/, + use: { ...devices["Desktop Firefox"] }, + }, + { + name: "webkit", + testIgnore: /responsive\.spec\.ts/, + use: { ...devices["Desktop Safari"] }, + }, + { + name: "mobile-chromium", + testMatch: /responsive\.spec\.ts/, + use: { ...devices["Pixel 5"] }, + }, ], }); diff --git a/public/CHANGELOG.md b/public/CHANGELOG.md index 1e9c898..0850604 100644 --- a/public/CHANGELOG.md +++ b/public/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 0.2.0 - 2026-09-02 + +- Add a visual label designer, richer vector templates, deterministic + multi-page SVG/ZIP output and vector PDF export with calibrated geometry. + ## 0.1.0 - 2026-09-01 - Add bounded CSV/JSON data merge, flattening and field mapping. diff --git a/public/LICENSES/npm-runtime-licenses.txt b/public/LICENSES/npm-runtime-licenses.txt index ee0c9f3..d7ddaa6 100644 --- a/public/LICENSES/npm-runtime-licenses.txt +++ b/public/LICENSES/npm-runtime-licenses.txt @@ -1,5 +1,5 @@ ============================================================================== -@add-ideas/toolbox-contract@0.2.3 +@add-ideas/toolbox-contract@0.3.0 Declared licence: Apache-2.0 ============================================================================== --- LICENSE --- @@ -198,7 +198,7 @@ Declared licence: Apache-2.0 ============================================================================== -@add-ideas/toolbox-helpers@0.1.0 +@add-ideas/toolbox-helpers@0.2.0 Declared licence: GPL-3.0-or-later ============================================================================== --- LICENSE --- @@ -879,7 +879,7 @@ Public License instead of this License. But first, please read ============================================================================== -@add-ideas/toolbox-shell-react@0.2.3 +@add-ideas/toolbox-shell-react@0.3.0 Declared licence: Apache-2.0 ============================================================================== --- LICENSE --- diff --git a/public/README.md b/public/README.md index 5904f67..d66dd43 100644 --- a/public/README.md +++ b/public/README.md @@ -1,9 +1,17 @@ # Label Tools +Label Tools combines bounded CSV/JSON data merge with calibrated label stocks +and a reusable visual template designer. Text, image, barcode and shape +elements can be positioned by drag, keyboard or exact mm/in/pt fields; template +JSON round-trips locally. Bleed, sheet bounds, barcode quiet zones and likely +scan-size problems are checked before print. Physical-size SVG and vector PDF +exports retain paths and text, while local raster images remain embedded image +objects. + Label Tools is a production-oriented, local-first browser studio for merging CSV/JSON records into printable label, badge and asset-tag sheets. -Version 0.1.0 includes: +Version 0.2.0 includes: - bounded CSV and JSON parsing with nested JSON field flattening; - explicit field mapping plus local serial and cryptographically random values; @@ -16,7 +24,7 @@ Version 0.1.0 includes: - start-slot selection, X/Y offset and independent scale calibration; - optional cut guides and registration marks; - paginated print preview and explicit print calibration guidance; -- vector SVG, deterministic multi-page SVG/report ZIP, and 144 DPI PDF export. +- vector SVG, deterministic multi-page SVG/report ZIP, and vector PDF export. Data and assets remain in browser memory. There is no telemetry, remote API, font download, CDN asset or automatic request. @@ -42,9 +50,11 @@ the actual sheet, print the registration/cut marks on plain paper, choose stock. Printer feed and unprintable margins are outside browser control. SVG uses one of four system font stacks and does not embed fonts. Results can -therefore vary by operating system. PDF export intentionally rasterizes each -generated SVG page at 144 DPI, preserving the preview appearance on the same -device but not creating an archival or press-quality font-embedded PDF. +therefore vary by operating system. PDF export maps application-owned SVG +geometry into pdf-lib vector paths, rectangles and embedded Helvetica text. +Characters outside the built-in WinAnsi repertoire are replaced visibly, and +unsupported WebP image objects remain available in SVG but are omitted from +PDF. The result is print-accurate in physical dimensions, not an archival PDF. ## Limits diff --git a/public/SECURITY.md b/public/SECURITY.md index 0977b69..c8df721 100644 --- a/public/SECURITY.md +++ b/public/SECURITY.md @@ -7,7 +7,8 @@ Label Tools keeps data in browser memory and accepts only bounded CSV/JSON and PNG/JPEG/WebP assets. Merge text and SVG attributes are escaped. Generated barcode markup comes from a pinned local encoder and is checked for active or linked elements. Remote image URLs, user-provided SVG and arbitrary templates -are not accepted in v0.1. +containing markup or code are not accepted in v0.2. Imported template JSON is +restricted to the bounded declarative v1 element schema. Generated labels can contain sensitive or guessable identifiers. Review the preview and warnings before export, and do not treat serial/random values as an diff --git a/public/SOURCE.md b/public/SOURCE.md index 25f0708..68bb0a0 100644 --- a/public/SOURCE.md +++ b/public/SOURCE.md @@ -1,7 +1,7 @@ # Source identity - Project: Label Tools -- Version: 0.1.0 +- Version: 0.2.0 - Canonical repository: https://git.add-ideas.de/lotobo/label-tools - Licence: GPL-3.0-or-later - Toolbox app id: `de.add-ideas.label-tools` diff --git a/public/THIRD_PARTY_NOTICES.md b/public/THIRD_PARTY_NOTICES.md index 07cbfd5..2ead298 100644 --- a/public/THIRD_PARTY_NOTICES.md +++ b/public/THIRD_PARTY_NOTICES.md @@ -5,7 +5,8 @@ never loaded from a CDN. Their licence texts are copied into the production artifact at build time. - React and React DOM — MIT -- add·ideas Toolbox Contract, Shell React and Helpers — GPL-3.0-or-later +- add·ideas Toolbox Contract and Shell React 0.3.0 — Apache-2.0 +- add·ideas Toolbox Helpers 0.2.0 — GPL-3.0-or-later - bwip-js — MIT - fflate — MIT - pdf-lib and its runtime dependencies — MIT diff --git a/public/docs/ARCHITECTURE.md b/public/docs/ARCHITECTURE.md index 7339606..b25fc4a 100644 --- a/public/docs/ARCHITECTURE.md +++ b/public/docs/ARCHITECTURE.md @@ -1,15 +1,29 @@ # Architecture +The sheet engine keeps all physical geometry in millimetres and converts only +at the editor boundary. Alongside the fixed address, badge and asset layouts, +`labels/template.ts` validates a reusable v1 element model for draggable text, +image, barcode and shape elements. Merge expressions are fixed `{{field}}` +lookups; they never evaluate code. Bounds include an explicit bleed allowance, +and barcode elements report format-specific quiet-zone and minimum-size risks. + +SVG pages use physical dimensions and application-owned escaped markup. PDF +export walks that generated markup into `pdf-lib` vector paths, rectangles and +embedded standard-font text; local PNG/JPEG assets remain image objects. No +page-wide rasterization is used. Unsupported WebP embedding remains visible in +SVG and is omitted from PDF rather than fetched or executed. + Label Tools separates untrusted input from rendering: 1. `labels/data.ts` uses toolbox helpers for bounded CSV/JSON parsing and the browser cryptographic RNG for optional virtual fields. 2. `labels/stocks.ts` stores canonical millimetre geometry and validates every preset/custom grid before rendering. -3. `labels/render.ts` maps scalar fields into three fixed layouts, escapes XML, - embeds validated local raster data, and places reviewed bwip-js output. +3. `labels/render.ts` maps scalar fields into three fixed layouts or the + validated designer model, escapes XML, embeds validated local raster data, + and places reviewed bwip-js output. 4. `labels/export.ts` exports exact SVG pages, deterministic fflate ZIPs, or - locally rasterized pdf-lib documents. + vector pdf-lib documents assembled from application-owned SVG markup. The React workbench retains the last valid data model after parse errors and limits interactive preview to the first 12 pages. Complete SVG export remains diff --git a/public/docs/PRIVACY-SECURITY.md b/public/docs/PRIVACY-SECURITY.md index 98a037a..3f6f921 100644 --- a/public/docs/PRIVACY-SECURITY.md +++ b/public/docs/PRIVACY-SECURITY.md @@ -7,10 +7,12 @@ inside the symbol; they are never requested. CSV/JSON structure, field lengths, record counts, image bytes/pixels, grid dimensions, calibration and output pages are bounded. Object URLs are revoked. -User strings are normalized for label display and XML-escaped. Only built-in -templates are supported, and only locally decoded PNG/JPEG/WebP is embedded. +User strings are normalized for label display and XML-escaped. Imported +templates use a bounded, declarative v1 schema with no script or remote-resource +field, and only locally decoded PNG/JPEG/WebP is embedded. Printing crosses a physical boundary the browser cannot verify: installed font metrics, device drivers, page scaling, feed skew and stock tolerances. Use the -calibration controls and test on plain paper. PDF is a 144 DPI raster export and -does not embed or preserve font semantics. +calibration controls and test on plain paper. PDF uses vector geometry and +embedded standard fonts, but it does not preserve the selected system-font +metrics or provide archival guarantees. diff --git a/public/sw.js b/public/sw.js index 6a520e5..e301bb3 100644 --- a/public/sw.js +++ b/public/sw.js @@ -1,4 +1,4 @@ -const CACHE = "label-tools-v0.1.0"; +const CACHE = "label-tools-v0.2.0"; const APP = [ "./", "./index.html", diff --git a/public/toolbox-app.json b/public/toolbox-app.json index 009e398..f140bfa 100644 --- a/public/toolbox-app.json +++ b/public/toolbox-app.json @@ -3,7 +3,7 @@ "schemaVersion": 1, "id": "de.add-ideas.label-tools", "name": "Label Tools", - "version": "0.1.0", + "version": "0.2.0", "description": "Merge and print labels locally.", "entry": "./", "icon": "./favicon.svg", @@ -21,6 +21,23 @@ "crossOriginIsolated": false, "topLevelContext": false }, + "io": { + "accepts": [ + { "mediaType": "text/csv", "extensions": [".csv"] }, + { "mediaType": "application/json", "extensions": [".json"] }, + { + "mediaType": "image/*", + "extensions": [".png", ".jpg", ".jpeg", ".webp"] + } + ], + "produces": [ + { "mediaType": "application/pdf", "extensions": [".pdf"] }, + { "mediaType": "image/svg+xml", "extensions": [".svg"] }, + { "mediaType": "application/zip", "extensions": [".zip"] }, + { "mediaType": "application/json", "extensions": [".json"] } + ] + }, + "capabilities": { "required": [], "optional": ["workers"] }, "privacy": { "processing": "local", "fileUploads": true, diff --git a/src/components/DesignerPanel.tsx b/src/components/DesignerPanel.tsx new file mode 100644 index 0000000..1616539 --- /dev/null +++ b/src/components/DesignerPanel.tsx @@ -0,0 +1,556 @@ +import { useRef, useState, type PointerEvent } from "react"; +import { triggerBlobDownload } from "@add-ideas/toolbox-helpers"; +import { BARCODE_FORMATS } from "../labels/barcode"; +import { fromMillimetres, toMillimetres } from "../labels/stocks"; +import { + exportTemplate, + importTemplate, + resizeTemplate, + validateTemplate, +} from "../labels/template"; +import type { + BarcodeFormat, + LabelElement, + LabelTemplate, + Unit, +} from "../labels/types"; + +export function DesignerPanel({ + template, + onChange, + targetWidthMm, + targetHeightMm, + unit, +}: { + template: LabelTemplate; + onChange: (template: LabelTemplate) => void; + targetWidthMm: number; + targetHeightMm: number; + unit: Unit; +}) { + const [selectedId, setSelectedId] = useState(template.elements[0]?.id ?? ""); + const [message, setMessage] = useState(""); + const svgRef = useRef(null); + const drag = useRef< + | { + id: string; + x: number; + y: number; + startX: number; + startY: number; + } + | undefined + >(undefined); + const selected = template.elements.find( + (element) => element.id === selectedId, + ); + const issues = validateTemplate(template); + const updateElement = (id: string, patch: Partial) => + onChange({ + ...template, + elements: template.elements.map((element) => + element.id === id + ? ({ ...element, ...patch } as LabelElement) + : element, + ), + }); + const add = (kind: LabelElement["kind"]) => { + const existing = new Set(template.elements.map((element) => element.id)); + let suffix = 1; + while (existing.has(`${kind}-${suffix}`)) suffix += 1; + const id = `${kind}-${suffix}`; + const geometry = { id, xMm: 2, yMm: 2, widthMm: 24, heightMm: 10 }; + const element: LabelElement = + kind === "text" + ? { + ...geometry, + kind, + content: "{{name}}", + fontSizePt: 10, + fontWeight: "400", + align: "left", + color: "#111827", + } + : kind === "barcode" + ? { + ...geometry, + kind, + content: "{{code}}", + format: "qrcode", + quietZoneMm: 2, + includeText: false, + } + : kind === "image" + ? { + ...geometry, + kind, + content: "{{image}}", + fit: "contain", + } + : { + ...geometry, + kind, + fill: "#ffffff", + stroke: "#111827", + strokeWidthMm: 0.2, + radiusMm: 1, + }; + onChange({ ...template, elements: [...template.elements, element] }); + setSelectedId(id); + }; + const position = (event: PointerEvent) => { + const box = svgRef.current?.getBoundingClientRect(); + if (!box) return { x: 0, y: 0 }; + return { + x: + ((event.clientX - box.left) / box.width) * + (template.widthMm + template.bleedMm * 2) - + template.bleedMm, + y: + ((event.clientY - box.top) / box.height) * + (template.heightMm + template.bleedMm * 2) - + template.bleedMm, + }; + }; + async function open(file: File | undefined) { + if (!file) return; + try { + const next = importTemplate(await file.text()); + onChange(next); + setSelectedId(next.elements[0]?.id ?? ""); + setMessage(`Opened ${file.name}.`); + } catch (error) { + setMessage(error instanceof Error ? error.message : String(error)); + } + } + return ( +
+
+
+

Reusable vector template

+

Designer canvas

+
+ {template.elements.length} elements +
+
+ {(["text", "barcode", "image", "shape"] as const).map((kind) => ( + + ))} + + + +
+ + + { + const active = drag.current; + if (!active) return; + const point = position(event); + const element = template.elements.find( + (item) => item.id === active.id, + ); + if (!element) return; + updateElement(active.id, { + xMm: clamp( + active.x + point.x - active.startX, + -template.bleedMm, + template.widthMm + template.bleedMm - element.widthMm, + ), + yMm: clamp( + active.y + point.y - active.startY, + -template.bleedMm, + template.heightMm + template.bleedMm - element.heightMm, + ), + }); + }} + onPointerUp={() => { + drag.current = undefined; + }} + onPointerCancel={() => { + drag.current = undefined; + }} + > + + + {template.elements.map((element) => ( + { + setSelectedId(element.id); + const point = position(event); + drag.current = { + id: element.id, + x: element.xMm, + y: element.yMm, + startX: point.x, + startY: point.y, + }; + event.currentTarget.setPointerCapture(event.pointerId); + }} + onKeyDown={(event) => { + const amount = event.shiftKey ? 1 : 0.1; + const delta = + event.key === "ArrowLeft" + ? [-amount, 0] + : event.key === "ArrowRight" + ? [amount, 0] + : event.key === "ArrowUp" + ? [0, -amount] + : event.key === "ArrowDown" + ? [0, amount] + : undefined; + if (!delta) return; + event.preventDefault(); + updateElement(element.id, { + xMm: element.xMm + delta[0]!, + yMm: element.yMm + delta[1]!, + }); + }} + > + + + {element.kind} + + + ))} + + {selected && ( +
+
+

+ {selected.kind} · {selected.id} +

+ +
+
+ {(["xMm", "yMm", "widthMm", "heightMm"] as const).map((key) => ( + + ))} +
+ {selected.kind !== "shape" && ( + + )} + {selected.kind === "barcode" && ( + <> + + + + + )} + {selected.kind === "text" && ( +
+ + + + +
+ )} + {selected.kind === "image" && ( + + )} + {selected.kind === "shape" && ( +
+ + + + +
+ )} +
+ )} + {issues.length ? ( +
    + {issues.map((issue, index) => ( +
  • + {issue.message} +
  • + ))} +
+ ) : ( +

+ Template geometry and barcode quiet zones are valid. +

+ )} + {message &&

{message}

} +
+ ); +} + +function clamp(value: number, minimum: number, maximum: number) { + return Math.max(minimum, Math.min(maximum, value)); +} +function round(value: number) { + return Number(value.toFixed(3)); +} diff --git a/src/components/HelpDialog.tsx b/src/components/HelpDialog.tsx index fa37e5a..8d12f6a 100644 --- a/src/components/HelpDialog.tsx +++ b/src/components/HelpDialog.tsx @@ -43,7 +43,8 @@ export function HelpDialog({

Paper feed, printer scaling and installed system fonts vary. Print a calibration sheet on plain paper at 100% / actual size before using - label stock. PDF export rasterizes at 144 DPI and does not embed fonts. + label stock. PDF export keeps reviewed geometry vector and embeds its + standard fonts; SVG continues to use the selected system font stack.

); diff --git a/src/components/Workbench.tsx b/src/components/Workbench.tsx index 917a4c0..5eef638 100644 --- a/src/components/Workbench.tsx +++ b/src/components/Workbench.tsx @@ -5,6 +5,7 @@ import { augmentRecords, collectFields, parseMergeData } from "../labels/data"; import { createSvgZip, svgFilename } from "../labels/export"; import { loadImageAssets } from "../labels/images"; import { renderLabelPages } from "../labels/render"; +import { defaultTemplate } from "../labels/template"; import { fromMillimetres, STOCKS, @@ -23,6 +24,7 @@ import type { RenderOptions, Unit, } from "../labels/types"; +import { DesignerPanel } from "./DesignerPanel"; const SAMPLE = `name,subtitle,address,code,image Ada Lovelace,Research,12 Analytical Engine Way · London,ASSET-0001, @@ -67,6 +69,9 @@ export function Workbench() { }); const [unit, setUnit] = useState("mm"); const [layout, setLayout] = useState("address"); + const [template, setTemplate] = useState(() => + defaultTemplate(STOCKS[0]!.labelWidthMm, STOCKS[0]!.labelHeightMm), + ); const [barcodeFormat, setBarcodeFormat] = useState("qrcode"); const [includeBarcodeText, setIncludeBarcodeText] = useState(false); const [fontFamily, setFontFamily] = useState("Arial, Helvetica, sans-serif"); @@ -105,6 +110,7 @@ export function Workbench() { cutMarks, registrationMarks, startPosition, + template, }), [ stock, @@ -120,6 +126,7 @@ export function Workbench() { cutMarks, registrationMarks, startPosition, + template, ], ); const preview = useMemo(() => { @@ -197,7 +204,7 @@ export function Workbench() { } async function downloadPdf() { - setStatus("Rasterizing pages locally for PDF export…"); + setStatus("Building vector PDF pages locally…"); try { const result = fullRender(); const { createPdf } = await import("../labels/pdf"); @@ -206,7 +213,7 @@ export function Workbench() { "label-tools-merge.pdf", ); setStatus( - `Exported ${result.totalPages} PDF page${result.totalPages === 1 ? "" : "s"} locally at 144 DPI.`, + `Exported ${result.totalPages} vector PDF page${result.totalPages === 1 ? "" : "s"} locally.`, ); } catch (error) { setStatus(error instanceof Error ? error.message : String(error)); @@ -274,6 +281,15 @@ export function Workbench() { fontFamily={fontFamily} setFontFamily={setFontFamily} /> + {layout === "designer" && ( + + )} void downloadPdf()} disabled={!preview.result} > - PDF (144 DPI) + Vector PDF