Compare commits

...
Author SHA1 Message Date
james.maesandClaude Fable 5.2 22063ccfb1 docs(design): labels in physical units, text and barcode fit, fonts everywhere, preview in the QBit
build / maven (push) Successful in 8m37s
maven / build (push) Successful in 8m37s
Design and work plan only; no code changes. Covers scene version 2 beside a frozen
version 1, bundled and database-stored fonts, whole-dot barcode fitting with
centring, structured diagnostics, a preview decoded from the validated ZPL, the
release order and what GG Forge pins and deletes.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-11 16:42:32 -06:00
CI 9a355670c0 Bump version to 1.4.0-SNAPSHOT [skip ci] 2026-11-10 23:13:57 +00:00
james.maes 228931a937 Merge pull request 'fix(postgres): accept full_page_writes off on Amazon Aurora PostgreSQL only' (#9) from fix/aurora-full-page-writes into develop
build / maven (push) Successful in 16m12s
maven / build (push) Successful in 16m12s
2026-11-10 22:57:56 +00:00
james.maesandClaude Fable 5.2 cfbf0744f9 fix(postgres): accept full_page_writes off on Amazon Aurora PostgreSQL only
build / maven (push) Successful in 6m14s
maven / build (push) Successful in 6m14s
Generation qualification refused every Amazon Aurora PostgreSQL server:
Aurora always reports full_page_writes = off and has no parameter to change
it, because its storage layer does not write torn pages.

The settings query now also returns
to_regprocedure('aurora_version()') IS NOT NULL (true on Aurora, false on
stock PostgreSQL, never an error). full_page_writes = off is accepted only
when that is true. Stock PostgreSQL must still report it on, and fsync on,
synchronous_commit on or remote_apply and every other check are unchanged
on both. The refusal text now says full-page writes are not required on
Aurora.

Tests run on a real postgres:17 engine whose settings are really changed;
only Aurora is simulated, by creating a function named aurora_version() in
the test database.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-09 20:25:55 -06:00
CI 1dc265647d Bump version to 1.3.0-SNAPSHOT [skip ci] 2026-11-09 02:46:35 +00:00
james.maes e762dac5d5 Merge pull request 'build(renderer): build engine and service images from a released version' (#7) from fix/renderer-images into develop
build / maven (push) Successful in 6m43s
maven / build (push) Successful in 6m43s
2026-11-09 02:39:57 +00:00
james.maesandClaude Sonnet 5.5 e19feb01a8 build(renderer): build the engine and service images from a released version
build / maven (push) Successful in 6m2s
maven / build (push) Successful in 6m2s
Both Dockerfiles copied 0.1.0-SNAPSHOT jars that the reactor no longer produces
(version is ${revision}), so neither built a released version. They now resolve the
released qbit-document-renderer and qqq-remote-renderer-transport (and their runtime
libraries) at an exact, required RENDERER_VERSION / TRANSPORT_VERSION from the
QRun-IO Gitea Maven registry and Maven Central, refusing SNAPSHOT and local
(*forge*) versions. The reviewed hardening is unchanged: digest-pinned
BUILD_IMAGE/JAVA_IMAGE, non-root 65532, read-only-root friendly, Zint built from
vendor/. The reactor build context is no longer needed.

Adds IMAGE-PUBLISH.md (repeatable build/scan/publish steps and the 1.0.0 pins) and
image-pins.sh (prints the gg-forge-cd classpath and SHA256 pins from a published
image). Images for 1.0.0 were built, smoke-tested over mTLS and published to GHCR
under gg-engineering by hand; no workflow publishes images.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-08 20:07:14 -06:00
CI d8a57607fd Bump version to 1.2.0-SNAPSHOT [skip ci] 2026-11-07 19:19:02 +00:00
james.maes 889296d8f5 Merge pull request 'build(deps): align with Gitea registry (parent 2.2.1, QQQ 4.1.0-RC.26)' (#6) from deps/align-gitea into develop
build / maven (push) Successful in 11m52s
maven / build (push) Successful in 11m52s
2026-11-07 19:07:22 +00:00
james.maesandClaude Opus 5.5 e085859a25 build(deps): move qqq.version into <properties>
build / maven (push) Successful in 7m0s
maven / build (push) Successful in 7m0s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 18:59:59 +00:00
james.maesandClaude Opus 5.5 4f850c2cf3 build(deps): align with Gitea registry (parent 2.2.1, QQQ 4.1.0-RC.26)
build / maven (push) Failing after 9s
maven / build (push) Failing after 9s
bridges/printing/pom.xml: qqq.version: 4.1.0-RC.22 -> 4.1.0-RC.26
bridges/printing/pom.xml: qbit-record-files.version: 0.1.0-RC.1 -> 0.4.0-RC.1
bridges/printing/pom.xml: qbit-printing.version: 1.0.0 -> 1.2.0-RC.1
bridges/record-files/pom.xml: qqq.version: 4.1.0-RC.22 -> 4.1.0-RC.26
bridges/record-files/pom.xml: qbit-record-files.version: 0.1.0-RC.1 -> 0.4.0-RC.1
bridges/scripting/pom.xml: qqq.version: 4.1.0-RC.22 -> 4.1.0-RC.26
bridges/scripting/pom.xml: qbit-scripting.version: 1.0.0 -> 1.1.0-RC.3
foundation/pom.xml: qqq.version: 4.1.0-RC.22 -> 4.1.0-RC.26
pom.xml: parent -> 2.2.1
pom.xml: added qqq.version=4.1.0-RC.26 (parent BOM import)
pom.xml: revision -> 1.1.0-SNAPSHOT

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 18:59:01 +00:00
CI f9adf1d3d3 Bump version to 0.3.0-SNAPSHOT [skip ci] 2026-11-06 23:35:57 +00:00
james.maes 2dcee6fbea Merge pull request 'ci: pin shared workflow to v1' (#4) from ci/pin-v1-develop into develop
QRun-IO/ci-policy: ci / fork-guard (push) Skipped
QRun-IO/ci-policy: native / fork-deny (push) Skipped
QRun-IO/ci-policy: native / dotnet-0 (push) Skipped
QRun-IO/ci-policy: ci / Scoped workflow [workflow 9cb8c13124a99d18da83cbcd4faa795859ac9e55] (push) Successful in 9s
QRun-IO/ci-policy: native / native-result (push) Successful in 6s
QRun-IO/ci-policy: ci / head-profile (push) Skipped
QRun-IO/ci-policy: ci / repository-policy (push) Skipped
QRun-IO/ci-policy: ci / application-template-0 (push) Skipped
QRun-IO/ci-policy: ci / java-core-2-head (push) Skipped
QRun-IO/ci-policy: ci / java-docker-3 (push) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (push) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-5 (push) Skipped
QRun-IO/ci-policy: ci / java-maven-7-head (push) Skipped
QRun-IO/ci-policy: ci / node-8 (push) Skipped
QRun-IO/ci-policy: ci / node-9 (push) Skipped
QRun-IO/ci-policy: ci / node-10 (push) Skipped
QRun-IO/ci-policy: ci / node-playwright-11 (push) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (push) Skipped
QRun-IO/ci-policy: ci / source-config-13 (push) Skipped
QRun-IO/ci-policy: ci / source-config-14 (push) Skipped
QRun-IO/ci-policy: ci / original-business (push) Skipped
QRun-IO/ci-policy: ci / original-voyage (push) Skipped
QRun-IO/ci-policy: ci / original-cli (push) Skipped
QRun-IO/ci-policy: ci / ci-result (push) Waiting to run
build / maven (push) Successful in 19m19s
QRun-IO/ci-policy: ci / gradle-1 (push) Skipped
QRun-IO/ci-policy: ci / java-docker-3-head (push) Skipped
QRun-IO/ci-policy: ci / java-maven-6 (push) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (push) Skipped
QRun-IO/ci-policy: ci / java-maven-7 (push) Skipped
QRun-IO/ci-policy: ci / android-0 (push) Skipped
QRun-IO/ci-policy: ci / original-mcp (push) Skipped
QRun-IO/ci-policy: ci / not-enrolled (push) Skipped
QRun-IO/ci-policy: ci / shared-pipeline (push) Skipped
maven / build (push) Successful in 19m19s
2026-11-06 23:11:03 +00:00
James MaesandClaude Opus 5.5 34a3666bbf Merge develop: adopt the released Document Generation 1.0 changes
QRun-IO/ci-policy: ci / fork-guard (pull_request) Skipped
QRun-IO/ci-policy: native / fork-deny (pull_request) Skipped
QRun-IO/ci-policy: native / dotnet-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / Scoped workflow [workflow 9cb8c13124a99d18da83cbcd4faa795859ac9e55] (pull_request) Successful in 7s
QRun-IO/ci-policy: ci / head-profile (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy (pull_request) Skipped
QRun-IO/ci-policy: ci / application-template-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-core-2-head (pull_request) Skipped
QRun-IO/ci-policy: native / native-result (pull_request) Successful in 6s
QRun-IO/ci-policy: ci / java-maven-6 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7-head (pull_request) Skipped
QRun-IO/ci-policy: ci / node-8 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-10 (pull_request) Skipped
QRun-IO/ci-policy: ci / android-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / original-business (pull_request) Skipped
QRun-IO/ci-policy: ci / original-mcp (pull_request) Skipped
QRun-IO/ci-policy: ci / original-voyage (pull_request) Skipped
QRun-IO/ci-policy: ci / original-cli (pull_request) Skipped
QRun-IO/ci-policy: ci / not-enrolled (pull_request) Skipped
QRun-IO/ci-policy: ci / shared-pipeline (pull_request) Skipped
QRun-IO/ci-policy: ci / ci-result (pull_request) Waiting to run
build / maven (push) Successful in 8m5s
QRun-IO/ci-policy: ci / gradle-1 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-5 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-9 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-playwright-11 (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-13 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-14 (pull_request) Skipped
maven / build (push) Successful in 8m5s
develop (PR #5) already fixed the renderer BarcodeDeclaredCapabilityTest
gaps, moved the family to QQQ 4.1.0-RC.22 and pinned the released Record
Files 0.1.0-RC.1 and Scripting 1.0.0, which is what resolves the scripting
bridge's "reviewed core isolation marker metadata" errors from run 6360.
Take develop's implementation and docs wholesale so this branch only
carries its own change: pinning the shared Maven workflow to v1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 16:54:15 -06:00
James MaesandClaude Opus 5.5 6a3153456e build: align the family on QQQ 4.1.0-RC.22
QRun-IO/ci-policy: ci / fork-guard (pull_request) Skipped
QRun-IO/ci-policy: native / fork-deny (pull_request) Skipped
QRun-IO/ci-policy: native / dotnet-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / Scoped workflow [workflow 9cb8c13124a99d18da83cbcd4faa795859ac9e55] (pull_request) Successful in 6s
QRun-IO/ci-policy: native / native-result (pull_request) Successful in 5s
QRun-IO/ci-policy: ci / head-profile (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy (pull_request) Skipped
QRun-IO/ci-policy: ci / application-template-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-core-2-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7-head (pull_request) Skipped
QRun-IO/ci-policy: ci / node-8 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-10 (pull_request) Skipped
QRun-IO/ci-policy: ci / android-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / original-business (pull_request) Skipped
QRun-IO/ci-policy: ci / original-mcp (pull_request) Skipped
QRun-IO/ci-policy: ci / original-voyage (pull_request) Skipped
QRun-IO/ci-policy: ci / original-cli (pull_request) Skipped
QRun-IO/ci-policy: ci / not-enrolled (pull_request) Skipped
QRun-IO/ci-policy: ci / shared-pipeline (pull_request) Skipped
QRun-IO/ci-policy: ci / ci-result (pull_request) Successful in 4s
build / maven (push) Successful in 8m48s
QRun-IO/ci-policy: ci / gradle-1 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-5 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-9 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-playwright-11 (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-13 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-14 (pull_request) Skipped
maven / build (push) Successful in 8m48s
The published qbit-scripting 0.1.0-SNAPSHOT (and its tests jar) is built
against QQQ Core 4.1.0-RC.22, whose Script entity carries the reviewed
executionMode isolation marker. The scripting bridge pinned RC.21 through
dependencyManagement, so ScriptingQBitProducer rejected the core script
table ("Scripting requires the reviewed core isolation marker metadata")
in every PublishedScriptContextAdapterTest. qbit-record-files and
qbit-printing have also moved to RC.22, so move foundation and all bridges
together rather than pinning older sibling snapshots.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 16:53:07 -06:00
CI 9073e9bf1c Bump version to 0.2.0-SNAPSHOT [skip ci] 2026-11-06 21:07:16 +00:00
james.maes 98eef612d0 Merge pull request 'Document Generation 1.0: fix renderer tests; pin released Record Files, Scripting and Core RC.22' (#5) from kof22/renderer-fix into develop
QRun-IO/ci-policy: ci / fork-guard (push) Skipped
QRun-IO/ci-policy: native / fork-deny (push) Skipped
QRun-IO/ci-policy: native / dotnet-0 (push) Skipped
QRun-IO/ci-policy: ci / head-profile (push) Skipped
QRun-IO/ci-policy: ci / repository-policy (push) Skipped
QRun-IO/ci-policy: native / native-result (push) Successful in 14s
QRun-IO/ci-policy: ci / application-template-0 (push) Skipped
QRun-IO/ci-policy: ci / java-core-2-head (push) Skipped
QRun-IO/ci-policy: ci / java-docker-3 (push) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (push) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-5 (push) Skipped
QRun-IO/ci-policy: ci / java-maven-7-head (push) Skipped
QRun-IO/ci-policy: ci / node-8 (push) Skipped
QRun-IO/ci-policy: ci / node-9 (push) Skipped
QRun-IO/ci-policy: ci / node-10 (push) Skipped
QRun-IO/ci-policy: ci / node-playwright-11 (push) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (push) Skipped
QRun-IO/ci-policy: ci / source-config-13 (push) Skipped
QRun-IO/ci-policy: ci / source-config-14 (push) Skipped
QRun-IO/ci-policy: ci / original-business (push) Skipped
QRun-IO/ci-policy: ci / original-voyage (push) Skipped
QRun-IO/ci-policy: ci / original-cli (push) Skipped
QRun-IO/ci-policy: ci / ci-result (push) Successful in 16s
build / maven (push) Successful in 11m33s
QRun-IO/ci-policy: ci / Scoped workflow [workflow 539bec410f7926fec3ef7fabe51934a91e0248be] (push) Successful in 12s
QRun-IO/ci-policy: ci / gradle-1 (push) Skipped
QRun-IO/ci-policy: ci / java-docker-3-head (push) Skipped
QRun-IO/ci-policy: ci / java-maven-6 (push) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (push) Skipped
QRun-IO/ci-policy: ci / java-maven-7 (push) Skipped
QRun-IO/ci-policy: ci / android-0 (push) Skipped
QRun-IO/ci-policy: ci / original-mcp (push) Skipped
QRun-IO/ci-policy: ci / not-enrolled (push) Skipped
QRun-IO/ci-policy: ci / shared-pipeline (push) Skipped
maven / build (push) Successful in 11m33s
2026-11-06 20:19:06 +00:00
james.maesandClaude Sonnet 5.5 cb420158ba release: prepare Document Generation 1.0 (released Record Files, Scripting, Core RC.22)
QRun-IO/ci-policy: ci / fork-guard (pull_request) Skipped
QRun-IO/ci-policy: native / fork-deny (pull_request) Skipped
QRun-IO/ci-policy: native / dotnet-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / Scoped workflow [workflow 539bec410f7926fec3ef7fabe51934a91e0248be] (pull_request) Successful in 10s
QRun-IO/ci-policy: ci / head-profile (pull_request) Skipped
QRun-IO/ci-policy: native / native-result (pull_request) Successful in 10s
QRun-IO/ci-policy: ci / repository-policy (pull_request) Skipped
QRun-IO/ci-policy: ci / application-template-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-core-2-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7-head (pull_request) Skipped
QRun-IO/ci-policy: ci / node-8 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-10 (pull_request) Skipped
QRun-IO/ci-policy: ci / android-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / original-business (pull_request) Skipped
QRun-IO/ci-policy: ci / original-mcp (pull_request) Skipped
QRun-IO/ci-policy: ci / original-voyage (pull_request) Skipped
QRun-IO/ci-policy: ci / original-cli (pull_request) Skipped
QRun-IO/ci-policy: ci / not-enrolled (pull_request) Skipped
QRun-IO/ci-policy: ci / ci-result (pull_request) Successful in 9s
build / maven (push) Successful in 12m21s
QRun-IO/ci-policy: ci / gradle-1 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-5 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-9 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-playwright-11 (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-13 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-14 (pull_request) Skipped
QRun-IO/ci-policy: ci / shared-pipeline (pull_request) Skipped
maven / build (push) Successful in 12m21s
Pin QQQ Core 4.1.0-RC.22, Record Files 0.1.0-RC.1 and Scripting 1.0.0 in place
of the SNAPSHOT placeholders, and replace the unreleased-checkpoint text.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-06 13:11:27 -06:00
james.maesandClaude Sonnet 5.5 10b3a716b5 fix(renderer): refuse barHeight where it has no effect; check the provider at the family version
QRun-IO/ci-policy: ci / fork-guard (pull_request) Skipped
QRun-IO/ci-policy: native / fork-deny (pull_request) Skipped
QRun-IO/ci-policy: native / dotnet-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / Scoped workflow [workflow 539bec410f7926fec3ef7fabe51934a91e0248be] (pull_request) Successful in 13s
QRun-IO/ci-policy: native / native-result (pull_request) Successful in 11s
QRun-IO/ci-policy: ci / head-profile (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy (pull_request) Skipped
QRun-IO/ci-policy: ci / application-template-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-core-2-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7-head (pull_request) Skipped
QRun-IO/ci-policy: ci / node-8 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-10 (pull_request) Skipped
QRun-IO/ci-policy: ci / android-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / original-business (pull_request) Skipped
QRun-IO/ci-policy: ci / original-mcp (pull_request) Skipped
QRun-IO/ci-policy: ci / original-voyage (pull_request) Skipped
QRun-IO/ci-policy: ci / original-cli (pull_request) Skipped
QRun-IO/ci-policy: ci / not-enrolled (pull_request) Skipped
QRun-IO/ci-policy: ci / shared-pipeline (pull_request) Skipped
QRun-IO/ci-policy: ci / ci-result (pull_request) Successful in 12s
build / maven (push) Failing after 16m35s
QRun-IO/ci-policy: ci / gradle-1 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-5 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-9 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-playwright-11 (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-13 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-14 (pull_request) Skipped
maven / build (push) Failing after 16m35s
barHeight was advertised and accepted for every encoder although the sixteen
fixed-geometry encoders (matrix symbols and postal four-state codes) ignore it.
It is now neither advertised nor accepted for them and is refused with the
closed code UNSUPPORTED_BARCODE_OPTION, as is GS1 DataBar in stacked mode. The
worker maps that code through, and VALIDATE refuses literal unsupported options
before any data exists.

The patched OpenHTML qualification compared against the old literal version;
it now compares against the family version surefire passes in.

All 174 renderer tests pass, including the four that failed.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-06 13:11:26 -06:00
James MaesandClaude Opus 5.5 207e3ce034 fix(renderer): make the renderer test suite pass on the shared CI workflow
QRun-IO/ci-policy: ci / fork-guard (pull_request) Skipped
QRun-IO/ci-policy: native / fork-deny (pull_request) Skipped
QRun-IO/ci-policy: native / dotnet-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / Scoped workflow [workflow 539bec410f7926fec3ef7fabe51934a91e0248be] (pull_request) Successful in 7s
QRun-IO/ci-policy: native / native-result (pull_request) Successful in 6s
QRun-IO/ci-policy: ci / head-profile (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy (pull_request) Skipped
QRun-IO/ci-policy: ci / application-template-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-core-2-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7-head (pull_request) Skipped
QRun-IO/ci-policy: ci / node-8 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-10 (pull_request) Skipped
QRun-IO/ci-policy: ci / android-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / original-business (pull_request) Skipped
QRun-IO/ci-policy: ci / original-mcp (pull_request) Skipped
QRun-IO/ci-policy: ci / original-voyage (pull_request) Skipped
QRun-IO/ci-policy: ci / original-cli (pull_request) Skipped
QRun-IO/ci-policy: ci / not-enrolled (pull_request) Skipped
QRun-IO/ci-policy: ci / shared-pipeline (pull_request) Skipped
QRun-IO/ci-policy: ci / ci-result (pull_request) Successful in 6s
build / maven (push) Failing after 10m0s
QRun-IO/ci-policy: ci / gradle-1 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-5 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-9 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-playwright-11 (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-13 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-14 (pull_request) Skipped
maven / build (push) Failing after 9m59s
- BackgroundImageQualification expected the patched OpenHTML provider at the
  old fixed version 1.1.105-qqq-p1, but PR #1 publishes the vendored module at
  the family revision. The exact-GAV check now takes the expected version from
  the build's own ${revision} (surefire system property) and fails closed if
  absent; group, artifact, single-class-definition and companion checks are
  unchanged.
- barHeight was advertised and accepted for every Okapi family but silently
  ignored by fixed-geometry encoders. It is now advertised/accepted only where
  it changes the symbol; the 16 fixed-geometry families and gs1-databar in
  STACKED mode refuse it with UNSUPPORTED_BARCODE_OPTION (catalog metadata
  carries notWithMode=STACKED).
- UNSUPPORTED_BARCODE_OPTION now crosses the worker boundary as its closed
  code (foundation already models it), and PDF VALIDATE checks literal
  (template-free) barcode options of in-process encoders.

Resolves the 3 known BarcodeDeclaredCapabilityTest failures and the
PDF_PROVIDER_GAV_MISMATCH error; no tests skipped or weakened.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 11:37:12 -06:00
james.maesandClaude Opus 5.5 063b1e8136 ci: pin shared workflow to v1
QRun-IO/ci-policy: ci / fork-guard (pull_request) Skipped
QRun-IO/ci-policy: native / fork-deny (pull_request) Skipped
QRun-IO/ci-policy: native / dotnet-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / Scoped workflow [workflow 1e25bb5a243dfdcd95d4b9a4f780200ff8c8778a] (pull_request) Successful in 13s
QRun-IO/ci-policy: ci / head-profile (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy (pull_request) Skipped
QRun-IO/ci-policy: native / native-result (pull_request) Successful in 13s
QRun-IO/ci-policy: ci / application-template-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-core-2-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7-head (pull_request) Skipped
QRun-IO/ci-policy: ci / node-8 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-10 (pull_request) Skipped
QRun-IO/ci-policy: ci / android-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / original-business (pull_request) Skipped
QRun-IO/ci-policy: ci / original-mcp (pull_request) Skipped
QRun-IO/ci-policy: ci / original-voyage (pull_request) Skipped
QRun-IO/ci-policy: ci / original-cli (pull_request) Skipped
QRun-IO/ci-policy: ci / shared-pipeline (pull_request) Skipped
QRun-IO/ci-policy: ci / not-enrolled (pull_request) Failing after 10s
QRun-IO/ci-policy: ci / ci-result (pull_request) Failing after 10s
build / maven (push) Failing after 10m5s
QRun-IO/ci-policy: ci / gradle-1 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-5 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-9 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-playwright-11 (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-13 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-14 (pull_request) Skipped
maven / build (push) Failing after 10m5s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 15:55:32 +00:00
25 changed files with 1448 additions and 56 deletions

No files matched your search

+1 -1
View File
@@ -3,5 +3,5 @@ on:
push:
jobs:
maven:
uses: self:KofTwentyTwo/ci-workflows/.gitea/workflows/maven.yaml@main
uses: self:KofTwentyTwo/ci-workflows/.gitea/workflows/maven.yaml@v1
secrets: inherit
+1 -1
View File
@@ -1,6 +1,6 @@
# QBit Document Generation
> **Paused, unreleased work in progress — 2026-11-04.** Implementation has stopped apart from the scoped definition-rename addition of 2026-11-06 (see [status](docs/RELEASE-STATUS.md)). This is a source checkpoint, not a production-ready release or a complete installable package set.
> **Version 1.0.** Published to the KOF22 Gitea Maven registry (`com.kingsrook.qbits`). Requires QQQ Core `4.1.0-RC.22`; the bridges need Record Files `0.1.0-RC.1` and Scripting `1.0.0`. See [status](docs/RELEASE-STATUS.md).
Start with [current status and build prerequisites](docs/RELEASE-STATUS.md) and [known issues](docs/KNOWN-ISSUES.md). These documents supersede older release-readiness statements below.
+2 -2
View File
@@ -4,9 +4,9 @@
<parent><groupId>com.kingsrook.qbits</groupId><artifactId>qbit-document-generation-reactor</artifactId><version>${revision}</version><relativePath>../../pom.xml</relativePath></parent>
<groupId>com.kingsrook.qbits</groupId><artifactId>qbit-document-generation-printing</artifactId><version>${revision}</version>
<name>Document Generation Printing Bridge</name>
<properties><qqq.version>4.1.0-RC.21</qqq.version><!-- Owner-supplied sibling QBit coordinates (not yet published): set to the released version --><qbit-record-files.version>0.1.0-SNAPSHOT</qbit-record-files.version><maven.compiler.release>21</maven.compiler.release><maven.compiler.proc>none</maven.compiler.proc><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<properties><qqq.version>4.1.0-RC.26</qqq.version><!-- Released sibling QBit coordinates from the Gitea Maven registry --><qbit-record-files.version>0.4.0-RC.1</qbit-record-files.version><maven.compiler.release>21</maven.compiler.release><maven.compiler.proc>none</maven.compiler.proc><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<!-- Released Printing 1.0.0: RetainedPrintFacade.Acceptance carries title/optionsJson and PrintingService enforces options.mediaProfile id+revision. -->
<qbit-printing.version>1.0.0</qbit-printing.version><enforcer.skip>true</enforcer.skip></properties>
<qbit-printing.version>1.2.0-RC.1</qbit-printing.version><enforcer.skip>true</enforcer.skip></properties>
<dependencyManagement><dependencies><dependency><groupId>com.kingsrook.qqq</groupId><artifactId>qqq-bom-pom</artifactId><version>${qqq.version}</version><type>pom</type><scope>import</scope></dependency>
<dependency><groupId>com.kingsrook.qqq</groupId><artifactId>qqq-backend-core</artifactId><version>${qqq.version}</version></dependency>
<dependency><groupId>com.kingsrook.qqq</groupId><artifactId>qqq-backend-module-rdbms</artifactId><version>${qqq.version}</version></dependency>
+1 -1
View File
@@ -4,7 +4,7 @@
<parent><groupId>com.kingsrook.qbits</groupId><artifactId>qbit-document-generation-reactor</artifactId><version>${revision}</version><relativePath>../../pom.xml</relativePath></parent>
<groupId>com.kingsrook.qbits</groupId><artifactId>qbit-document-generation-record-files</artifactId><version>${revision}</version>
<name>Document Generation Record Files Bridge</name>
<properties><qqq.version>4.1.0-RC.21</qqq.version><!-- Owner-supplied sibling QBit coordinates (not yet published): set to the released version --><qbit-record-files.version>0.1.0-SNAPSHOT</qbit-record-files.version><maven.compiler.release>21</maven.compiler.release><maven.compiler.proc>none</maven.compiler.proc><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><enforcer.skip>true</enforcer.skip></properties>
<properties><qqq.version>4.1.0-RC.26</qqq.version><!-- Released sibling QBit coordinates from the Gitea Maven registry --><qbit-record-files.version>0.4.0-RC.1</qbit-record-files.version><maven.compiler.release>21</maven.compiler.release><maven.compiler.proc>none</maven.compiler.proc><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><enforcer.skip>true</enforcer.skip></properties>
<dependencyManagement><dependencies><dependency><groupId>com.kingsrook.qqq</groupId><artifactId>qqq-bom-pom</artifactId><version>${qqq.version}</version><type>pom</type><scope>import</scope></dependency>
<dependency><groupId>com.kingsrook.qqq</groupId><artifactId>qqq-backend-core</artifactId><version>${qqq.version}</version></dependency>
<dependency><groupId>com.kingsrook.qqq</groupId><artifactId>qqq-backend-module-rdbms</artifactId><version>${qqq.version}</version></dependency>
+17 -4
View File
@@ -1,9 +1,22 @@
# Build only on an operator-approved, immutable existing renderer image.
# This file does not build/download the engine or confer isolation qualification.
# Build only on an operator-approved, immutable existing renderer image (RENDERER_IMAGE, the engine image from
# renderer/Dockerfile). This file does not build the engine or confer isolation qualification.
# The service is the RELEASED transport artifact, resolved at the exact TRANSPORT_VERSION from the QRun-IO Gitea Maven
# registry and Maven Central (no SNAPSHOT, no local build, no default). BUILD_IMAGE must be an immutable digest.
# docker build --build-arg BUILD_IMAGE=... --build-arg RENDERER_IMAGE=... --build-arg TRANSPORT_VERSION=1.0.0 bridges/remote-renderer/
ARG BUILD_IMAGE
ARG RENDERER_IMAGE
FROM ${BUILD_IMAGE} AS release
ARG TRANSPORT_VERSION
WORKDIR /release
COPY release-image/pom.xml ./pom.xml
RUN test -n "${TRANSPORT_VERSION}" && case "${TRANSPORT_VERSION}" in *SNAPSHOT*|*forge*) echo "TRANSPORT_VERSION must be a released version" >&2; exit 1;; esac \
&& mvn -B -Dtransport.version="${TRANSPORT_VERSION}" -Dmaven.repo.local=/release/.m2 package \
&& mv "target/lib/qqq-remote-renderer-transport-${TRANSPORT_VERSION}.jar" target/service.jar \
&& test -f target/service.jar && ls target/lib
FROM ${RENDERER_IMAGE}
COPY transport/target/qqq-remote-renderer-transport-0.1.0-SNAPSHOT.jar /remote/service.jar
COPY transport/target/lib/ /remote/lib/
COPY --from=release /release/target/service.jar /remote/service.jar
COPY --from=release /release/target/lib/ /remote/lib/
COPY LICENSE NOTICE /usr/share/doc/qqq-remote-renderer/
USER 65532:65532
EXPOSE 18445
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Image-assembly project, not a published module. It resolves the RELEASED remote renderer transport service and its runtime
libraries from the QRun-IO Gitea Maven registry (and Maven Central) at one exact version, so the service image
carries release bytes and not a local build. transport.version has no default: pass -Dtransport.version=<released>. -->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.kingsrook.qbits</groupId><artifactId>qqq-remote-renderer-image</artifactId><version>0</version><packaging>pom</packaging>
<repositories>
<repository><id>central</id><url>https://repo.maven.apache.org/maven2</url><snapshots><enabled>false</enabled></snapshots></repository>
<repository><id>qrun-io</id><url>https://git.kof22.com/api/packages/QRun-IO/maven</url><snapshots><enabled>false</enabled></snapshots><releases><checksumPolicy>fail</checksumPolicy></releases></repository>
</repositories>
<dependencies>
<dependency><groupId>com.kingsrook.qbits</groupId><artifactId>qqq-remote-renderer-transport</artifactId><version>${transport.version}</version></dependency>
</dependencies>
<build><plugins>
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-dependency-plugin</artifactId><version>3.9.0</version>
<executions><execution><id>release-libraries</id><phase>package</phase><goals><goal>copy-dependencies</goal></goals>
<configuration><includeScope>runtime</includeScope><outputDirectory>${project.build.directory}/lib</outputDirectory></configuration>
</execution></executions></plugin>
</plugins></build>
</project>
+1 -1
View File
@@ -5,7 +5,7 @@
<groupId>com.kingsrook.qbits</groupId><artifactId>qbit-document-generation-scripting</artifactId><version>${revision}</version>
<name>Document Generation Scripting Bridge</name>
<licenses><license><name>GNU Affero General Public License v3.0</name><url>https://www.gnu.org/licenses/agpl-3.0-standalone.html</url><distribution>repo</distribution></license></licenses>
<properties><qqq.version>4.1.0-RC.21</qqq.version><!-- Owner-supplied sibling QBit coordinates (not yet published): set to the released version --><qbit-scripting.version>0.1.0-SNAPSHOT</qbit-scripting.version><maven.compiler.release>21</maven.compiler.release><maven.compiler.proc>none</maven.compiler.proc><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><enforcer.skip>true</enforcer.skip></properties>
<properties><qqq.version>4.1.0-RC.26</qqq.version><!-- Released sibling QBit coordinates from the Gitea Maven registry --><qbit-scripting.version>1.1.0-RC.3</qbit-scripting.version><maven.compiler.release>21</maven.compiler.release><maven.compiler.proc>none</maven.compiler.proc><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><enforcer.skip>true</enforcer.skip></properties>
<dependencyManagement><dependencies>
<dependency><groupId>com.kingsrook.qqq</groupId><artifactId>qqq-backend-core</artifactId><version>${qqq.version}</version></dependency>
<dependency><groupId>com.kingsrook.qqq</groupId><artifactId>qqq-backend-module-rdbms</artifactId><version>${qqq.version}</version></dependency>
+1 -1
View File
@@ -26,7 +26,7 @@ Submitted records provide only IDs. The source body is always fetched afresh thr
## Database/deployment constraints
The durable queue supports file-backed H2 with configured and actual WRITE_DELAY=0, or native MySQL 8.4 under the live checks in [MYSQL.md](MYSQL.md), or native PostgreSQL 17 under the equivalent live checks in `GenerationPostgres`. Both require fresh independent writable connections and the owned tables on one backend. Ambient QQQ transactions are refused. Native mapping drift and unqualified physical targets are denied. Owned statements have five-second JDBC deadlines; MySQL row/metadata lock waits must each be 1–5 seconds. H2 tests pin 2.5.252 and close all connection providers before a cold reopen. Actual pinned MySQL 8.4.13 tests cover concurrent claims/edits, physical pooled isolation restoration, weakened durability/settings, index/engine/mapping drift, bounded lock contention and server restart. PostgreSQL/Aurora are not implicitly supported. Schema changes are operator migrations; metadata registration does not execute DDL. Host power-loss and volume/backup restoration remain deployment acceptance work.
The durable queue supports file-backed H2 with configured and actual WRITE_DELAY=0, or native MySQL 8.4 under the live checks in [MYSQL.md](MYSQL.md), or native PostgreSQL 17 under the equivalent live checks in `GenerationPostgres`. Both require fresh independent writable connections and the owned tables on one backend. Ambient QQQ transactions are refused. Native mapping drift and unqualified physical targets are denied. Owned statements have five-second JDBC deadlines; MySQL row/metadata lock waits must each be 1–5 seconds. H2 tests pin 2.5.252 and close all connection providers before a cold reopen. Actual pinned MySQL 8.4.13 tests cover concurrent claims/edits, physical pooled isolation restoration, weakened durability/settings, index/engine/mapping drift, bounded lock contention and server restart. Amazon Aurora PostgreSQL 17 passes through the same PostgreSQL checks with one difference: Aurora always runs with `full_page_writes` off and has no parameter to change it, because its storage layer does not write torn pages, so `GenerationPostgres` accepts that value when the server resolves `aurora_version()` (`to_regprocedure('aurora_version()') IS NOT NULL`, evaluated in the settings query). Stock PostgreSQL must still report `full_page_writes` on, and `fsync`, `synchronous_commit` and every other check are unchanged on Aurora. The test for this rule runs on a real `postgres:17` engine with a simulated `aurora_version()` function; no test in this repository runs on Aurora itself. Aurora MySQL is not qualified (see [MYSQL.md](MYSQL.md)). Schema changes are operator migrations; metadata registration does not execute DDL. Host power-loss and volume/backup restoration remain deployment acceptance work.
No queue/render/file/script code is executed through a trusted host JavaScript VM. The renderer and scripting integrations continue to require qualified Linux/runsc; this workstation has no runsc runtime, so hostile execution remains unavailable. The queue does not weaken those gates.
+4 -8
View File
@@ -1,10 +1,6 @@
# Known issues and unfinished work
# Known issues
Updated: 2026-11-04. These are unresolved items, not a plan currently being executed.
- Renderer verification has **3 failing tests out of 174**: `BarcodeDeclaredCapabilityTest.barHeightIsAdvertisedAcceptedAndEffectiveTogether`, `gs1DatabarBarHeightDependsOnMode`, and `unsupportedOptionIsAClosedWorkerCode`. The imported main implementation and capability tests disagree; do not hide this by skipping tests or advertising unsupported options.
- The Record Files and Scripting bridge dependencies, including their `tests` classifier artifacts, are not available as a completed public release cohort. The complete root reactor is not release-qualified.
- Earlier foundation runs hit local socket exhaustion/PostgreSQL connection resets. The final 225-test run passed; the transport failures remain a reproducibility concern.
- Renderer image deployment, hostile-template containment, production capacity and physical barcode/printing acceptance are not established by the Maven tests. Source-first templates are implemented; a visual designer is outside this version.
- Renderer image deployment, hostile-template containment, production capacity and physical barcode and printing acceptance are not established by the Maven tests. Source-first templates are implemented; a visual designer is outside this version.
- Barcode `barHeight` is accepted only where it changes the output. The sixteen fixed-geometry encoders (matrix symbols and postal four-state codes) neither advertise nor accept it and refuse it with `UNSUPPORTED_BARCODE_OPTION`, as does GS1 DataBar in stacked mode.
- Native Zint and patched OpenHTML retain their own licenses and corresponding source. The checked-in archives and hash files must ship with the relevant distribution.
- Some historical design/verification notes describe earlier private builds. This status and this issue list supersede their release claims.
- Some historical design and verification notes describe earlier private builds; this status and this issue list supersede their release claims.
+2 -4
View File
@@ -1,6 +1,6 @@
# Document Generation — current status
Updated: 2026-11-06. **Unreleased work in progress; not a production 1.0 release.** The 2026-11-04 stop was lifted only for the definition-rename change below; no other implementation is in progress.
**Version 1.0**, built and published by the shared `ci-workflows` Maven pipeline to the KOF22 Gitea Maven registry (`com.kingsrook.qbits`). `mvn deploy` runs every module's tests once. Depends on QQQ Core `4.1.0-RC.22`, Record Files `0.1.0-RC.1` and Scripting `1.0.0`.
## Purpose
@@ -20,6 +20,4 @@ Foundation: 225 tests passed with zero failures/errors/skips, Checkstyle clean a
## Source and release state
The starting source was extracted from Website-Backend commit b6fb23825a87412397dec703b9617148ae7eb601, then adapted for standalone packaging. This repository preserves the resulting source and existing licenses. It does not declare a published artifact cohort, completed deployment or complete acceptance.
See [known issues](KNOWN-ISSUES.md) and the README for the existing API and integration documentation. No further implementation, tests, releases or deployment should be started without explicit user instruction; the definition rename was added under such an instruction so a published release can replace GG Forge's local `0.1.0-forge.ee782b7d6ea8-rename1` pin. The outstanding dependency order is Core, Collaboration, Scripting/Record Files, then the integrated Generation bridges and matching UI.
The starting source was extracted from Website-Backend commit b6fb23825a87412397dec703b9617148ae7eb601, then adapted for standalone packaging; existing licenses are preserved. See [known issues](KNOWN-ISSUES.md) and the README for the API and integration documentation.
+973
View File
@@ -0,0 +1,973 @@
# Design: great labels, barcodes, page layout and preview
Status: draft for review, 2026-11-11. No code in this change. Owner decisions are in section 12.
This document designs four things the document module lacks and the preview James asked for, then gives a work
plan that separate agents can build from. It covers `qbit-document-generation` (foundation, renderer and bridges)
and says what the neighbouring repositories and GG Forge must do.
1. Layouts in millimetres, inches or points, so one design serves 203, 300 and 600 dpi.
2. Alignment and fit rules for text and barcodes, so a long value shrinks or wraps by a stated rule.
3. A preview inside the QBit that shows the exact bitmap before anything prints.
4. Fonts that work in every environment: a bundled default set, plus uploads that need no object storage.
## 0. What exists today
Every statement here was read in code on 2026-11-11. Paths are relative to this repository unless a repository is
named. "Not verified" marks what was not confirmed. Facts about other repositories (Forge, printing, the print
agent, the scanner QBit, Record Files, the frontend and QQQ core) come from read-only surveys of those repositories
made for this task, read at the versions Forge pins where that matters.
| Area | Today | Where |
|---|---|---|
| Label scene | `qqq-zpl-scene/1`, a JSON list of `text`, `rasterText`, `box`, `image`, `barcode`. Every coordinate and size is an integer printer dot. Stock dpi must be 203, 300 or 600. | `renderer/.../ZplRenderer.java` |
| Printer font text | `text` emits `^A0N` (printer font 0). The renderer cannot measure that font, so it refuses when `x + characters * width` passes the label width. | `ZplRenderer.java` `render`, case `text` |
| TrueType text | `rasterText` draws one supplied TrueType face into a dot box. It already wraps and already has `align` left, center, right. It never shrinks. Overflow is `RASTER_TEXT_DOES_NOT_FIT`. | `renderer/.../RasterText.java` |
| Barcode | 71 catalogue ids. A `fit` box in microns picks the largest whole-dot module for 13 symbologies; the raster sits at the box origin (no centring). Linear bar height scales with the module. No human-readable line. | `BarcodeFit.java`, `Barcodes.java`, `renderer/BARCODE-FIT.md` |
| Fonts | None bundled in the renderer. A font is a template asset whose bytes the host must supply on every render. | `RenderInput.java`, `bridges/renderer/.../IsolatedRendererBridge.java` `resolve` |
| Asset bytes | The foundation stores an asset row only (name, file version id, hash). The only shipped reader is Record Files, whose upload path is a Collaboration attachment and needs a storage bucket. | `GenerationTables.java`, `bridges/record-files/...`, `qbit-record-files` |
| Refusals | Apart from 13 barcode fit codes, every renderer failure is the single code `RENDER_FAILED`, by design, so that no record data leaks. | `RendererMain.java` `errorCode` |
| Output check | The host re-parses the ZPL and allows only `^A0N` text, `^GB` and uncompressed `^GFA`. | `bridges/renderer/.../ZplOutputPolicy.java` |
| Preview | "Preview draft" generates a retained, audited file from a real record. A PDF opens in the shared viewer. A ZPL file cannot be shown as an image. | `GenerationAuthoringProcesses.java`, `DocumentGenerationService.preview`, `qqq-frontend-next` `attachment-download.ts` |
| Stock and dpi | A template has one stock; the stock carries the dpi. A revision copies both. | `GenerationTables.java` |
| Page documents | FreeMarker HTML with paged CSS 2.1 through a patched OpenHTML to PDF; TrueType assets; one starter (packing slip). | `PdfRenderer.java`, `renderer/README.md` |
| Coverage gate | JaCoCo 85% instructions and 90% classes in `foundation` only. `renderer` and the bridges have no gate. | module `pom.xml` files |
| Release | Shared Gitea workflow: `develop` publishes a SNAPSHOT, `release/X.Y` publishes `X.Y.0-RC.N` and tags it. `develop` is `1.4.0-SNAPSHOT`. Renderer images are built by hand from a released version. | `.gitea/workflows/build.yaml`, `renderer/IMAGE-PUBLISH.md` |
How GG Forge uses it today (read from Forge `origin/develop`):
- Forge pins the document generation family at `1.1.0-RC.2` and serves `qqq-frontend-next` `1.0.0-RC.41`.
- Forge renders through `RemoteRendererBridge`, so `ZplOutputPolicy` applies to every label.
- `EchoTemplatePublisher` computes all geometry by hand (microns to dots, insets, a module search) and Forge seeds
ten documents: the same designs repeated for 4 x 1 in at 203 and 300 dpi and 3 x 2 in at 203 dpi.
- `EchoPrintingDestinations.resolve` offers a document only when the revision's stock width, height and dpi equal
the media profile the print agent reports from the Windows queue default. When nothing matches, the document
list is empty and no reason is given. This filter is Forge code, not scanner or printing QBit code.
- `EchoBundledFonts` answers one font from Forge's jar because no environment could hold an uploaded font, and a
Liquibase changeset cannot put bytes in a bucket.
The printing QBit's built-in test label (`qbit-printing` `PrintingTestDocuments`) is the quality bar: it is drawn in
points on a Java canvas, scaled by `dpi / 72`, with a bundled bold TrueType face, and sent as whole-label `^GFA`
strips plus a native `^BC` barcode.
## 1. Goals and non-goals
### What a person designing a label experiences
- They write sizes in millimetres, inches or points. They never compute dots.
- They say "centre this in that box" for any element, and it is centred for every value and every dpi.
- They pick a font by name from a small set that is always there, or upload their own once.
- They state what happens to a long value: shrink to a floor, wrap to a number of lines, cut with a visible mark,
or refuse. The default never changes data silently.
- They open the template and see the label. They change the layout and see it again in seconds, at 203, 300 or
600 dpi, with sample values or a real record, without printing.
- Beside the picture they read plain warnings: text was shrunk, a barcode is at its smallest safe module, a field
would not fit its longest allowed value.
- Publishing proves the layout fits the longest value each field declares, so a published label is not refused
for length at print time.
### What a person printing a label experiences
- The label prints at the printer's own resolution. Nobody creates a 203 dpi copy and a 300 dpi copy.
- When a label cannot print, the message names the field, what it needed and what it had.
- When a document is not offered for a printer, the screen says why (the stock size differs) instead of showing
an empty list.
### Non-goals in this round
- A drag-and-drop visual designer. The editor stays source first; the preview makes that workable.
- Raw ZPL templates or any printer command escape hatch.
- Printer languages other than ZPL (EPL, TSPL), colour, RFID.
- Changing how existing `qqq-zpl-scene/1` documents print.
- GS1 conformance grading or certification. The design gives rules and tests, not a verifier.
## 2. Layout model
### 2.1 Scene version 2
A new schema id, `qqq-zpl-scene/2`, lives in the same `zplSource` field. The renderer chooses the code path from
the `schema` value alone.
```json
{
"schema": "qqq-zpl-scene/2",
"design": { "width": "4in", "height": "1in" },
"defaults": { "font": "qqq-sans-bold", "size": "9pt" },
"samples": {
"typical": { "record": { "humanReadable": "(01)00012345678905(10)L240611A", "rendererValue": "[01]00012345678905[10]L240611A" } },
"longest": { "record": { "humanReadable": "(01)00012345678905(10)L240611A-RETURNED-000123", "rendererValue": "[01]00012345678905[10]L240611A-RETURNED-000123" } }
},
"elements": [
{ "id": "code", "type": "barcode", "symbology": "gs1-128", "valuePath": "/record/rendererValue",
"anchor": "top-center", "y": "0.5mm", "width": "94mm", "height": "11mm",
"align": "center", "valign": "middle", "minModule": "0.25mm", "quiet": 10 },
{ "id": "caption", "type": "text", "valuePath": "/record/humanReadable",
"anchor": "bottom-center", "y": "0.5mm", "width": "94mm", "height": "6.5mm",
"align": "center", "valign": "middle", "size": "11pt", "maxLength": 48,
"fit": { "maxLines": 2, "shrinkTo": "7pt", "overflow": "refuse" } }
],
"overrides": [
{ "when": { "dpi": [203] }, "set": { "caption": { "threshold": 144 } } }
]
}
```
The example assumes 3 mm margins on 4 x 1 in stock, a printable area of 95.6 x 19.4 mm.
### 2.2 Units
Every length is a string made of a number and a unit: `"12.5mm"`, `"0.5in"`, `"9pt"`, `"25%"`, `"1dot"`.
| Unit | Meaning | Use |
|---|---|---|
| `mm`, `in` | Physical length | Positions and sizes |
| `pt` | 1/72 inch | Font sizes, line thickness |
| `%` | Share of the container's width (for `x`, `width`) or height (for `y`, `height`) | Layouts that adapt to the stock |
| `dot` | One printer dot | Only where a device unit is meant (a hairline). Outside an override it raises the warning `DEVICE_UNIT_USED`. |
One rule, no ambient unit setting, so a value read out of context is never ambiguous. Up to three decimals are
accepted. Lengths convert to integer micrometres first (the unit the stock already uses), then to dots:
| Quantity | Rounding | Reason |
|---|---|---|
| Position | Nearest dot, the existing `Stock.dots` rule | Symmetric error |
| Box width and height | Down, the existing `BarcodeFit.dotsInside` rule | Content never leaves its designed box |
| Line and box thickness | Nearest, never less than 1 dot | A thin rule must not vanish |
| Barcode module | Up to a whole dot from the stated minimum (section 4) | Bars must be whole dots |
| Font size | Not rounded; the face is scaled by `points * dpi / 72` | Type scales continuously |
### 2.3 Stock, printable area and render target
- Coordinates start at the top left of the printable area: the stock less its margins. A design therefore never
prints in the margin.
- `design.width` and `design.height` state the stock the layout was drawn for. At render time the stock must match
within 0.5 mm on each side, otherwise the refusal `STOCK_MISMATCH` reports both sizes. (Exact micrometre
equality, which Forge's filter uses today, fails on driver rounding such as 101.5 against 101.6 mm.)
- Resolution stops being a property of the document. A version 2 revision renders at a **render target** dpi
supplied by the caller: the printer's reported resolution when printing, the chosen value when previewing. The
stock row's `dpi` remains and becomes the default target and the only target for version 1 scenes.
- `DocumentGenerationService.Request` gains an optional `targetDpi` (203, 300 or 600). It is recorded on the
request row in a new nullable column and framed into the intent digest when present, following the pattern used
for the media profile (`docs/EXPECTED-PRINT-INTENT.md`). The renderer refuses a target that differs from the
stock's dpi for a version 1 scene (`TARGET_REQUIRES_SCENE_2`), because only the renderer reads the scene.
- Later round: `design.stock: "atLeast"` lets one design print on a larger stock, with anchors and percentages
resolving against the actual printable area. The first round supports `exact` only.
### 2.4 Anchors and alignment
Two separate ideas, both available on every element.
**Where the box sits (`anchor`, `x`, `y`, `width`, `height`).** `anchor` names one of nine points:
`top-left` (default), `top-center`, `top-right`, `middle-left`, `center`, `middle-right`, `bottom-left`,
`bottom-center`, `bottom-right`. The same point of the element's box is placed on that point of its container.
`x` and `y` are then distances inward from the anchored edge; on a centred axis they shift right or down and may be
negative. An omitted `width` or `height` means the rest of the container on that axis.
```
container (printable area or group)
+--------------------------------------------------+
| anchor: top-center, y: 1mm |
| +------------------------------+ |
| | box (width x height) | |
| | content placed by | |
| | align / valign | |
| +------------------------------+ |
| |
| anchor: bottom-left anchor: bottom-right
| x: 2mm, y: 1mm x: 2mm, y: 1mm |
| +--------+ +--------+ |
+--------------------------------------------------+
```
**Where the content sits in the box (`align`, `valign`).** `align` is `left`, `center` or `right`; `valign` is `top`,
`middle` or `bottom`. They apply to text lines, to a fitted barcode whose size depends on its data, and to a scaled
image. Slack is split with integer division, so a centred element is off by at most one dot.
`rotation` is 0, 90, 180 or 270 degrees clockwise for text, barcodes and images. The box is given as it appears on
the label, after rotation.
### 2.5 Containers
`group` is in: a box with its own `anchor`, position and size, whose children are positioned relative to it. It
lets a block move as a unit and gives percentages something smaller than the label to refer to. Groups nest to a
depth of four.
Rows and columns that distribute children automatically (`stack`, tables) are out of this round. No label in hand
needs them, and they can be added to version 2 later without breaking a scene that does not use them.
### 2.6 One design, several resolutions
A scene has no dots in it, so the same revision renders at any target. Small physical differences between
resolutions are expected (a 0.25 mm module is 2 dots at 203 dpi and 3 dots, 0.254 mm, at 300 dpi) and the
diagnostics report the actual values.
`overrides` is the only place a scene may depend on resolution. An entry has `when` (`dpi`: a list) and `set` (a
map from element id to replacement properties; `id` and `type` cannot change). Legitimate uses:
- a heavier face or a different `threshold` for small type at 203 dpi;
- a larger minimum type size at 203 dpi;
- a logo asset prepared for one resolution;
- a `dot` thickness for a hairline.
An override is not for repositioning the layout per printer. If a design needs that, it is two designs.
### 2.7 Versions and upgrade
- **Version 1 is frozen.** A `qqq-zpl-scene/1` source keeps its existing code path and its exact output bytes. A
test corpus pins this (section 10). No new field is added to version 1, including `align` on a barcode, because
published scenes already carry that word and would start printing differently.
- **Version 2 is a separate package** in the renderer. The two share only the barcode encoders and the font
rasteriser.
- **Upgrade is an explicit act by an author, never automatic.** A new authoring action, "Convert layout to physical
units", asks the renderer for a version 2 rendering of the draft's version 1 scene (new operation
`UPGRADE_SCENE`, a pure function that returns the new source and a list of notes). It converts dots to
millimetres with the template stock's dpi, turns `rasterText` into `text` with the same asset, turns a fitted
barcode into a barcode box, and turns printer font `text` into `text` in the default bundled face with a note
that the face changed. The action shows the two previews side by side and saves the result as the draft only.
Publishing stays a separate human step. Nothing rewrites a stored revision.
- A renderer that does not know version 2 answers a version 2 scene with a refusal, and the host reports
`RENDERER_TOO_OLD` (section 11.3).
## 3. Text
### 3.1 Fonts
Version 2 has one kind of text: a TrueType face rasterised by the renderer. Printer font 0 is not offered, because
the renderer cannot measure it or draw it, so it can be neither fitted nor previewed truthfully. Version 1 keeps
its `text` element.
| Source | How it is named in a scene | Where the bytes live | Works without object storage |
|---|---|---|---|
| Bundled default set | `qqq-sans`, `qqq-sans-bold`, `qqq-condensed-bold`, `qqq-mono`, `qqq-mono-bold` | Inside the renderer jar and image, with licence files and hashes | Yes |
| Built-in asset store | The asset's name in the template manifest | A new table owned by this QBit, bytes in the database (section 3.2) | Yes |
| Record Files (as today) | The asset's name in the template manifest | Collaboration attachment storage | No |
The prefix `qqq-` is reserved: a version 2 scene whose manifest has an asset with that prefix is refused with
`RESERVED_FONT_NAME`.
**The bundled set.** Five faces: a proportional sans in regular and bold, a condensed bold for long values on
narrow labels, and a monospace in regular and bold for codes and human-readable lines. Selection criteria: real
bold and condensed cuts (stroke weight comes from the face, not from a threshold; Forge's Abel needed
`threshold` 220 to print dark enough), clear `0/O` and `1/l/I`, Latin Extended coverage, static TrueType outlines
(the renderer accepts only `font/ttf`), a licence that permits bundling. Proposed: IBM Plex Sans (Regular, Bold),
IBM Plex Sans Condensed (Bold) and JetBrains Mono (Regular, Bold). JetBrains Mono Bold already ships in
`qbit-printing` and its OFL text was read there; the IBM Plex licence was not verified in this task and must be
confirmed from the licence file of the exact release taken. Each face ships with its licence text, a provenance
note and a SHA-256, and a test fails the build if any of the three is missing. Licence handling is decision 1 in
section 12.
The same faces are also registered as CSS families for page documents (section 7).
### 3.2 Uploaded fonts in every environment
Today an uploaded font needs Record Files, Collaboration attachments and a bucket, and it cannot be seeded by a
database migration. The design adds a store that needs only the database the QBit already has.
- **Table** `document_generation_asset_content`: `sha256` (unique), `media_type`, `length`, `bytes`, `file_name`,
`rights_statement`, `created_by`, `create_date`. Limit 2 MiB per asset, the renderer's existing bound. Shipped as
migration scripts for PostgreSQL, MySQL and H2 beside the existing ones; the host applies them (Forge writes its
own changeset).
- **Reference.** An asset row points at it with `fileVersionId = "dg:<sha256>"`. The bridge resolves that prefix
from the built-in store itself and passes every other id to the host's reader as today. No host code is needed.
- **Upload.** A new authoring action, "Upload font or image", takes a file through the existing `FILE_UPLOAD` field
adornment, a name, and a required rights statement. Permission: `document-generation.configure`.
- **Checks.** The host checks size and leading bytes only. The file is then sent to the isolated renderer
(new operation `INSPECT_ASSET`), which parses it and returns family names, glyph count and the embedding
permission bits. The application JVM never parses a font or an image. A font whose embedding bits forbid
embedding is refused.
- **Seeding.** Because the bytes are a database row, a host can seed a logo or a brand font with a migration, which
is how Forge installs its setup data.
- **When Record Files is absent** nothing is lost: selection lists the built-in store and the bundled faces. When
it is present its released versions appear in the same list, as today.
This moves a boundary: the README says the QBit "does not own retained file bytes". That stays true for generated
output. Template assets are definition data, like the scene source. This is decision 2 in section 12.
### 3.3 Size, fit and legibility
`size` is the em size as a length (`"9pt"`, `"3mm"`). `lineHeight` is a multiple of the size (default from the face
metrics).
`fit` states what happens when the value does not fit the box:
| Property | Meaning | Default |
|---|---|---|
| `maxLines` | Wrap to at most this many lines | 1 |
| `shrinkTo` | Smallest size the text may be reduced to | 70% of `size`, not below 5 pt |
| `overflow` | `refuse` or `truncate`, applied when it still does not fit at the floor | `refuse` |
The order is fixed and stated: lay out at the nominal size within `maxLines`; if it does not fit, find the largest
size down to the floor (0.25 pt steps) that fits within `maxLines`; if none fits, apply `overflow`. `truncate` cuts
at a character boundary, appends an ellipsis and raises the warning `TEXT_TRUNCATED`. It is opt-in per element
because a shortened identifier on a warehouse label can be misread; the default keeps the label from printing
something different from the data.
`maxLength` (characters) declares the longest value a bound element must hold. Validation at publish lays out a
worst-case string of that length (the widest glyph of the face, repeated) and refuses publication if it does not
fit. At print time a value longer than `maxLength` is refused with `VALUE_TOO_LONG` before layout. Together these
make a length refusal a design-time event. An element without `maxLength` raises the warning `FIT_NOT_PROVEN` at
preview and publish.
Legibility on thermal heads:
- Glyphs are drawn antialiased and reduced to black and white at `threshold` (1 to 254, default 128). A higher
value prints heavier. The default is not changed per resolution; an override may.
- The renderer warns `TEXT_SMALL_FOR_DPI` when the capital height at the target is under 12 dots (about 6 pt at
203 dpi), and `TEXT_LIGHT_FACE` when a regular-weight face is used with a capital height under 20 dots (about
10 pt at 203 dpi). The numbers are starting values to be confirmed on the specimen label (section 10.6).
- Guidance for the authoring guide: at 203 dpi use a bold face below 10 pt, keep text at or above 6 pt, prefer the
condensed bold over shrinking.
## 4. Barcodes
### 4.1 Symbologies
Supported today: 71 ids (the OkapiBarcode families, four through Zint, Code 93i, Co-op 2 of 5). Fit boxes are
qualified for 13: `code128`, `code39`, `code93`, `itf`, `gs1-128`, `qr`, `ean13`, `ean8`, `upca`, `upce`,
`datamatrix`, `pdf417`, `aztec`.
Version 2 keeps the catalogue and the dynamic symbology allowlist. To add, in this order:
1. GS1 Data Matrix and GS1 QR as fit-qualified ids (`gs1-datamatrix`, `gs1-qr`). The encoders already accept a GS1
data type as an option, but the fit profile admits default options only.
2. SSCC-18 (`nve18` exists in the catalogue) and ITF-14 with bearer bars as fit-qualified ids.
3. GS1 DataBar family fit qualification, when a label needs it.
### 4.2 GS1 application identifiers and the human-readable line
Today a host builds the bracketed element string and the caption itself (Forge's `EchoGs1`). Version 2 adds:
- `gs1`: a list of `{ "ai": "01", "valuePath": "/record/gtin" }` entries in place of `value`. The renderer builds
the element string, checks each identifier's length and character set, checks the check digit of 00, 01 and 02,
and places separators after variable-length fields. An unknown identifier or a bad value is the refusal
`GS1_INVALID` with the identifier and a reason code. (What the encoder library already validates was not
verified here; the builder must not duplicate it.)
- `hri`: `{ "position": "below" | "above" | "none", "font": "qqq-mono", "size": "7pt", "fit": {...} }`. The line is
drawn by the text engine inside the barcode element's box, so bars and caption are centred as one unit. For GS1
it reads `(01)00012345678905(10)L240611A`. It uses the same fit rules as any text and may wrap between
identifiers.
A plain `value` in bracket syntax keeps working, so the first release does not depend on the builder.
### 4.3 Module size, centring and quiet zones
- Bars are whole dots. The renderer picks the largest module, in dots, that fits the box on both axes including
quiet zones, never smaller than `minModule` and never larger than `maxModule` if given.
- `minModule` is physical. Defaults: 0.25 mm for linear symbologies and 0.375 mm for two-dimensional ones. That is
2 and 3 dots at 203 dpi, 3 and 5 at 300 dpi, 6 and 9 at 600 dpi. These defaults are proposals to confirm on a
printer (section 10.6).
- For linear symbologies the bar height fills the box height (less the human-readable line) and no longer scales
with the module. In version 1 the height scales with the module, so a short box caps the module.
- The fitted symbol is placed in its box by `align` and `valign`. Left over dots are blank.
- `quiet` is modules on each side, with the existing minimums (10 linear, 4 matrix). Quiet zones are inside the
box and are never cropped. The warning `QUIET_ZONE_INTRUDED` fires when another element's ink lands in one.
### 4.4 When the data does not fit
The label never prints a code below its stated minimum module.
- If even `minModule` does not fit, the refusal is `BARCODE_DOES_NOT_FIT` with the dots needed at the minimum and
the dots available. The message reads, for example: "Barcode 'code': the value needs 742 dots at the 2 dot
minimum module; the box is 690 dots wide at 203 dpi."
- An author who accepts the risk writes `"minModule": "1dot"`. Preview and publish then show the warning
`BARCODE_MODULE_BELOW_RECOMMENDED` every time.
- A later addition, not in the first round: `fallback`, an ordered list of alternative symbologies to try.
### 4.5 Print quality at 203 dpi
For the authoring guide, to be confirmed by the physical check:
- A module is 0.125 mm per dot. Use 2 dots or more for linear codes; 1 dot bars are at the limit of the head.
- Print linear bars parallel to the feed direction (rotation 0 on a normally fed label). Bars across the feed lose
edge quality at speed.
- Keep the quiet zone clear; a centred code inside a wide box is the simplest way.
- Prefer a two-dimensional symbol when the data is long and the label is short.
### 4.6 Verification by decoding
Every barcode test decodes what will be printed, not what the encoder returned: the test takes the emitted ZPL,
rebuilds the bitmap from its graphic fields, and decodes it with ZXing (already a test dependency). It asserts the
payload, the symbology, the module size in dots and the blank margins. This runs at 203, 300 and 600 dpi for each
fit-qualified symbology, for short, typical and longest values.
## 5. Graphics
- **Image**: an asset (PNG or JPEG, as today) in a physical box. `fit` is `contain` (scale to the box, keep the
ratio; default) or `none` (one image pixel per dot, as version 1). Scaling averages area and then reduces to
black and white by `mode`: `threshold` (default, for line art and logos) or `dither` (for photographs).
`IMAGE_UPSCALED` warns when a small image is enlarged.
- **Box and line**: `box` with `thickness` (a length, at least 1 dot) and optional `fill`. A line is a box whose
height equals its thickness.
- **Reverse print**: `"reverse": true` on a text or box draws white on a black field the size of the box.
- **Order**: elements paint in list order onto one canvas, so text over a filled box behaves as written.
**How version 2 reaches the printer.** The renderer composes the whole label on one 1-bit canvas at the target
resolution and emits it as `^GFA` strips, trimmed to the rows and columns that hold ink. The output uses only
commands the host output policy already allows, so `ZplOutputPolicy` needs no grammar change. This is the same
method as the printing QBit's test label. The cost is size: a full 4 x 6 in label is about 250 KB of ZPL at 203 dpi,
540 KB at 300 dpi and 2.2 MB at 600 dpi before trimming; a 4 x 1 in label is about 41 KB at 203 dpi. Section 12
covers the consequences.
## 6. Preview inside the QBit
### 6.1 What the preview is
The preview is the print path stopped before retention. The same `IsolatedRenderer` renders the same revision
content with the same context code, the host runs the same `ZplOutputPolicy`, and the picture is then decoded on
the host **from the ZPL bytes that would be sent**. There is no second renderer and no parallel drawing code, so
the preview cannot differ from the print for a version 2 scene.
```
editor / record screen
| template, draft fingerprint, sample name or record id, target dpi
v
DocumentPreviewService (foundation) authorise, build context, no rows written
| PreviewRequest
v
IsolatedRenderer.preview (bridges/renderer, or the remote renderer adapter over it)
| RENDER with diagnostics collected -> isolated renderer (remote service image)
| <- ZPL bytes + diagnostics
| ZplOutputPolicy.validate (unchanged)
| ZplRaster: ^GFA and ^GB -> 1-bit bitmap -> PNG
v
Preview: PNG pages, exact flag, diagnostics, output size and hash
```
- **Which component renders.** The isolated renderer, through whatever `IsolatedRenderer` the host configured (the
remote renderer image in Forge). Never in process: the renderer README forbids adding the engine to the
application JVM, and this design keeps that.
- **Who makes the picture.** New class `ZplRaster` in `bridges/renderer`. It walks the already validated output and
paints `^GFA` and `^GB` exactly. PNG encoding of our own bitmap is the only image work in the application JVM. A
preview over 16 million dots is refused (`PREVIEW_TOO_LARGE`); a 4 x 6 in label at 600 dpi is 8.6 million.
- **Version 1 scenes** can be previewed too. Graphics and boxes are exact. Printer font text cannot be drawn, so
each `^A0N` field is shown as a hatched placeholder of its estimated size, the result carries `exact: false` and
the warning `PRINTER_FONT_NOT_SHOWN`. A version 1 scene previews only at its stock's dpi.
- **Refusals in preview.** The request asks the renderer to collect diagnostics instead of stopping at the first
failure. Elements that failed are left out of the bitmap and reported with their boxes, and the result says
`printable: false`. When there is no error, the ZPL is byte for byte what printing produces; a test asserts the
hashes are equal.
### 6.2 Data for a preview
- **Sample data.** `samples` in the scene holds up to eight named context objects (64 KiB in total). They are
written by the author and must be synthetic. The first is the default. They travel with the scene, so a seeded
document previews on a fresh database.
- **Stress.** "Longest values" replaces every bound value with the worst-case string for its `maxLength`.
- **A real record.** The author chooses a source record, as in today's "Preview draft". The context is built the
same way as for generation, including the pinned context script when the host has one.
### 6.3 API and permissions
- Port: `GenerationPorts.IsolatedRenderer.preview(PreviewRequest)`, a default method that refuses, implemented by
`IsolatedRendererBridge` and delegated by `RemoteRendererBridge`. A host with its own adapter keeps compiling.
- Service: `DocumentPreviewService.preview(templateId, draftFingerprint or published, format, data, targetDpi)`
returns `Preview(pages, exact, printable, diagnostics, outputBytes, outputSha256)`.
- It writes no revision, request, event or file. It logs one structured line (actor, template id, target, outcome,
diagnostic codes) with no content.
| Preview of | Data | Permission |
|---|---|---|
| A draft | Sample or stress | `document-generation.configure` |
| A draft | A real record | `document-generation.configure` and `.generate`, plus read access to that record, as today |
| The published revision | Sample | `document-generation.view` |
| The published revision | A real record | `document-generation.generate`, plus read access to that record |
No new permission name is needed.
### 6.4 How it appears, with no host code
Forge serves `qqq-frontend-next`. A QBit can show an image there today in three ways that need no frontend change:
a `FILE_PREVIEW` field holding a `data:image/png` value (a thumbnail of at most 512 px that opens a lightbox), a
composite widget with an `IMAGE` block (an inline `<img>` whose `path` may be a data URL, with width and height
styles) and text blocks, or sanitised HTML. The content security policy allows `data:` images. The QBit already
uses standard forms and adornments only; this design keeps to that for the first release.
**First release (existing components only):**
1. **On the template record screen**: a new section "Label preview" holding a composite widget
(`QFieldSection.withWidgetName`). Its renderer class, in the foundation, calls `DocumentPreviewService` with the
draft, the default sample and the stock's dpi, and returns an `IMAGE` block (the PNG as a data URL, shown at a
fixed physical scale) followed by one text block per diagnostic. Opening a template shows its label. A
template with no ZPL source shows a line saying so.
2. **Action "Preview layout"** on a template: a form with the data choice (a sample, "longest values", or a
record), the format and the resolution; then a result step that is the same composite widget, seeded from the
process value of the same name, with the list of diagnostics beside the image. Running it again after an edit
takes one click.
3. The existing action is relabelled "Generate retained preview file" so the two are not confused. Its behaviour
does not change.
The first task of that package is a spike in a running frontend to confirm that a seeded composite widget in a
process step renders an `IMAGE` block from a data URL (the pieces were read in the frontend source, the
combination was not run). The fallback, also already present, is a view form with a `FILE_PREVIEW` field and an
HTML list.
**Second release (a `qqq-frontend-next` change):** a purpose-built preview component, in the frontend's existing
pattern for QBit screens (`src/components/<qbit>/` plus a widget type): the source editor and the picture side by
side, refresh on save, zoom, a resolution switch, and element boxes and quiet zones drawn over the picture from the
diagnostics' coordinates. The backend contract is the same `Preview` value, so nothing in this repository changes
for it.
**A host application** gets all of the above by registering the QBit as it does now. Forge adds help entries for
the new process, fields and widget (its help coverage test requires them) and nothing else.
### 6.5 Page (PDF) templates
- Today's retained preview keeps working for PDF and stays the way to obtain the exact file.
- Second release: the same "Preview layout" action for PDF. The frontend preview accepts a data URL only for
images, so the isolated renderer rasterises the first pages of the PDF it just produced (PDFBox, at a stated
screen resolution) and returns PNG pages through the same `Preview` value. The result is marked as page images of
the exact PDF.
## 7. Templates and page layout beyond labels
**Today.** HTML with FreeMarker (autoescaped), paged CSS 2.1: stock size and margins, tables, repeating headers and
footers, page breaks, absolute positioning, borders, images, TrueType assets, the `<qqq-barcode>` element with fit
boxes. One starter, the packing slip. No flex or grid, no SVG, no script.
**What great means for page documents.**
- Default fonts without an upload: the bundled faces are available as CSS families (`'QQQ Sans'`, `'QQQ Sans
Condensed'`, `'QQQ Mono'`).
- A fast preview with sample data (section 6.5) and the same diagnostics list.
- Starters that look finished: packing slip (exists), pick list, pallet content sheet, each with sample data.
- Refusals that name the cause: an unknown asset, an image over the limits, more pages than allowed.
**In this round:** bundled CSS families; page preview images; refusal codes for the PDF path. **Out:** a visual
designer, flex or grid layout, SVG, new starters beyond one, any change to the OpenHTML fork.
**Label starters.** A small library of version 2 scenes ships as resources, like the packing slip starter:
GS1-128 on 4 x 1 in, a product label with text and a code on 3 x 2 in, a 4 x 6 in shipping layout, and two tools,
a **type specimen** (each bundled face at 5 to 14 pt and several thresholds) and a **resolution ruler** (rulers
drawn for 203, 300 and 600 dpi; the one that measures true tells the operator the printer's resolution).
## 8. Errors and diagnosis
### 8.1 Diagnostics
The renderer's answer gains a `diagnostics` list, and its error answer gains one diagnostic as `detail`. A
diagnostic is closed vocabulary and numbers only:
```json
{ "code": "TEXT_DOES_NOT_FIT", "severity": "ERROR", "element": "caption",
"box": [24, 131, 764, 56], "measured": { "widthDots": 902, "sizeCentipoints": 700, "lines": 2 },
"limit": { "widthDots": 764, "lines": 2 } }
```
- `element` is the element's `id`, which the template author wrote and which is limited to
`[A-Za-z][A-Za-z0-9_-]{0,31}`. No record value, context text or exception message is ever included. This keeps the
rule the renderer has today (errors must not carry source data) while giving up the single opaque code.
- The host turns a diagnostic into a sentence with the element, the measured value and the limit, in physical
units as well as dots. `GenerationPorts.FailureCode` grows, and `RenderException` carries the diagnostic.
| Severity | Codes (first release) |
|---|---|
| ERROR | `SCENE_INVALID` (with the JSON pointer of the offending property), `STOCK_MISMATCH`, `TARGET_REQUIRES_SCENE_2`, `ELEMENT_OUTSIDE_PRINTABLE_AREA`, `TEXT_DOES_NOT_FIT`, `VALUE_MISSING`, `VALUE_TOO_LONG`, `FONT_UNKNOWN`, `FONT_MISSING_GLYPH`, `RESERVED_FONT_NAME`, `BARCODE_DOES_NOT_FIT`, `GS1_INVALID`, `ASSET_UNKNOWN`, `PREVIEW_TOO_LARGE`, `RENDERER_TOO_OLD` |
| WARNING | `TEXT_TRUNCATED`, `TEXT_SMALL_FOR_DPI`, `TEXT_LIGHT_FACE`, `FIT_NOT_PROVEN`, `BARCODE_MODULE_BELOW_RECOMMENDED`, `QUIET_ZONE_INTRUDED`, `ELEMENTS_OVERLAP`, `IMAGE_UPSCALED`, `DEVICE_UNIT_USED`, `PRINTER_FONT_NOT_SHOWN` |
| INFO | `TEXT_SHRUNK` (from and to size), `TEXT_WRAPPED` (lines), `BARCODE_MODULE` (dots and micrometres), `IMAGE_SCALED` |
Version 1 scenes keep their existing codes and the opaque `RENDER_FAILED`.
### 8.2 A document that is not offered for a printer
Whether a document can print on a printer has three owners, and the message is assembled from them:
| Part | Owner | What it provides |
|---|---|---|
| What the document needs | Document generation | New `DocumentAvailability.explain(table, format, target)`: for each published template, offered or not, with a reason code and both sizes |
| What the printer has | Printing | The media profile the agent reports (width, height, dpi), as today |
| Joining the two | The printing bridge in this repository (`bridges/printing`) | New `PrintableDocuments.forPrinter(...)`, so hosts stop writing the match themselves |
| Showing it | The application screen (Echo in Forge) | Lists what is offered and states what is not and why |
With version 2 and a render target, resolution is no longer a reason; a 203 dpi printer and a 300 dpi printer take
the same document. The remaining reasons are `STOCK_SIZE_MISMATCH` ("needs 4 x 1 in; the printer reports 4 x 2 in")
and, for version 1 documents only, `RESOLUTION_MISMATCH` ("this older document was drawn for 300 dpi; the printer
reports 203 dpi; convert it to physical units").
The scanner QBit holds no document logic (its base module was searched), and the Android client only shows what
the server sends, so this needs no scanner or app release. The client's own wording for an empty list was not found and is not changed.
## 9. Security and data
- **Template source** is still interpreted only inside the isolated renderer. The version 2 parser, the asset
inspector and the upgrade function all run there. The host's new code handles only bytes it already validated
(`ZplRaster`) and bytes it stores (`document_generation_asset_content`).
- **Uploads.** Fonts: TrueType only (as today), 2 MiB, leading bytes checked on the host, parsed in the renderer,
embedding bits honoured, a rights statement required and stored. Images: PNG or JPEG, the existing dimension and
pixel limits, SVG still refused. Assets are addressed by hash; identical bytes are stored once.
- **Bundled fonts.** Unmodified, each with licence, provenance and hash, listed in `THIRD-PARTY.md` and `NOTICE`.
- **No preview is stored.** No revision, request, event, retained file or log content. The PNG of a real-record
preview exists in the response and, when shown through a process step, in that process's server-side state until
it expires, as any process screen showing record data does. (That store's retention was not verified.) The record
screen widget uses sample data only, so opening a template never renders customer data.
- **Samples** are synthetic by rule, stated in the field help and the authoring guide.
- **Diagnostics and logs** carry element ids and numbers only.
- **Permissions** are the existing five; section 6.3 gives the mapping. Uploading and converting need `configure`.
- **Parser hardening.** The version 2 scene parser and `ZplRaster` take untrusted input. Both get negative tests
and a bounded mutation campaign (section 10.5).
## 10. Test strategy and coverage
### 10.1 Compatibility of version 1 (first, before any renderer change)
A corpus of version 1 scenes with the SHA-256 of their ZPL at 203, 300 and 600 dpi, generated from the tag Forge
runs (`v1.1.0-RC.2`; the renderer sources are identical on `develop` today) and never regenerated. It contains the
shapes stored in Forge's database (plain, scanner fidelity, compact GS1, the raster text document, with synthetic
values), the README examples and one scene per element type and option. The test fails on any byte difference.
### 10.2 Golden images per resolution
For each version 2 fixture (every starter and one scene per feature) a PNG at 203, 300 and 600 dpi is committed.
Barcode and box pixels are compared exactly everywhere. Text pixels depend on the Java runtime's rasteriser, so
unit tests on a developer machine compare with a small tolerance and always assert structure (ink bounds inside
the box, centring within one dot), and the exact comparison runs in the image qualification on the pinned runtime,
the existing `*ImageQualification` pattern. A changed golden is reviewed as a picture in the pull request.
### 10.3 Decode tests
As section 4.6: decode the bitmap rebuilt from the emitted ZPL, per symbology, per resolution, per value length.
The same rebuild is `ZplRaster`, so the preview code is exercised by every barcode test.
### 10.4 Property tests for fit
Seeded, deterministic generators (no clock, fixed seeds) over text, box, size, floor, lines and resolution assert:
- the result is either a layout whose ink is inside the box or a refusal whose measured value exceeds its limit;
- ink is never clipped;
- the chosen size is never below the floor and never above the nominal size;
- a shorter string never gets a smaller size than a longer one in the same box;
- a published `maxLength` implies no length refusal for any string up to that length in the tested alphabets;
- for barcodes, the module is whole dots, at least the minimum, and the symbol decodes.
### 10.5 Negative and fuzz tests
Malformed scenes, lengths and assets; ZPL with wrong lengths for `ZplRaster`. A bounded mutation campaign over the
scene parser and `ZplRaster` runs in the normal test phase, with its seeds, limits and replay command in
`docs/security/fuzzing.md` (new), as the testing standard requires for parsers of untrusted input.
### 10.6 Physical check before a final release
Print the specimen, the ruler and the GS1 starter on a 203 dpi Zebra and a 300 dpi Zebra and scan them. This
confirms the proposed defaults (minimum modules, small type thresholds) and is recorded with the release. An RC
may reach dev before it; a final version may not. No agent can do this step.
### 10.7 Coverage
The testing standard requires 80% line coverage of changed lines and 100% for published libraries, per package.
The repository today gates only `foundation` at 85% instructions.
- New packages (`com.kingsrook.qbits.renderer.scene2` and below, and the new foundation and bridge classes) are
gated at 100% line coverage, with exclusions only by explicit marker and written reason.
- JaCoCo is added to `renderer` and `bridges/renderer` with a bundle gate at the measured baseline, never lowered.
- The `foundation` gate is not lowered.
- Changed-line coverage of 80% is checked by the reviewer from the JaCoCo report; the shared Gitea workflow has no
such gate that I could find.
### 10.8 Review
Every package goes through a pull request into `develop` with: the full `./mvnw verify` on the branch head with no
skipped gate; an independent review by an agent that did not write it, covering correctness, silent failures and
test adequacy; and the review focus listed for the package. James merges.
## 11. Repositories, versions and what Forge does
### 11.1 Order and versions
| Order | Repository | Change | Released as |
|---|---|---|---|
| 1 | `qbit-document-generation` | First slice (section B) | `1.4.0-RC.1` from a new `release/1.4` |
| 2 | same | Renderer images built from that version | `qqq-document-renderer` and `qqq-remote-renderer`, tag `1.4.0-RC.1` |
| 3 | `gg-forge`, `gg-forge-cd` | Pins, schema, one version 2 document on dev | Forge dev build |
| 4 | `qbit-document-generation` | Follow-on packages | `1.4.0-RC.2` and later |
| 5 | `qqq-frontend-next` | Purpose-built preview component | next `v1.0.0-RC.N` tag |
| 6 | `gg-forge` | Remove workarounds, convert the standard documents | Forge dev, then staging |
`develop` is `1.4.0-SNAPSHOT` and the workflow names an RC from the branch, so the new line is `release/1.4`. The
`release/1.1` line that Forge runs in production stays untouched for fixes. Only one release-branch merge may be in
flight across QRun repositories.
### 11.2 Other repositories
| Repository | Needed for the first slice | Later |
|---|---|---|
| `qbit-printing` | Nothing. A label is opaque ZPL to it, and the default limit is 20 MiB. | Optional: move the resolution ruler into the built-in test documents. |
| `qqq-print-agent` | Nothing. ZPL is written raw to the queue and never parsed. No agent release is needed for anything in this design. | It repeats the payload once per copy; for large 600 dpi labels and many copies that multiplies the spool size. |
| `qqq-print-protocol` | Nothing. | Nothing. |
| `qbit-mobile-scanners` and the Android client | Nothing. | Nothing. |
| `qbit-record-files`, `qbit-collaboration` | Nothing. Record Files remains an optional asset source. | Nothing. |
| `qqq` core | Nothing. The widget, block, adornment and section APIs used exist in the version Forge pins. | Nothing. |
| `qqq-frontend-next` | Nothing. | The preview component (section 6.4). |
Nothing in this design is held back by a client or agent release.
### 11.3 Rollout rule for the renderer image
The bridge accepts only an exact renderer version string, and the new answers have new fields. To allow a rolling
change:
- Bridge `1.4` accepts renderer `0.1.0` (today's answers) and `0.2.0` (new answers). With an old image, version 1
printing continues and a version 2 scene or a preview is refused with `RENDERER_TOO_OLD`, which names the fix.
- Renderer `0.2.0` is not accepted by bridges before `1.4`.
- Therefore: upgrade the host library first, then the image. Never the reverse.
### 11.4 What Forge pins, adds and deletes
Pins and additions (first slice):
- The five document generation artifacts at `1.4.0-RC.1`; the two image digests in `gg-forge-cd`, after the host.
- A changeset for the new request column (`target_dpi`). Later a changeset for
`document_generation_asset_content`.
- Help entries for the new process, fields, section and widget; the permission and composition lists its tests
check.
- `EchoDocumentGenerator` passes the printer's reported dpi as the render target.
- One new setup-data changeset that seeds a version 2 GS1 document for 4 x 1 in, using a bundled face. Existing
changesets are not edited.
Deletions, once the matching upstream piece is on dev and one label has printed:
| Forge workaround | Retired by | Condition |
|---|---|---|
| `EchoBundledFonts`, its font resources, test and script | Bundled faces in the renderer | No published revision still references the `forge-bundled-font:` asset. The 0050 document is republished by a new changeset as a version 2 scene with a bundled face; its old revision row stays but is never rendered again, because reprints use retained bytes. |
| Hand geometry in `EchoTemplatePublisher` (dot conversion, insets, module search, the centred scene builder) and the duplicate scene builders in `scripts/local` | Version 2 scenes | Standard documents seeded as version 2 |
| One document per resolution (0039 seeds 203 and 300 dpi copies) | Render target | New seeds carry one document per size; old rows remain editable data |
| The exact stock match and silent drop in `EchoPrintingDestinations.resolve` and `EchoService.stockMatches` | `PrintableDocuments` in the printing bridge | Follow-on package |
| Offline bitmaps in `docs/history` used to judge a layout | Preview | First slice |
Forge's `echoPublishDocument` process and the SQL that re-implements the draft fingerprint are not addressed here.
A declarative import for documents would retire them and is worth its own design.
## 12. Risks and decisions
### Decisions for James
| # | Decision | Recommendation |
|---|---|---|
| 1 | **Bundled fonts and their licence.** The dependency standard's allowlist does not include the SIL Open Font License 1.1, yet `qbit-printing` already ships JetBrains Mono under it and Forge ships Abel under it, with no recorded exception. | Record one exception for unmodified OFL 1.1 fonts shipped as data, covering printing's existing font and the renderer's set. Bundle IBM Plex Sans (Regular, Bold), IBM Plex Sans Condensed (Bold) and JetBrains Mono (Regular, Bold), after confirming each licence file. The alternative with no exception is to restrict the set to fonts published under Apache 2.0. |
| 2 | **Asset bytes in the QBit's own table.** Fonts and logos up to 2 MiB stored in the database, so upload and seeding work everywhere. It moves the stated boundary that the QBit owns no file bytes. | Yes. Assets are definition data. Record Files stays as an optional second source. |
| 3 | **Version 2 prints the whole label as a bitmap and offers no printer font.** This is what makes preview exact and fit measurable. It makes jobs larger (section 5) and gives up printer-drawn text. | Yes. It is how the printing test label already works. Add compression only if 600 dpi job size proves a problem; that would mean widening the host output policy. |
| 4 | **Release line.** A new `release/1.4` for this work, leaving `release/1.1` for production fixes, with the host-first rollout rule of section 11.3. Production later moves from the 1.1 line to the 1.4 line. | Yes. |
| 5 | **Renderer images.** They are built and pushed by hand to `ghcr.io/gg-engineering`, which is the slowest step between a merged change and dev. | Add a build job for them to this repository's CI in the follow-on, and decide whether QRun images belong in a QRun registry. Not required for the first slice. |
### Decided in this design
- Version 1 is frozen; version 2 is a separate schema id and code path; upgrade is an explicit authoring action.
- Pull request 11 is replaced, not merged (see below).
- Lengths are strings with a unit; coordinates are relative to the printable area; nine-point anchors plus
`align` and `valign`; `group` is in, rows and tables are out.
- Default text fit is one line, shrink to 70%, then refuse; truncation is opt-in; `maxLength` makes fit provable at
publish.
- The barcode never prints below its stated minimum module; the default refuses.
- The preview picture is decoded on the host from the validated ZPL; the renderer does not draw a second image.
- The first preview UI uses components the frontend already has; the purpose-built component follows.
- Resolution is a render target chosen at print or preview time; flexible stock size comes later.
- Structured diagnostics replace the opaque failure for version 2, carrying ids and numbers only.
### Pull request 11
PR 11 ("align a fitted ZPL barcode inside its fit box; justified font 0 text blocks") changes version 1 in place.
- Its barcode half is sound and its arithmetic and decode tests are carried into version 2, where every element
has `align` and `valign`. It is not applied to version 1, because published scenes that already carry `align` on
a barcode would begin to print differently.
- Its text half emits `^FB` field blocks. The host output policy (`ZplOutputPolicy.text`) requires `^FH_^FD`
directly after the font command and rejects anything else, and the PR does not change the bridge. Through the
bridge Forge uses, every label with a justified text block would be refused. It also keeps the character-count
estimate and relies on the printer to centre, which cannot be previewed.
So the design absorbs the idea and replaces the PR. Its tests move into the version 2 barcode package, and the PR
is closed when that package merges. Forge's unused `zplGs1CentredScene`, which targets PR 11, is deleted with the
other hand geometry.
### Risks
| Risk | Mitigation |
|---|---|
| Text rasterisation differs between Java builds, so goldens flap | Exact comparison only on the pinned runtime; structural assertions elsewhere (10.2) |
| Proposed module and type defaults are wrong for real media | Physical check before a final version (10.6); defaults are data in one place |
| Larger ZPL exceeds a host limit (Forge sets a 1 MiB output limit) or slows a 600 dpi printer | Ink trimming; documented sizes; the limit is host configuration; compression held in reserve (decision 3) |
| Host and image versions out of step | Section 11.3 rule and the named `RENDERER_TOO_OLD` refusal |
| The seeded composite widget does not render as read | Spike first; `FILE_PREVIEW` fallback (6.4) |
| The record screen widget renders on every open | Renderer admission already bounds concurrency; the widget uses sample data and can be collapsed |
| Scope: the plan is large | The first slice is seven packages; everything else is independent and follows |
| Manual image builds delay dev | Decision 5 |
---
# B. Work plan
All packages are in `qbit-document-generation` unless a repository is named. Each is one pull request into
`develop`. Branch names never start with `release/`. Sizes: S about a day, M two to three days, L about a week of
agent work including review.
## B.1 Shared contracts (so packages can be built in parallel)
**Renderer wire (protocol id unchanged, renderer version `qqq-renderer-0.2.0`).**
- `RENDER` and `VALIDATE` accept optional `"targetDpi"` and `"diagnostics": "COLLECT"`.
- A successful answer adds `"diagnostics": [ ... ]` (section 8.1). With `COLLECT` and at least one error, the
outcome is `"REFUSED"`, with `diagnostics` and the partial output.
- An error answer adds `"detail": { ... }`, one diagnostic.
- New operations, each routed by `RendererMain` to one class: `INSPECT_ASSET` (`AssetInspector`), `UPGRADE_SCENE`
(`scene2.upgrade.SceneUpgrade`). P1 creates both as classes that refuse with `UNSUPPORTED_OPERATION`.
**Renderer element contract (package `com.kingsrook.qbits.renderer.scene2`, owned by P1).**
- `Length` (parse, micrometres, dots by rule), `Frame` (a resolved box in dots), `Layout` (anchor resolution),
`Canvas` (1-bit, paint order, reverse), `Diagnostics` (collector), `SceneElement` with
`Frame place(Layout)` and `void paint(Canvas, Frame, RenderInput, Diagnostics)`, `ElementFactory` keyed by `type`.
- P2, P3 and P4 each add one sub-package and register one factory in a file they own
(`scene2/text/TextFactory`, `scene2/barcode/BarcodeFactory`, `scene2/graphics/GraphicsFactory`); P1 lists the
three class names in `ElementFactory` from the start, with placeholders that refuse.
**Foundation ports (owned by P6a).** `GenerationPorts.Diagnostic`, `PreviewRequest`, `PreviewPage`, `Preview`, the
new `FailureCode` values, `RenderException(FailureCode, Diagnostic)`, `IsolatedRenderer.preview` as a refusing
default, `Integer targetDpi` on `RenderRequest`. P6a also adds the registration calls for
`GenerationPreviewProcesses`, `GenerationAssetProcesses`, `GenerationUpgradeProcesses` and `GenerationAssetTables`
in the producer, and the call to `GenerationPreviewWidget.section(table)` in `GenerationTables`, each an empty
class or no-op, so later packages fill only their own file.
## B.2 Packages
### First slice
| Id | Package | Branch | Size | Depends on |
|---|---|---|---|---|
| P0 | Version 1 compatibility corpus | `test/v1-compat-corpus` | S | none |
| P13 | Coverage gates and fuzz scaffolding | `test/coverage-and-fuzz-gates` | S | none |
| P6a | Ports and registration hooks | `feat/preview-ports` | S | none |
| P1 | Scene 2 core, canvas, emitter, diagnostics | `feat/scene2-core` | L | P0, P13 |
| P2 | Text: bundled fonts, measurement, fit | `feat/scene2-text` | L | P1 |
| P3a | Barcode: module fit, alignment, decode tests | `feat/scene2-barcode-fit` | M | P1 |
| P5 | Bridge: `ZplRaster`, diagnostics, preview, version rule | `feat/bridge-preview` | M | P6a |
| P6b | Preview service, action and record widget | `feat/preview-ui` | M | P6a (P5 for the integration test) |
| P7 | Render target dpi | `feat/render-target-dpi` | M | P6a |
**P0. Version 1 compatibility corpus.**
Scope: section 10.1. Files: new tests and resources only, under `renderer/src/test` and `bridges/renderer/src/test`
(`ZplSceneOneCompatibilityTest`, `resources/compat-v1/`). Tests: the corpus itself, run against untouched code,
plus proof that each stored ZPL passes `ZplOutputPolicy`. Review focus: the corpus covers every version 1 element,
option and the Forge scene shapes; values are synthetic; hashes come from the tagged source, not from the branch.
**P13. Coverage gates and fuzz scaffolding.**
Scope: section 10.7 and 10.5 scaffolding. Files: `renderer/pom.xml`, `bridges/renderer/pom.xml` (JaCoCo with a
per-package rule for new packages and a baseline bundle rule), `docs/security/fuzzing.md`, a seeded mutation test
harness class in each module's test tree. This package is the only one that edits those two `pom.xml` files in the
first slice. Review focus: gates fail when they should (prove with a deliberate miss), no skip switches.
**P6a. Ports and registration hooks.**
Scope: B.1 foundation contract. Files: `foundation/.../GenerationPorts.java`, `DocumentGenerationQBitProducer.java`
and `GenerationTables.java` (registration calls only), five new empty classes. Tests: record validation, default
refusals, source compatibility of existing constructors. Review focus: nothing breaks an existing host adapter; diagnostics cannot hold free text.
**P1. Scene 2 core.**
Scope: sections 2, 5 (box, canvas, emission), 8.1 for the renderer; `RendererMain` routing by schema and for the
new operations; renderer version `0.2.0`; `renderer/schema/zpl-scene-2.schema.json`; `box` and `group` elements;
`overrides`; `samples` accepted and ignored by rendering. Files: new package `scene2`; `RendererMain.java`,
`Wire.java` and `RenderInput.java` (target dpi, bundled-name reservation hook). `ZplRenderer.java`,
`RasterText.java` and `BarcodeFit.java` are not edited. Tests: unit tests for every length and rounding rule,
anchor resolution for all nine anchors and negative shifts, row-band emission (the rebuilt bitmap equals the
canvas), goldens for box scenes at three resolutions, negative tests for the parser, P0 still green, 100% line
coverage of the package. Review focus: rounding rules match section 2.2 exactly; no path from context values into
diagnostics; emitted ZPL passes the unchanged `ZplOutputPolicy`; limits on elements, depth and canvas size.
**P2. Text.**
Scope: section 3.1 (bundled set and reserved names), 3.3 (fit, `maxLength`, legibility warnings), reverse text,
rotation. Files: `scene2/text/*`; `renderer/src/main/resources/fonts/**` with licence, provenance and hash files;
`renderer/THIRD-PARTY.md`, `renderer/NOTICE`. Tests: property tests of section 10.4; goldens; glyph coverage
refusal; a test that every bundled face has licence, provenance and a matching hash; worst-case proof for
`maxLength`. Review focus: the fit order is exactly as stated; truncation only when asked; licences confirmed from
files; determinism (no locale, no installed fonts).
**P3a. Barcode fit and alignment.**
Scope: sections 4.3, 4.4, 4.6 for the 13 fit-qualified symbologies, with `value` and `valuePath`, dynamic
symbology allowlist and scanner profile carried over. Files: `scene2/barcode/*` (new `ModuleFit`; the version 1
`BarcodeFit` is called, not changed). The alignment tests of PR 11 are ported here. Tests: decode tests at three
resolutions for short, typical and longest values; centring within one dot; refusal content; below-minimum warning.
Review focus: no code prints below the stated minimum; quiet zones never cropped; integer arithmetic only.
**P5. Bridge preview.**
Scope: sections 6.1, 6.3 (port implementation), 11.3. Files: `bridges/renderer/.../ZplRaster.java` (new),
`IsolatedRendererBridge.java` (diagnostics parsing, accepted versions, `preview`, target dpi in the request and in
the stock passed to the policy), `RendererJson.java`; `bridges/remote-renderer/adapter/.../RemoteRendererBridge.java`
(delegate). `ZplOutputPolicy.java` grammar is not changed. Tests: raster of every corpus ZPL from P0; refusal of
malformed graphics; the size limit; old-image behaviour (`RENDERER_TOO_OLD`); print and preview hashes equal;
mutation campaign over `ZplRaster`. Review focus: only validated bytes are rasterised; no font or image decoding on
the host; error mapping stays closed vocabulary; the version rule.
**P6b. Preview service and UI.**
Scope: sections 6.2 to 6.4 first release. Files: new `DocumentPreviewService.java`,
`GenerationPreviewProcesses.java`, `GenerationPreviewWidget.java` (including the "Label preview" section it
attaches to the template); `GenerationAuthoringProcesses.java` (the relabel only); help text and labels for every
new field. It begins with the frontend
spike of section 6.4. Tests: permission matrix of section 6.3; nothing written to any table (assert row counts);
sample, stress and record data paths with a fake renderer; diagnostics to sentences; one browser check in the
host's local stack. Review focus: no persistence and no content in logs; record access checked before context is
built; messages name element, measured value and limit.
**P7. Render target dpi.**
Scope: section 2.3. Files: `DocumentGenerationService.java`, `GenerationStore.java`, `GenerationProcesses.java`
(option on generate), `GenerationTables.java` (the request field), `DocumentGenerationSchema.java`, three
migration scripts, `docs/EXPECTED-PRINT-INTENT.md`;
`bridges/printing/.../PrintingDispatch.java` only if it must carry the value. Tests: digest framing (absent value
keeps old digests; present value changes them); refusal for version 1; MySQL, PostgreSQL and H2 suites; reprint
unchanged. Review focus: old rows and old digests are untouched; the target is validated against 203, 300, 600.
### Follow-on
| Id | Package | Branch | Repository | Size | Depends on |
|---|---|---|---|---|---|
| P3b | GS1 builder and human-readable line | `feat/scene2-gs1-hri` | this | M | P2, P3a |
| P4 | Graphics: scaled images, dither, reverse box | `feat/scene2-graphics` | this | M | P1 |
| P8 | Built-in asset store and upload | `feat/asset-store` | this | L | P6a, P1 |
| P9 | Version 1 to 2 conversion | `feat/scene-upgrade` | this | M | P2, P3a, P6b |
| P10 | Document availability and printer match | `feat/printable-documents` | this | M | P7 |
| P11 | Page documents: bundled CSS families, page preview | `feat/pdf-fonts-preview` | this | M | P2, P5, P6b |
| P12 | Starters, specimen and ruler | `feat/label-starters` | this | S | P2, P3a |
| P14 | Authoring guide and README | `docs/label-authoring` | this | S | first slice |
| P15 | Flexible stock (`atLeast`) | `feat/flexible-stock` | this | M | P10 |
| P16 | Renderer image build in CI | `ci/renderer-images` | this | S | decision 5 |
| F1 | Purpose-built preview component | `feat/document-preview` | `qqq-frontend-next` | L | P6b released |
| G1 | Forge: pins, schema, one version 2 document on dev | per Forge rules | `gg-forge`, `gg-forge-cd` | M | `1.4.0-RC.1` and images |
| G2 | Forge: remove workarounds, convert standard documents | per Forge rules | `gg-forge` | M | G1, P9, P10 |
Notes for the follow-on packages:
- **P3b** files: `scene2/barcode/gs1/*`, `scene2/barcode/Hri.java`. Tests: identifier table cases, check digits,
separator placement decoded back, caption wraps between identifiers. Review focus: do not duplicate what the
encoder already validates.
- **P4** files: `scene2/graphics/*`. Tests: goldens, upscaling warning, pixel limits.
- **P8** files: `GenerationAssetTables.java`, `GenerationAssetStore.java`, `GenerationAssetProcesses.java`,
`GenerationAssetChoices.java`, three migration scripts, `renderer/.../AssetInspector.java`, and the `dg:` branch
in `IsolatedRendererBridge.resolve` (coordinate with P5's owner; it is one method). Tests: upload limits and
refusals, hash addressing, permission, selection listing, the three databases. Review focus: no parsing on the
host; rights statement required; bytes never leave through a query API.
- **P9** files: `scene2/upgrade/SceneUpgrade.java`, `GenerationUpgradeProcesses.java`. Tests: each corpus scene
converts, and the converted scene's graphics match the original within one dot.
- **P10** files: `DocumentAvailability.java` (foundation), `bridges/printing/.../PrintableDocuments.java`.
- **P11** files: `PdfRenderer.java` and a new page rasteriser class in the renderer; the PDF branch of the preview.
- **P14** integrates each package's own notes (written under `docs/labels/` by that package) into
`renderer/README.md` and `docs/AUTHORING.md`, which no other package edits.
## B.3 First on dev
The smallest set that puts a real improvement on dev is the first slice: P0, P13, P6a, P1, P2, P3a, P5, P6b, P7.
It delivers physical-unit layout, anchors and alignment, measured text with fit rules and bundled fonts, centred
whole-dot barcodes that never go below their minimum, structured refusals, one design for every resolution, and a
preview on the template screen.
```
P0 ----+
P13 ---+--> P1 --+--> P2 ---+
+--> P3a --+
P6a ---+--> P5 -------------+--> release/1.4 -> 1.4.0-RC.1 -> images -> G1 (Forge dev)
+--> P6b ------------+
+--> P7 -------------+
```
P0, P13 and P6a start together. P1 follows; P5, P6b and P7 need only P6a and run beside it. P2 and P3a start when
P1 merges. Then: one merge of `develop` into `release/1.4`, the two images built from `1.4.0-RC.1`, and Forge's G1
(host pins first, image digests second). The acceptance for the slice is one version 2 GS1 label, previewed in
Forge on dev, then printed on the dev printer at its reported resolution and scanned.
What follows, in value order: P3b and P12 (GS1 done properly, starters and the ruler), P8 (uploads everywhere),
P9 with G2 (convert the standard documents and delete the Forge workarounds), P10 (say why a document is not
offered), P4, P11, P14, F1, P15, P16.
+1 -1
View File
@@ -7,7 +7,7 @@
<version>${revision}</version>
<name>QBit Document Generation</name>
<description>User-defined document templates and generation coordination for QQQ</description>
<properties><qqq.version>4.1.0-RC.21</qqq.version><maven.compiler.source>21</maven.compiler.source><maven.compiler.target>21</maven.compiler.target><maven.compiler.proc>none</maven.compiler.proc><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><enforcer.skip>false</enforcer.skip></properties>
<properties><qqq.version>4.1.0-RC.26</qqq.version><maven.compiler.source>21</maven.compiler.source><maven.compiler.target>21</maven.compiler.target><maven.compiler.proc>none</maven.compiler.proc><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><enforcer.skip>false</enforcer.skip></properties>
<licenses><license><name>Apache License, Version 2.0</name><url>https://www.apache.org/licenses/LICENSE-2.0</url><distribution>repo</distribution></license></licenses>
<dependencyManagement><dependencies><dependency><groupId>com.kingsrook.qqq</groupId><artifactId>qqq-bom-pom</artifactId><version>${qqq.version}</version><type>pom</type><scope>import</scope></dependency></dependencies></dependencyManagement>
<dependencies>
@@ -38,7 +38,8 @@ import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSBackendMetaDat
/*******************************************************************************
** Qualification of the native QQQ PostgreSQL17 backend (vendor and backend type
** "postgres") and this QBit's owned tables. Observes the live connection only;
** it never changes session or server settings, schema or data.
** it never changes session or server settings, schema or data. Amazon Aurora
** PostgreSQL is the one accepted variant: see durable().
*******************************************************************************/
final class GenerationPostgres
{
@@ -46,7 +47,11 @@ final class GenerationPostgres
private static final String SETTINGS = "SELECT current_setting('server_version_num')::int, current_setting('transaction_read_only'), pg_is_in_recovery(),"
+ " current_setting('synchronous_commit'), current_setting('fsync'), current_setting('full_page_writes'), current_setting('TimeZone'),"
+ " (SELECT setting::bigint FROM pg_settings WHERE name='lock_timeout'), current_setting('transaction_isolation'), current_schema(), current_database()";
+ " (SELECT setting::bigint FROM pg_settings WHERE name='lock_timeout'), current_setting('transaction_isolation'), current_schema(), current_database(),"
+ " to_regprocedure('aurora_version()') IS NOT NULL";
private static final String REQUIREMENTS = "Generation requires synchronous PostgreSQL commits, full-page writes (except on Amazon Aurora PostgreSQL),"
+ " a primary writable server, READ COMMITTED and UTC sessions";
private static final String TABLE = "SELECT c.relpersistence FROM pg_class c JOIN pg_namespace n ON n.oid=c.relnamespace"
+ " WHERE n.nspname=current_schema() AND c.relname=? AND c.relkind='r'";
@@ -91,7 +96,25 @@ final class GenerationPostgres
}
/***************************************************************************
** Exact server, durability, session and owned-table checks for PostgreSQL 17.
** Commit durability of the observed server. Every server needs
** synchronous_commit on or remote_apply and fsync on. Stock PostgreSQL also
** needs full_page_writes on, which protects crash recovery from torn pages.
** Amazon Aurora PostgreSQL runs with full_page_writes off and offers no
** parameter to change it, because its storage layer does not write torn
** pages; so off is accepted there, and only there. Aurora is detected in the
** settings query by to_regprocedure('aurora_version()') IS NOT NULL, which is
** true on Aurora, false on stock PostgreSQL and never raises an error (calling
** a missing function would abort the transaction).
***************************************************************************/
static boolean durable(String synchronousCommit, String fsync, String fullPageWrites, boolean aurora)
{
return (("on".equals(synchronousCommit) || "remote_apply".equals(synchronousCommit)) && "on".equals(fsync)
&& ("on".equals(fullPageWrites) || aurora && "off".equals(fullPageWrites)));
}
/***************************************************************************
** Exact server, durability, session and owned-table checks for PostgreSQL 17,
** with the single Amazon Aurora PostgreSQL allowance stated on durable().
***************************************************************************/
static void qualify(Connection connection, RDBMSBackendMetaData backend) throws SQLException, QException
{
@@ -105,7 +128,7 @@ final class GenerationPostgres
{
if(!row.next())
{
throw new QException("Generation requires synchronous PostgreSQL commits, full-page writes, a primary writable server, READ COMMITTED and UTC sessions.");
throw new QException(REQUIREMENTS + ".");
}
List<String> weak = new java.util.ArrayList<>();
if(row.getInt(1) / 10000 != 17 || row.getString(10) == null || !Objects.equals(backend.getDatabaseName(), row.getString(11)))
@@ -116,7 +139,7 @@ final class GenerationPostgres
{
weak.add("writable primary");
}
if(!Set.of("on", "remote_apply").contains(row.getString(4)) || !"on".equals(row.getString(5)) || !"on".equals(row.getString(6)))
if(!durable(row.getString(4), row.getString(5), row.getString(6), row.getBoolean(12)))
{
weak.add("synchronous commit");
}
@@ -130,7 +153,7 @@ final class GenerationPostgres
}
if(!weak.isEmpty())
{
throw new QException("Generation requires synchronous PostgreSQL commits, full-page writes, a primary writable server, READ COMMITTED and UTC sessions; not satisfied: " + String.join(", ", weak));
throw new QException(REQUIREMENTS + "; not satisfied: " + String.join(", ", weak));
}
long lockTimeout = row.getLong(8);
if(lockTimeout < 1 || lockTimeout > 5000)
@@ -52,13 +52,24 @@ final class PostgresGenerationDatabase implements AutoCloseable
** Starts only this suite's loopback container with durable settings.
***************************************************************************/
PostgresGenerationDatabase()
{
this(true);
}
/***************************************************************************
** Pinned durability settings come from the server command line, which no
** ALTER SYSTEM can override. An unpinned engine starts with the same values
** (the PostgreSQL defaults) and lets a test weaken them through system().
***************************************************************************/
PostgresGenerationDatabase(boolean pinned)
{
// pgjdbc announces the JVM zone as the session TimeZone; qualification requires UTC.
previousZone = java.util.TimeZone.getDefault();
java.util.TimeZone.setDefault(java.util.TimeZone.getTimeZone("UTC"));
container = new GenericContainer<>(IMAGE)
.withEnv("POSTGRES_PASSWORD", rootPassword).withEnv("POSTGRES_DB", "bootstrap")
.withCommand("postgres", "-c", "fsync=on", "-c", "synchronous_commit=on", "-c", "full_page_writes=on", "-c", "max_connections=60")
.withCommand(pinned ? new String[] { "postgres", "-c", "fsync=on", "-c", "synchronous_commit=on", "-c", "full_page_writes=on", "-c", "max_connections=60" }
: new String[] { "postgres", "-c", "max_connections=60" })
.withExposedPorts(5432)
.withCreateContainerCmdModifier(command -> command.getHostConfig().withMemory(768L * 1024 * 1024).withNanoCPUs(2_000_000_000L)
.withPortBindings(FixtureEndpoint.publishedBinding(5432)))
@@ -143,6 +154,38 @@ final class PostgresGenerationDatabase implements AutoCloseable
}
}
/***************************************************************************
** Change one reloadable server setting of an unpinned engine (ALTER SYSTEM and
** a configuration reload), then wait until a new session actually reports it.
** Fixed fixture names and values only, never application input.
***************************************************************************/
void system(String name, String value) throws Exception
{
ConnectionManager.closeConnectionProviders();
try(var connection = DriverManager.getConnection(server + "bootstrap" + OPTIONS, "postgres", rootPassword); var statement = connection.createStatement())
{
statement.execute("ALTER SYSTEM SET " + name + " = '" + value + "'");
statement.execute("SELECT pg_reload_conf()");
}
long deadline = System.nanoTime() + java.time.Duration.ofSeconds(30).toNanos();
while(true)
{
try(var connection = DriverManager.getConnection(server + "bootstrap" + OPTIONS, "postgres", rootPassword); var statement = connection.createStatement();
var row = statement.executeQuery("SELECT current_setting('" + name + "')"))
{
if(row.next() && value.equals(row.getString(1)))
{
return;
}
}
if(System.nanoTime() >= deadline)
{
throw new IllegalStateException("PostgreSQL fixture did not apply " + name);
}
Thread.sleep(100);
}
}
/***************************************************************************
** Cold server restart retains only this container's data layer (committed
** data must survive; Docker may publish a different loopback port).
@@ -41,6 +41,9 @@ class PostgresGenerationQueueTest extends GenerationQueueJdbcTest
{
private static PostgresGenerationDatabase postgres;
// The engine the fixture calls of the running test address: the shared pinned one unless that test started its own.
private PostgresGenerationDatabase current;
/***********************************************************************
** One exact disposable engine per suite; all test cases use new databases.
***********************************************************************/
@@ -68,7 +71,7 @@ class PostgresGenerationQueueTest extends GenerationQueueJdbcTest
@Override
protected RDBMSBackendMetaData database(String name, String defaultUrl) throws Exception
{
return (postgres.database(name));
return (engine().database(name));
}
/***********************************************************************
@@ -77,7 +80,7 @@ class PostgresGenerationQueueTest extends GenerationQueueJdbcTest
@Override
protected Connection connection() throws Exception
{
return (postgres.connection());
return (engine().connection());
}
/***********************************************************************
@@ -86,7 +89,15 @@ class PostgresGenerationQueueTest extends GenerationQueueJdbcTest
@Override
protected void sql(String source) throws Exception
{
postgres.sql(source);
engine().sql(source);
}
/***********************************************************************
** The shared pinned engine, or the unpinned one a single test started.
***********************************************************************/
private PostgresGenerationDatabase engine()
{
return (current == null ? postgres : current);
}
/***********************************************************************
@@ -178,6 +189,86 @@ class PostgresGenerationQueueTest extends GenerationQueueJdbcTest
assertThat(queue.status(enqueue("restored"), -1, 10).items()).hasSize(2);
}
/***************************************************************************
** Amazon Aurora PostgreSQL always runs with full_page_writes off (its storage
** layer does not write torn pages), so that one setting is accepted there and
** nowhere else. The engine is a real PostgreSQL 17 whose settings are really
** changed. Only Aurora itself is simulated: a function named aurora_version()
** is created in the test database, so the same to_regprocedure detection that
** is true on Aurora resolves here.
***************************************************************************/
@Test
void fullPageWritesOffIsAcceptedOnlyOnAuroraAndNothingElseIsRelaxed() throws Exception
{
current = new PostgresGenerationDatabase(false);
try
{
// Rebuild the instance, schema and definitions on the unpinned engine (durable defaults, so this qualifies).
cleanup();
setup();
assertThat(queue.advance(enqueue("stock-durable")).state()).isEqualTo("GENERATED");
// Stock PostgreSQL with full_page_writes off is refused, and the refusal states the rule.
current.system("full_page_writes", "off");
assertThatThrownBy(() -> enqueue("stock-torn-pages")).isInstanceOf(QException.class)
.hasMessageContaining("full-page writes (except on Amazon Aurora PostgreSQL)").hasMessageContaining("not satisfied: synchronous commit");
// The same server, now resolving aurora_version(), is accepted and work really commits.
sql("CREATE FUNCTION aurora_version() RETURNS text LANGUAGE sql IMMUTABLE AS $$ SELECT '17.9'::text $$");
ConnectionManager.closeConnectionProviders();
assertThat(queue.advance(enqueue("aurora")).state()).isEqualTo("GENERATED");
// Aurora relaxes nothing else: fsync off is refused.
current.system("fsync", "off");
assertThatThrownBy(() -> enqueue("aurora-no-fsync")).isInstanceOf(QException.class).hasMessageContaining("not satisfied: synchronous commit");
current.system("fsync", "on");
// Aurora relaxes nothing else: asynchronous commit is refused.
sql("ALTER ROLE generation_app SET synchronous_commit='off'");
ConnectionManager.closeConnectionProviders();
assertThatThrownBy(() -> enqueue("aurora-async-commit")).isInstanceOf(QException.class).hasMessageContaining("not satisfied: synchronous commit");
sql("ALTER ROLE generation_app SET synchronous_commit='on'");
ConnectionManager.closeConnectionProviders();
assertThat(enqueue("aurora-restored")).isNotNull();
// Without the function the server is stock PostgreSQL again and is refused again.
sql("DROP FUNCTION aurora_version()");
ConnectionManager.closeConnectionProviders();
assertThatThrownBy(() -> enqueue("stock-again")).isInstanceOf(QException.class).hasMessageContaining("not satisfied: synchronous commit");
}
finally
{
ConnectionManager.closeConnectionProviders();
current.close();
current = null;
}
}
/***************************************************************************
** The durability decision itself, for the combinations the live case does
** not reach: no server and nothing simulated, only the values of one row.
***************************************************************************/
@Test
void durabilityRuleAcceptsOnlyTheAuroraFullPageWritesException()
{
assertThat(GenerationPostgres.durable("on", "on", "on", false)).isTrue();
assertThat(GenerationPostgres.durable("remote_apply", "on", "on", false)).isTrue();
assertThat(GenerationPostgres.durable("on", "on", "on", true)).isTrue();
assertThat(GenerationPostgres.durable("on", "on", "off", true)).isTrue();
assertThat(GenerationPostgres.durable("remote_apply", "on", "off", true)).isTrue();
assertThat(GenerationPostgres.durable("on", "on", "off", false)).isFalse();
assertThat(GenerationPostgres.durable("on", "off", "on", false)).isFalse();
assertThat(GenerationPostgres.durable("on", "off", "off", true)).isFalse();
for(String weak : List.of("off", "local", "remote_write"))
{
assertThat(GenerationPostgres.durable(weak, "on", "on", false)).isFalse();
assertThat(GenerationPostgres.durable(weak, "on", "off", true)).isFalse();
}
assertThat(GenerationPostgres.durable(null, "on", "on", true)).isFalse();
assertThat(GenerationPostgres.durable("on", null, "on", true)).isFalse();
assertThat(GenerationPostgres.durable("on", "on", null, true)).isFalse();
}
/***************************************************************************
** The native identity must be the postgres module on a real PostgreSQL17 server, not a vendor label.
***************************************************************************/
+2 -2
View File
@@ -5,7 +5,7 @@
no -Drevision, so a module that declared its own revision would stay at the SNAPSHOT. The root takes qbit-build-parent 2.1.1 (the version foundation already used);
modules that had no parent now inherit its defaults and this Apache-2.0 license (the vendored OpenHTML module keeps its explicit LGPL-2.1+; bridges/scripting keeps
its previously inherited AGPL-3.0 explicitly). Each module still keeps its own dependency boundary and builds on its own through its relative parent. -->
<parent><groupId>com.kingsrook</groupId><artifactId>qbit-build-parent</artifactId><version>2.1.1</version><relativePath/></parent>
<parent><groupId>com.kingsrook</groupId><artifactId>qbit-build-parent</artifactId><version>2.2.1</version><relativePath/></parent>
<groupId>com.kingsrook.qbits</groupId>
<artifactId>qbit-document-generation-reactor</artifactId>
<version>${revision}</version>
@@ -14,7 +14,7 @@
<url>https://github.com/Kingsrook/qbit-bom</url>
<scm><connection>scm:git:https://github.com/Kingsrook/qbit-bom.git</connection><developerConnection>scm:git:ssh://git@github.com/Kingsrook/qbit-bom.git</developerConnection><url>https://github.com/Kingsrook/qbit-bom</url><tag>HEAD</tag></scm>
<licenses><license><name>Apache License, Version 2.0</name><url>https://www.apache.org/licenses/LICENSE-2.0</url><distribution>repo</distribution></license></licenses>
<properties><revision>0.1.0-SNAPSHOT</revision><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><enforcer.skip>true</enforcer.skip></properties>
<properties><revision>1.4.0-SNAPSHOT</revision><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><enforcer.skip>true</enforcer.skip><qqq.version>4.1.0-RC.26</qqq.version></properties>
<!-- Every module of the repository, so one release builds and publishes every GAV. Maven orders the reactor by dependency (the patched OpenHTML
module before the renderer; foundation before the bridges). Build a subset with -pl <module> -am. -->
<modules>
+16 -15
View File
@@ -10,18 +10,18 @@ COPY Dockerfile ./
RUN apt-get update && apt-get install -y --no-install-recommends gcc g++ libc6-dev make cmake patch \
&& sh ./build-zint.sh
# The renderer's version comes from its parent, the repository reactor POM (../pom.xml). Build from this directory with that file as a named BuildKit context:
# docker build --build-context reactor=<repository root> ... (the reactor POM is copied to /pom.xml, where ../pom.xml resolves for /renderer/pom.xml)
FROM ${BUILD_IMAGE} AS build
COPY --from=reactor pom.xml /pom.xml
WORKDIR /renderer
COPY vendor/openhtml-1.1.105-qqq-p1 ./vendor/openhtml-1.1.105-qqq-p1
RUN sh ./vendor/openhtml-1.1.105-qqq-p1/build-openhtml.sh /renderer/openhtml-build /renderer/.m2
COPY pom.xml ./
COPY LICENSE NOTICE ./
COPY checkstyle ./checkstyle
COPY src ./src
RUN mvn -B -Dmaven.repo.local=/renderer/.m2 verify
# The engine and its libraries are the RELEASED artifacts, resolved at the exact RENDERER_VERSION from the QRun-IO
# Gitea Maven registry and Maven Central (Maven checks each registry checksum). No SNAPSHOT, no local build and no
# local-cache coordinate: the version is a required build argument with no default.
# docker build --build-arg BUILD_IMAGE=... --build-arg JAVA_IMAGE=... --build-arg RENDERER_VERSION=1.0.0 renderer/
FROM ${BUILD_IMAGE} AS release
ARG RENDERER_VERSION
WORKDIR /release
COPY release-image/pom.xml ./pom.xml
RUN test -n "${RENDERER_VERSION}" && case "${RENDERER_VERSION}" in *SNAPSHOT*|*forge*) echo "RENDERER_VERSION must be a released version" >&2; exit 1;; esac \
&& mvn -B -Drenderer.version="${RENDERER_VERSION}" -Dmaven.repo.local=/release/.m2 package \
&& mv "target/lib/qbit-document-renderer-${RENDERER_VERSION}.jar" target/renderer.jar \
&& test -f target/renderer.jar && ls target/lib
FROM ${JAVA_IMAGE}
# Reviewed runtime family: Temurin 21.0.13+9 on Ubuntu noble. The approved digest
@@ -37,8 +37,9 @@ RUN . /etc/os-release \
WORKDIR /renderer
COPY --from=zint-build /zint-build/build/frontend/zint /usr/local/bin/zint
COPY --from=zint-build /zint-build/corresponding-source /usr/share/doc/qqq-zint
COPY --from=build /renderer/openhtml-build/corresponding-source /usr/share/doc/qqq-openhtml
COPY --from=build /renderer/target/qbit-document-renderer-0.1.0-SNAPSHOT.jar ./renderer.jar
COPY --from=build /renderer/target/lib ./lib
# LGPL corresponding source for the vendored OpenHTML module that the released openhtmltopdf-pdfbox jar was built from.
COPY vendor/openhtml-1.1.105-qqq-p1 /usr/share/doc/qqq-openhtml
COPY --from=release /release/target/renderer.jar ./renderer.jar
COPY --from=release /release/target/lib ./lib
USER 65532:65532
ENTRYPOINT ["java", "-Xmx256m", "-XX:ActiveProcessorCount=1", "-Djava.awt.headless=true", "-Dpdfbox.fontcache=/tmp", "-jar", "/renderer/renderer.jar"]
+120
View File
@@ -0,0 +1,120 @@
# Renderer container images: build from a released version and publish
Two images, built in this order. Both are built from **released** Maven artifacts (QRun-IO Gitea registry
`https://git.kof22.com/api/packages/QRun-IO/maven` and Maven Central); there is no SNAPSHOT, local-cache or reactor
input. The version is a required build argument with no default, and the Dockerfiles refuse a `SNAPSHOT` or `forge`
version.
| Image | Dockerfile (context) | Version argument | Contents |
|---|---|---|---|
| `ghcr.io/gg-engineering/qqq-document-renderer` | `renderer/Dockerfile` (`renderer/`) | `RENDERER_VERSION` (`qbit-document-renderer`) | engine, its libraries, patched Zint built from `renderer/vendor`, Temurin runtime |
| `ghcr.io/gg-engineering/qqq-remote-renderer` | `bridges/remote-renderer/Dockerfile` (`bridges/remote-renderer/`) | `TRANSPORT_VERSION` (`qqq-remote-renderer-transport`), `RENDERER_IMAGE` | the mTLS service, on top of the engine image |
The service image is the one Forge deploys. The adapter (`qbit-document-generation-remote-renderer`) is a client-side
library and is not in either image.
## Version choice
`1.0.0` is the released, non-RC version and the one `gg-forge` pins for the document-generation family. `1.1.0-RC.1`
contains the same `renderer/` and `bridges/remote-renderer/` sources as `1.0.0` (only POM/CI alignment differs), so it
adds nothing for the renderer and is a release candidate. Build the version the host library pins.
## Reviewed base images
`BUILD_IMAGE` and `JAVA_IMAGE` must be immutable digests. `JAVA_IMAGE` is the reviewed runtime in `DEPLOYMENT.md`.
`BUILD_IMAGE` only resolves artifacts; no layer of it reaches the final image.
```sh
BUILD_IMAGE=maven@sha256:99e61abcff91a9b1333463bd8451fb18495d6eba9250ac66a338b518f8278320 # maven:3.9-eclipse-temurin-21
JAVA_IMAGE=eclipse-temurin@sha256:db724162e4ff5f71fa0a2815026314d9e5e0e9a139f49be1d4717e6930e93341 # 21.0.13_9-jre-noble (amd64)
VERSION=1.0.0
```
## Build, smoke, push
Disable BuildKit provenance and SBOM attestations so each tag is a single OCI image manifest (the digest the chart pins).
```sh
docker build --provenance=false --sbom=false --build-arg BUILD_IMAGE=$BUILD_IMAGE --build-arg JAVA_IMAGE=$JAVA_IMAGE --build-arg RENDERER_VERSION=$VERSION -t qqq-document-renderer:$VERSION renderer
docker build --provenance=false --sbom=false --build-arg BUILD_IMAGE=$BUILD_IMAGE --build-arg RENDERER_IMAGE=qqq-document-renderer:$VERSION --build-arg TRANSPORT_VERSION=$VERSION -t qqq-remote-renderer:$VERSION bridges/remote-renderer
# Optional scan of the final image
docker save qqq-remote-renderer:$VERSION -o svc.tar
docker run --rm -v "$PWD/svc.tar:/svc.tar:ro" aquasec/trivy:latest image --input /svc.tar --severity HIGH,CRITICAL --scanners vuln
# Publish (docker must be logged in to ghcr.io with a token that has write:packages)
for n in qqq-document-renderer qqq-remote-renderer; do
docker tag $n:$VERSION ghcr.io/gg-engineering/$n:$VERSION
docker push ghcr.io/gg-engineering/$n:$VERSION # prints the manifest digest
done
```
New GHCR packages are private. Package access is per package and is managed in the GitHub UI (Package settings,
Manage access); the REST API cannot grant it. The credential behind `ghcr-pull-secret` must be able to read both packages.
## Pins for gg-forge-cd
`sh renderer/image-pins.sh ghcr.io/gg-engineering/qqq-remote-renderer@sha256:<manifest digest>` pulls the published
image and prints the `echoRenderer.*` image, classpath and file-hash pins from the image itself. `imageId` is
`docker image inspect --format '{{.Id}}'`; with the containerd image store this equals the manifest digest, otherwise
it is the config digest. Whatever value is chosen must be the same on Forge and the service.
Values for `1.0.0` as published (the java, zint and patchset hashes match the 2026-11-05 local qualification; the
engine JAR and the OpenHTML JAR differ because they are the released artifacts):
```yaml
echoRenderer:
image:
repository: ghcr.io/gg-engineering/qqq-remote-renderer
digest: sha256:4152a4253e543068bedfd438775ed5327807bdddf679bc5a8e6eab26855e09ea
imageId: sha256:4152a4253e543068bedfd438775ed5327807bdddf679bc5a8e6eab26855e09ea
classpath:
- /renderer/renderer.jar
- /renderer/lib/commons-logging-1.4.0.jar
- /renderer/lib/fontbox-3.0.8.jar
- /renderer/lib/freemarker-2.3.35.jar
- /renderer/lib/graphics2d-3.0.1.jar
- /renderer/lib/jackson-annotations-2.22.jar
- /renderer/lib/jackson-core-2.22.4.jar
- /renderer/lib/jackson-databind-2.22.4.jar
- /renderer/lib/jsoup-1.23.2.jar
- /renderer/lib/okapibarcode-0.5.7.jar
- /renderer/lib/openhtmltopdf-core-1.1.105.jar
- /renderer/lib/openhtmltopdf-pdfbox-1.0.0.jar
- /renderer/lib/pdfbox-3.0.8.jar
- /renderer/lib/pdfbox-io-3.0.8.jar
- /renderer/lib/slf4j-api-2.0.17.jar
- /renderer/lib/slf4j-nop-2.0.17.jar
- /renderer/lib/xmpbox-3.0.8.jar
fileHashes:
/opt/java/openjdk/bin/java: 7afc28801f744050aeba646b20e7314265279f76f9d0273b21a2b66411e1bf6d
/renderer/renderer.jar: 0061e1bce631e468b0d7d08735bb764ba2b6d85260c79c144beabad7ffbc19db
/usr/local/bin/zint: ea8dc40c917abd7e8ccc824ef0adafed245a2425abbdfac6a30faeb8e4642233
/usr/share/doc/qqq-zint/patchset-id.txt: a287946580d5eeb8691ac1d98d6a7f50bf22dcc5dc0ef039d7033e76e15c52d2
/renderer/lib/commons-logging-1.4.0.jar: d175dbd751dd782a63bde28c7a039520e971f25e84b79c19b8435edc3603e0dc
/renderer/lib/fontbox-3.0.8.jar: a1915c24e3edbe0ecec93896dfbf6d41427810b663ade97bd4e8bae86ec3fdab
/renderer/lib/freemarker-2.3.35.jar: 0fac87dddd78f1223139e8ef88e819c7f483c0a3835cdf5982ad5e4576d1d896
/renderer/lib/graphics2d-3.0.1.jar: 4849aaa6f536d75dea1b835a71086702216787036f42716adfad438c6e7928c0
/renderer/lib/jackson-annotations-2.22.jar: 21ddb598807d3a51a876704eb979d9296e1c6a6f47ab1826ff88c6d6a127a2d0
/renderer/lib/jackson-core-2.22.4.jar: 2d30c375cddea9657a1e1ef97547a2694b663b355facb4de53a097782b312d1a
/renderer/lib/jackson-databind-2.22.4.jar: d4cbf8947087f305869826801913abdc50735a1fab2074a1993e7bc67ca974a3
/renderer/lib/jsoup-1.23.2.jar: e9d8c856856680427f096d156f02a414289985c930998fcb519ce5be39b42003
/renderer/lib/okapibarcode-0.5.7.jar: f33b7d17b7040aa1d9c23b1f3bb274e492b786c340b02d9557bcc027ac874355
/renderer/lib/openhtmltopdf-core-1.1.105.jar: d5951a8407c3be41108698066e6d67cf952e526be39d4e49b0b9fbcee761de7d
/renderer/lib/openhtmltopdf-pdfbox-1.0.0.jar: ec2865bfa45513e63adaa1a3c6387eb6bbd481e6b23f4d68a33cfedfc753cfe8
/renderer/lib/pdfbox-3.0.8.jar: 97647cfbde61ebcfc06b4cf8c9b0ffcaaee073396eceb4a7f6836a9b9128903c
/renderer/lib/pdfbox-io-3.0.8.jar: 36a0e04001010b4c764857817412b96339930b19755e728959805cc0352061b2
/renderer/lib/slf4j-api-2.0.17.jar: 7b751d952061954d5abfed7181c1f645d336091b679891591d63329c622eb832
/renderer/lib/slf4j-nop-2.0.17.jar: 3716f83649ec66161a2edefd4f49df34d1dd1c51cdcf941996c6987260f0a829
/renderer/lib/xmpbox-3.0.8.jar: f044d2e316e57714a0adf272ad8abf4a863b8978fd17dc5b86dcfa8135469b10
```
Not covered by these pins: `identity.*`, `certificateNamespaces`, TLS identities, and chart switches.
## What was and was not verified (1.0.0 build)
* Verified: `renderer` (174) and transport (19) tests and Checkstyle pass on this source (same sources as the `v1.0.0` tag);
both images build; the service image runs read-only, non-root, all capabilities dropped, and served CATALOG, PDF
(Code128, GS1-128, Zint DotCode) and ZPL renders over a real mutual-TLS handshake with ephemeral certificates.
* Not verified: no cluster run, no handshake against Forge, no gVisor or hostile-input qualification (the service is
`TRUSTED_LAB` only), no byte-for-byte rebuild of the OpenHTML jar from `vendor/`.
+25
View File
@@ -0,0 +1,25 @@
#!/bin/sh
# Print the gg-forge-cd echoRenderer.* pins (non-secret) for a PUBLISHED service image, read from the image itself:
# sh renderer/image-pins.sh ghcr.io/gg-engineering/qqq-remote-renderer@sha256:<manifest digest>
# The classpath is /renderer/renderer.jar followed by /renderer/lib/*.jar in byte order; hashes are SHA256 of the files in the image.
set -eu
image=${1:?image reference by digest required}
case "$image" in *@sha256:*) ;; *) echo 'Reference the image by @sha256: digest' >&2; exit 2;; esac
repository=${image%@*}
digest=${image#*@}
docker pull "$image" >/dev/null
image_id=$(docker image inspect "$image" --format '{{.Id}}')
echo "echoRenderer:"
echo " image:"
echo " repository: $repository"
echo " digest: $digest"
echo " imageId: $image_id"
docker run --rm --pull never --network none --user 65532:65532 --entrypoint sh "$image" -c '
set -eu
echo " classpath:"
echo " - /renderer/renderer.jar"
for f in $(LC_ALL=C ls /renderer/lib/*.jar); do echo " - $f"; done
echo " fileHashes:"
for f in /opt/java/openjdk/bin/java /renderer/renderer.jar /usr/local/bin/zint /usr/share/doc/qqq-zint/patchset-id.txt $(LC_ALL=C ls /renderer/lib/*.jar); do
printf " %s: %s\n" "$f" "$(sha256sum "$f" | cut -d" " -f1)"
done'
+1 -1
View File
@@ -32,7 +32,7 @@
</execution></executions>
</plugin>
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>3.14.1</version><configuration><showWarnings>true</showWarnings><failOnWarning>true</failOnWarning><compilerArgs><arg>-XDcompilePolicy=simple</arg><arg>--should-stop=ifError=FLOW</arg><arg>-XDaddTypeAnnotationsToSymbol=true</arg><arg>-Xplugin:ErrorProne -Xep:UnnecessaryParentheses:OFF -Xep:NotJavadoc:OFF</arg><arg>-Xlint:all</arg><arg>-Werror</arg></compilerArgs><annotationProcessorPaths><path><groupId>com.google.errorprone</groupId><artifactId>error_prone_core</artifactId><version>2.49.0</version></path></annotationProcessorPaths></configuration></plugin>
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-surefire-plugin</artifactId><version>3.5.4</version><configuration><dependenciesToScan combine.self="override"/><systemPropertyVariables><java.awt.headless>true</java.awt.headless><pdfbox.fontcache>${project.build.directory}</pdfbox.fontcache></systemPropertyVariables></configuration></plugin>
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-surefire-plugin</artifactId><version>3.5.4</version><configuration><dependenciesToScan combine.self="override"/><systemPropertyVariables><kof22.provider.version>${revision}</kof22.provider.version><java.awt.headless>true</java.awt.headless><pdfbox.fontcache>${project.build.directory}</pdfbox.fontcache></systemPropertyVariables></configuration></plugin>
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-dependency-plugin</artifactId><version>3.9.0</version><executions><execution><id>runtime-libraries</id><phase>package</phase><goals><goal>copy-dependencies</goal></goals><configuration><includeScope>runtime</includeScope><outputDirectory>${project.build.directory}/lib</outputDirectory></configuration></execution></executions></plugin>
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-jar-plugin</artifactId><version>3.4.2</version><configuration><archive><manifest><mainClass>com.kingsrook.qbits.renderer.RendererMain</mainClass><addClasspath>true</addClasspath><classpathPrefix>lib/</classpathPrefix></manifest></archive></configuration></plugin>
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-checkstyle-plugin</artifactId><version>3.3.1</version><dependencies><dependency><groupId>com.puppycrawl.tools</groupId><artifactId>checkstyle</artifactId><version>10.12.6</version></dependency></dependencies><configuration><configLocation>${project.basedir}/checkstyle/config.xml</configLocation><propertyExpansion>checkstyle.reactor.root=${project.basedir}</propertyExpansion><includeTestSourceDirectory>true</includeTestSourceDirectory><consoleOutput>true</consoleOutput><violationSeverity>warning</violationSeverity><failOnViolation>true</failOnViolation></configuration><executions><execution><phase>validate</phase><goals><goal>check</goal></goals></execution></executions></plugin>
+21
View File
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Image-assembly project, not a published module. It resolves the RELEASED renderer engine and its runtime
libraries from the QRun-IO Gitea Maven registry (and Maven Central) at one exact version, so the renderer image
carries release bytes and not a local build. renderer.version has no default: pass -Drenderer.version=<released>. -->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.kingsrook.qbits</groupId><artifactId>qbit-document-renderer-image</artifactId><version>0</version><packaging>pom</packaging>
<repositories>
<repository><id>central</id><url>https://repo.maven.apache.org/maven2</url><snapshots><enabled>false</enabled></snapshots></repository>
<repository><id>qrun-io</id><url>https://git.kof22.com/api/packages/QRun-IO/maven</url><snapshots><enabled>false</enabled></snapshots><releases><checksumPolicy>fail</checksumPolicy></releases></repository>
</repositories>
<dependencies>
<dependency><groupId>com.kingsrook.qbits</groupId><artifactId>qbit-document-renderer</artifactId><version>${renderer.version}</version></dependency>
</dependencies>
<build><plugins>
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-dependency-plugin</artifactId><version>3.9.0</version>
<executions><execution><id>release-libraries</id><phase>package</phase><goals><goal>copy-dependencies</goal></goals>
<configuration><includeScope>runtime</includeScope><outputDirectory>${project.build.directory}/lib</outputDirectory></configuration>
</execution></executions></plugin>
</plugins></build>
</project>
@@ -51,6 +51,15 @@ public final class BarcodeCatalog
{
private static final Map<String, Supplier<Symbol>> FACTORIES = factories();
/*******************************************************************************
** Encoders whose geometry is fixed by their specification (matrix symbols and
** postal four-state codes): barHeight changes nothing for them, so it is
** neither advertised nor accepted. BarcodeDeclaredCapabilityTest proves this
** set is exactly the encoders on which barHeight has no effect.
*******************************************************************************/
private static final java.util.Set<String> FIXED_GEOMETRY = java.util.Set.of("australia-post", "aztec", "aztec-rune", "code-one", "code16k",
"code49", "datamatrix", "grid-matrix", "japan-post", "kix", "maxicode", "micro-qr", "qr", "royal-mail", "swiss-qr", "upn-qr");
/*******************************************************************************
** Utility class.
@@ -142,7 +151,14 @@ public final class BarcodeCatalog
{
Symbol symbol = FACTORIES.get(id).get();
Map<String, Object> options = new LinkedHashMap<>();
options.put("barHeight", Map.of("type", "integer", "minimum", 1, "maximum", 1000));
if("gs1-databar".equals(id))
{
options.put("barHeight", Map.of("type", "integer", "minimum", 1, "maximum", 1000, "notWithMode", "STACKED"));
}
else if(!FIXED_GEOMETRY.contains(id))
{
options.put("barHeight", Map.of("type", "integer", "minimum", 1, "maximum", 1000));
}
if(symbol instanceof Coop2Of5)
{
options.put("checkDigit", Map.of("type", "string", "values", List.of("NONE", "ADD", "VALIDATE"), "default", "NONE"));
@@ -380,6 +396,10 @@ public final class BarcodeCatalog
}
Symbol symbol = factory.get();
structuredAppend(type, symbol, options);
if(options.has("barHeight") && (FIXED_GEOMETRY.contains(type) || ("gs1-databar".equals(type) && options.has("mode") && "STACKED".equals(options.get("mode").asText()))))
{
throw new IllegalArgumentException("UNSUPPORTED_BARCODE_OPTION");
}
for(Map.Entry<String, JsonNode> field : options.properties())
{
if(List.of("qr", "datamatrix", "aztec").contains(type) && field.getKey().startsWith("structuredAppend"))
@@ -96,6 +96,36 @@ public final class PdfRenderer
public static void validate(RenderInput input) throws Exception
{
template(input.source);
for(Element element : Jsoup.parse(input.source).select("qqq-barcode"))
{
refuseUnsupportedOptions(element.attr("type"), element.attr("options"));
}
}
/*******************************************************************************
** Literal barcode options are checked against the catalog before any data
** exists, so an option the encoder cannot honour is refused at validation as
** its own closed code. Templated type or options are checked when rendered,
** and any other problem is left for the render to report.
*******************************************************************************/
private static void refuseUnsupportedOptions(String type, String options)
{
if(options.isBlank() || type.contains("${") || type.contains("<#") || options.contains("${") || options.contains("<#"))
{
return;
}
try
{
BarcodeCatalog.create(type, Wire.JSON.readTree(options));
}
catch(Exception exception)
{
if(exception instanceof IllegalArgumentException refusal && "UNSUPPORTED_BARCODE_OPTION".equals(refusal.getMessage()))
{
throw refusal;
}
}
}
@@ -103,7 +103,8 @@ public final class RendererMain
"BARCODE_FIT_TOO_SMALL", "BARCODE_FIT_UNSUPPORTED", "BARCODE_FIT_BOX_OUTSIDE_STOCK",
"BARCODE_FIT_GEOMETRY_MISMATCH", "INVALID_BARCODE_FIT_PROFILE", "INVALID_BARCODE_FIT_REQUEST",
"BARCODE_SYMBOLOGY_NOT_ALLOWED", "SYMBOLOGY_ALLOWLIST_REQUIRED", "SCANNER_BARCODE_PROFILE_UNSUPPORTED",
"SCANNER_BARCODE_PAYLOAD_UNSUPPORTED", "SCANNER_BARCODE_CHECKSUM_REQUIRED", "SCANNER_BARCODE_CHECKSUM_MISMATCH").contains(message))
"SCANNER_BARCODE_PAYLOAD_UNSUPPORTED", "SCANNER_BARCODE_CHECKSUM_REQUIRED", "SCANNER_BARCODE_CHECKSUM_MISMATCH",
"UNSUPPORTED_BARCODE_OPTION").contains(message))
{
return (message);
}
@@ -84,16 +84,32 @@ public final class BackgroundImageQualification
cases += 2;
}
return (Map.of("cases", cases, "surfaces", List.of("element", "pseudo", "page"), "exactRgbAlphaAndDimensions", true,
"bothAliasPaintOrders", true, "provider", "com.kingsrook.thirdparty:openhtmltopdf-pdfbox:1.1.105-qqq-p1",
"bothAliasPaintOrders", true, "provider", "com.kingsrook.thirdparty:openhtmltopdf-pdfbox:" + providerVersion(),
"qualification", "trusted-product-fixtures-only"));
}
/***************************************************************************
** The patched OpenHTML provider is published at the family version of this
** reactor, which surefire passes in; there is no second literal to keep in sync.
***************************************************************************/
private static String providerVersion()
{
String version = System.getProperty("kof22.provider.version");
if(version == null || version.isBlank())
{
throw new IllegalArgumentException("PDF_PROVIDER_VERSION_PROPERTY_MISSING");
}
return (version);
}
/***************************************************************************
** One provider class must come from the private patched GAV, with unchanged official companion versions.
***************************************************************************/
static void verifyProvider() throws Exception
{
verifyArtifact(com.openhtmltopdf.pdfboxout.PdfBoxFastOutputDevice.class, "com.kingsrook.thirdparty", "openhtmltopdf-pdfbox", "1.1.105-qqq-p1");
verifyArtifact(com.openhtmltopdf.pdfboxout.PdfBoxFastOutputDevice.class, "com.kingsrook.thirdparty", "openhtmltopdf-pdfbox", providerVersion());
verifyArtifact(com.openhtmltopdf.css.parser.CSSParser.class, "io.github.openhtmltopdf", "openhtmltopdf-core", "1.1.105");
verifyArtifact(org.apache.pdfbox.pdmodel.PDDocument.class, "org.apache.pdfbox", "pdfbox", "3.0.8");
}