Compare commits

...
Author SHA1 Message Date
james.maesandClaude Fable 5.2 65344f5981 feat(renderer): align a fitted ZPL barcode inside its fit box; justified font 0 text blocks
build / maven (push) Successful in 11m47s
maven / build (push) Successful in 11m47s
A fitted barcode's size depends on its data, so a scene cannot centre it by
position: until now the fitted raster always sat at the box origin and an
"align" on a barcode element was read and ignored.

- barcode with fit: optional align (left, center, right) and valign (top,
  middle, bottom) place the fitted raster inside the fit box. Absent, the
  raster stays at the origin, so published scenes print as before.
- text: optional align, blockWidth and maxLines make a justified block. Each
  line is one font 0 field in a one-line ^FB block; the renderer breaks lines
  itself (at a space, or before an opening parenthesis) so the printer never
  wraps or hyphenates. Text without these fields is unchanged.

Renderer module verify: 180 tests (6 new in ZplAlignmentTest), 0 Checkstyle
violations. Printer-side ^FB justification is not yet qualified on hardware.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-11 11:07:55 -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
27 changed files with 925 additions and 64 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.
+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/`.
+7 -1
View File
@@ -84,7 +84,13 @@ ZPL sources are JSON scenes with schema `qqq-zpl-scene/1`, **not raw ZPL templat
}
```
Text/barcodes require exactly one literal `value` or JSON Pointer `valuePath`; only scalar values are accepted. Barcode `options` is the same typed object as PDF. Boxes provide outlines or lines. Images print at their supplied pixel dimensions; preprocess retained assets to the intended printer resolution. Existing `text` uses the printer's font 0 with explicit width/height. The additive `rasterText` element renders dynamic record text using one exact retained TrueType face, without installing fonts on the printer or host:
Text/barcodes require exactly one literal `value` or JSON Pointer `valuePath`; only scalar values are accepted. Barcode `options` is the same typed object as PDF. Boxes provide outlines or lines. Images print at their supplied pixel dimensions; preprocess retained assets to the intended printer resolution. Existing `text` uses the printer's font 0 with explicit width/height.
A barcode with a `fit` box may carry `align` (`left`, `center`, `right`) and `valign` (`top`, `middle`, `bottom`). The fitted size depends on the data, so the renderer places the fitted raster inside the box; without them it stays at the box origin, as before. Either field without `fit`, or any other word, refuses with `INVALID_BARCODE_FIT_PROFILE`. To centre a barcode on a label, make the box symmetric about the label's centre.
A `text` element becomes a justified block when it has any of `align` (`left`, `center`, `right`), `blockWidth` (dots from `x`; default the rest of the stock) or `maxLines` (1 to 8, default 1). Each line is its own font 0 field inside a one-line `^FB` field block that the printer justifies. The renderer breaks the lines itself, so the printer never wraps or hyphenates: a line holds at most `blockWidth / width` characters, and ends at the last space, or just before the last opening parenthesis, that fits. Lines are `height + ceil(height / 8)` dots apart. Text needing more than `maxLines` lines refuses with `ELEMENT_OUTSIDE_STOCK`. Text without these fields is the single field it always was. Justification by the printer's own font has not been qualified on hardware; print one label on the target printer before relying on it.
The additive `rasterText` element renders dynamic record text using one exact retained TrueType face, without installing fonts on the printer or host:
```json
{"type":"rasterText","x":20,"y":250,"fontAsset":"headingItalic","fontSize":32,"width":500,"height":120,"valuePath":"/record/customerName","align":"left","rotation":0,"threshold":128}
+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);
}
@@ -101,6 +101,7 @@ public final class ZplRenderer
{
Wire.integer(element.get("width"), 1, 999);
Wire.integer(element.get("height"), 1, 999);
textBlock(element, x, input.stock.dots(input.stock.widthMicrons()));
}
else if("rasterText".equals(type))
{
@@ -134,6 +135,8 @@ public final class ZplRenderer
else if("barcode".equals(type))
{
String symbology = symbology(element, input, resolveBinary);
alignment(element, "align", "left", "center", "right");
alignment(element, "valign", "top", "middle", "bottom");
if(element.has("fit"))
{
fitRequest(element, input, symbology, false);
@@ -257,6 +260,115 @@ public final class ZplRenderer
}
/*******************************************************************************
** Where a fitted barcode sits inside its fit box: 0 at the box origin, 1 in
** the middle, 2 against the far edge. The fitted size depends on the data, so
** only the renderer can place it; without a fit box there is nothing to align in.
*******************************************************************************/
private static int alignment(JsonNode element, String field, String near, String middle, String far)
{
if(!element.has(field))
{
return (0);
}
int index = List.of(near, middle, far).indexOf(Wire.text(element.get(field), 16));
if(index < 0 || !element.has("fit"))
{
throw new IllegalArgumentException("INVALID_BARCODE_FIT_PROFILE");
}
return (index);
}
/*******************************************************************************
** Printer font 0 text justified inside a block of dots, on at most maximumLines lines.
*******************************************************************************/
private record TextBlock(char justification, int width, int maximumLines)
{
}
/*******************************************************************************
** Text without align, blockWidth or maxLines keeps its original single field.
** A block starts at x and must end on the stock; its width defaults to the rest
** of the stock.
*******************************************************************************/
private static TextBlock textBlock(JsonNode element, int x, int pageWidth)
{
if(!element.has("align") && !element.has("blockWidth") && !element.has("maxLines"))
{
return (null);
}
int justification = element.has("align") ? List.of("left", "center", "right").indexOf(Wire.text(element.get("align"), 16)) : 0;
if(justification < 0)
{
throw new IllegalArgumentException("INVALID_ZPL_TEXT");
}
int width = element.has("blockWidth") ? Wire.integer(element.get("blockWidth"), 1, 9999) : pageWidth - x;
if(x + (long) width > pageWidth)
{
throw new IllegalArgumentException("ELEMENT_OUTSIDE_STOCK");
}
return (new TextBlock("LCR".charAt(justification), width, element.has("maxLines") ? Wire.integer(element.get("maxLines"), 1, 8) : 1));
}
/*******************************************************************************
** The renderer breaks block text itself, so the printer never wraps or inserts
** a hyphen: a line holds at most perLine characters (the same font-width bound
** the single field uses) and ends at the last space, or just before the last
** opening parenthesis, that fits, else at the bound. More lines than allowed refuses.
*******************************************************************************/
static List<String> lines(String text, int perLine, int maximumLines)
{
int[] codes = text.codePoints().toArray();
List<String> lines = new java.util.ArrayList<>();
int start = 0;
while(start < codes.length)
{
if(perLine < 1 || lines.size() == maximumLines)
{
throw new IllegalArgumentException("ELEMENT_OUTSIDE_STOCK");
}
int end = Math.min(codes.length, start + perLine);
int next = end;
if(end < codes.length)
{
for(int index = end; index > start; index--)
{
if(codes[index] == ' ' || codes[index] == '(')
{
end = index;
next = codes[index] == ' ' ? index + 1 : index;
break;
}
}
}
lines.add(new String(codes, start, end - start));
start = next;
}
if(lines.isEmpty())
{
throw new IllegalArgumentException("ELEMENT_OUTSIDE_STOCK");
}
return (lines);
}
/*******************************************************************************
** Field data as hexadecimal UTF-8 bytes; no character reaches the printer raw.
*******************************************************************************/
private static String hexadecimal(String text)
{
StringBuilder escaped = new StringBuilder();
for(byte value : text.getBytes(StandardCharsets.UTF_8))
{
escaped.append('_').append(HexFormat.of().withUpperCase().toHexDigits(value));
}
return (escaped.toString());
}
/*******************************************************************************
** An explicit JSON Pointer provides data binding without executable code.
*******************************************************************************/
@@ -322,13 +434,28 @@ public final class ZplRenderer
{
throw new IllegalArgumentException("INVALID_ZPL_TEXT");
}
placement(x, y, (long) fontWidth * text.codePointCount(0, text.length()), fontHeight, width, height);
StringBuilder escaped = new StringBuilder();
for(byte value : text.getBytes(StandardCharsets.UTF_8))
TextBlock block = textBlock(element, x, width);
if(block == null)
{
escaped.append('_').append(HexFormat.of().withUpperCase().toHexDigits(value));
placement(x, y, (long) fontWidth * text.codePointCount(0, text.length()), fontHeight, width, height);
emit(output, "^FO" + x + "," + y + "^A0N," + fontHeight + "," + fontWidth + "^FH_^FD" + hexadecimal(text) + "^FS\n");
}
else
{
//////////////////////////////////////////////////////////////////////////////////////
// One field per line, each a one-line field block that the printer justifies. //
// Inside a field block a backslash starts a command, so a literal one is doubled; //
// centred data ends with the line terminator Zebra asks for to centre exactly. //
//////////////////////////////////////////////////////////////////////////////////////
int line = 0;
for(String part : lines(text, block.width() / fontWidth, block.maximumLines()))
{
int top = y + line++ * (fontHeight + (fontHeight + 7) / 8);
placement(x, top, block.width(), fontHeight, width, height);
emit(output, "^FO" + x + "," + top + "^A0N," + fontHeight + "," + fontWidth + "^FB" + block.width() + ",1,0," + block.justification() + ",0^FH_^FD"
+ hexadecimal(part.replace("\\", "\\\\") + (block.justification() == 'C' ? "\\&" : "")) + "^FS\n");
}
}
emit(output, "^FO" + x + "," + y + "^A0N," + fontHeight + "," + fontWidth + "^FH_^FD" + escaped + "^FS\n");
}
case "box" ->
{
@@ -349,8 +476,20 @@ public final class ZplRenderer
if("barcode".equals(type))
{
String symbology = symbology(element, input, true);
image = element.has("fit") ? BarcodeFit.fit(fitRequest(element, input, symbology, true)).image()
: Barcodes.renderForFormat("ZPL", symbology, value(element, input), Wire.integer(element.get("scale"), 1, 16), Wire.integer(element.get("quiet"), 0, 64), element.has("options") ? element.get("options") : Wire.JSON.createObjectNode(), BinaryBarcodes.encoding(element));
if(element.has("fit"))
{
////////////////////////////////////////////////////////////////////////////////////
// The fitted raster is never larger than its box, so the slack is not negative. //
////////////////////////////////////////////////////////////////////////////////////
BarcodeFit.Request request = fitRequest(element, input, symbology, true);
image = BarcodeFit.fit(request).image();
x += alignment(element, "align", "left", "center", "right") * (BarcodeFit.dotsInside(request.widthMicrons(), input.stock.dpi()) - image.getWidth()) / 2;
y += alignment(element, "valign", "top", "middle", "bottom") * (BarcodeFit.dotsInside(request.heightMicrons(), input.stock.dpi()) - image.getHeight()) / 2;
}
else
{
image = Barcodes.renderForFormat("ZPL", symbology, value(element, input), Wire.integer(element.get("scale"), 1, 16), Wire.integer(element.get("quiet"), 0, 64), element.has("options") ? element.get("options") : Wire.JSON.createObjectNode(), BinaryBarcodes.encoding(element));
}
}
else
{
@@ -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");
}
@@ -0,0 +1,297 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qbits.renderer;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HexFormat;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
/*******************************************************************************
** A fitted barcode's size depends on its data, so only the renderer can centre
** it: these tests hold the placement of the bars inside the fit box, and of the
** printed value in a justified block beneath, on four by one inch label stock.
*******************************************************************************/
class ZplAlignmentTest
{
private static final String TYPICAL = "(01)00875011000222(11)201016(21)FA1683504152";
private static final String SHORT = "(01)00875011000222";
private static final String LONG = "(01)00875011000222(11)201016(10)LOT1234567(21)FA16835041";
private static final Pattern GRAPHIC = Pattern.compile("\\^FO(\\d+),(\\d+)\\^GFA,(\\d+),(\\d+),(\\d+),([0-9A-F]+)\\^FS");
private static final Pattern TEXT = Pattern.compile("\\^FO(\\d+),(\\d+)\\^A0N,(\\d+),(\\d+)(?:\\^FB(\\d+),1,0,([LCR]),0)?\\^FH_\\^FD((?:_[0-9A-F]{2})*)\\^FS");
/*******************************************************************************
** Where the black dots of one rendered graphic field are on the label.
*******************************************************************************/
private record Ink(int left, int right, int top, int bottom)
{
}
/*******************************************************************************
** One printer font field: position, font size, optional block and its text.
*******************************************************************************/
private record Field(int x, int y, int height, int width, Integer block, String justification, String text)
{
}
/*******************************************************************************
** Four by one inch stock with three millimetre margins at one resolution.
*******************************************************************************/
private static RenderInput.Stock stock(int dpi)
{
return (new RenderInput.Stock(101600, 25400, 3000, 3000, 3000, 3000, dpi));
}
/*******************************************************************************
** A GS1-128 fit box across the printable width, then the printed value.
*******************************************************************************/
private static ObjectNode scene(int dpi, String align, String valign, boolean block)
{
ObjectNode scene = Wire.JSON.createObjectNode().put("schema", "qqq-zpl-scene/1");
int margin = dpi == 300 ? 36 : 24;
ObjectNode barcode = scene.putArray("elements").addObject().put("type", "barcode").put("symbology", "gs1-128")
.put("valuePath", "/record/rendererValue").put("quiet", 10).put("x", margin).put("y", margin);
barcode.putObject("fit").put("widthMicrons", dpi == 300 ? 95504 : 95595).put("heightMicrons", dpi == 300 ? 15240 : 15015);
if(align != null)
{
barcode.put("align", align);
}
if(valign != null)
{
barcode.put("valign", valign);
}
ObjectNode text = ((com.fasterxml.jackson.databind.node.ArrayNode) scene.get("elements")).addObject().put("type", "text")
.put("valuePath", "/record/humanReadable").put("x", margin).put("y", dpi == 300 ? 211 : 138)
.put("height", dpi == 300 ? 40 : 27).put("width", dpi == 300 ? 20 : 13);
if(block)
{
text.put("align", "center").put("blockWidth", dpi == 300 ? 1128 : 764).put("maxLines", 2);
}
return (scene);
}
/*******************************************************************************
** Render a scene for one printed value through the worker's own entry point.
*******************************************************************************/
private static String render(ObjectNode scene, int dpi, String readable) throws Exception
{
ObjectNode input = UltracodeImageQualification.request("ZPL", Wire.JSON.writeValueAsString(scene), dpi);
input.set("stock", Wire.JSON.valueToTree(stock(dpi)));
((ObjectNode) input.get("context")).putObject("record").put("rendererValue", readable.replace('(', '[').replace(')', ']')).put("humanReadable", readable);
return (new String(java.util.Base64.getDecoder().decode((String) RendererMain.process(input).get("base64")), StandardCharsets.US_ASCII));
}
/*******************************************************************************
** The bounding box of the black dots of the only graphic field of a label.
*******************************************************************************/
private static Ink ink(String zpl)
{
Matcher matcher = GRAPHIC.matcher(zpl);
assertTrue(matcher.find());
int x = Integer.parseInt(matcher.group(1));
int y = Integer.parseInt(matcher.group(2));
int stride = Integer.parseInt(matcher.group(5));
byte[] raster = HexFormat.of().parseHex(matcher.group(6));
int left = Integer.MAX_VALUE;
int right = -1;
int top = Integer.MAX_VALUE;
int bottom = -1;
for(int index = 0; index < raster.length * 8; index++)
{
if((raster[index / 8] & (128 >> index % 8)) != 0)
{
left = Math.min(left, index % (stride * 8));
right = Math.max(right, index % (stride * 8));
top = Math.min(top, index / (stride * 8));
bottom = Math.max(bottom, index / (stride * 8));
}
}
assertFalse(matcher.find());
return (new Ink(x + left, x + right, y + top, y + bottom));
}
/*******************************************************************************
** Every printer font field of a label, with its field data decoded.
*******************************************************************************/
private static List<Field> fields(String zpl)
{
List<Field> fields = new ArrayList<>();
Matcher matcher = TEXT.matcher(zpl);
while(matcher.find())
{
fields.add(new Field(Integer.parseInt(matcher.group(1)), Integer.parseInt(matcher.group(2)), Integer.parseInt(matcher.group(3)), Integer.parseInt(matcher.group(4)),
matcher.group(5) == null ? null : Integer.valueOf(matcher.group(5)), matcher.group(6),
new String(HexFormat.of().parseHex(matcher.group(7).replace("_", "")), StandardCharsets.UTF_8)));
}
return (fields);
}
/*******************************************************************************
** Centred bars leave the same blank width on both sides of the label, within
** one dot, for short, typical and long values, at the largest module that fits.
*******************************************************************************/
@Test
void centredBarcodeIsCentredOnTheLabelForShortTypicalAndLongValues() throws Exception
{
for(int dpi : List.of(203, 300))
{
int labelWidth = stock(dpi).dots(101600);
for(String value : List.of(SHORT, TYPICAL, LONG))
{
Ink ink = ink(render(scene(dpi, "center", "bottom", true), dpi, value));
assertTrue(Math.abs(ink.left() - (labelWidth - 1 - ink.right())) <= 1, dpi + " dpi " + value + " " + ink);
}
Ink typical = ink(render(scene(dpi, "center", "bottom", true), dpi, TYPICAL));
assertEquals(288 * (dpi == 300 ? 3 : 2), typical.right() - typical.left() + 1);
assertEquals(40 * (dpi == 300 ? 3 : 2), typical.bottom() - typical.top() + 1);
}
}
/*******************************************************************************
** Without align the barcode stays at the box origin, as every published scene
** expects; right and bottom put it against the far edges of the box.
*******************************************************************************/
@Test
void alignmentMovesTheBarcodeInsideItsBoxAndDefaultsToTheOrigin() throws Exception
{
Ink origin = ink(render(scene(300, null, null, false), 300, SHORT));
assertEquals(36 + 30, origin.left());
assertEquals(36 + 30, origin.top());
assertEquals(origin, ink(render(scene(300, "left", "top", false), 300, SHORT)));
Ink far = ink(render(scene(300, "right", "bottom", false), 300, SHORT));
assertEquals(36 + 1128 - 30 - 1, far.right());
assertEquals(36 + 180 - 30 - 1, far.bottom());
ObjectNode tall = scene(203, "center", "middle", false);
((ObjectNode) tall.get("elements").get(0).get("fit")).put("heightMicrons", 17000);
Ink middle = ink(render(tall, 203, SHORT));
assertEquals(24 + (764 - 308) / 2 + 20, middle.left());
assertEquals(24 + (135 - 120) / 2 + 20, middle.top());
}
/*******************************************************************************
** The printed value is one centred field block across the printable width,
** inside the label and below the bars, carrying the whole value.
*******************************************************************************/
@Test
void centredTextIsABlockBelowTheBarsWithTheWholeValue() throws Exception
{
for(int dpi : List.of(203, 300))
{
String zpl = render(scene(dpi, "center", "bottom", true), dpi, TYPICAL);
Ink ink = ink(zpl);
List<Field> fields = fields(zpl);
assertEquals(1, fields.size(), zpl);
Field field = fields.getFirst();
assertEquals(TYPICAL + "\\&", field.text());
assertEquals("C", field.justification());
assertEquals(stock(dpi).dots(101600), 2 * field.x() + field.block());
assertTrue(field.y() > ink.bottom());
assertTrue(field.y() + field.height() <= stock(dpi).dots(25400));
}
}
/*******************************************************************************
** A value too long for one line breaks before an application identifier onto
** a second centred line; nothing is dropped and no line exceeds the block.
*******************************************************************************/
@Test
void longValueBreaksBeforeAnIdentifierOntoASecondLine() throws Exception
{
String value = "(01)00875011000222(11)201016(13)201017(15)221016(10)LOT1234567(21)FA16835041";
ObjectNode scene = scene(300, "center", "bottom", true);
((ObjectNode) scene.get("elements").get(1)).put("y", 190);
List<Field> fields = fields(render(scene, 300, value));
assertEquals(2, fields.size());
assertEquals("(01)00875011000222(11)201016(13)201017(15)221016\\&", fields.get(0).text());
assertEquals("(10)LOT1234567(21)FA16835041\\&", fields.get(1).text());
assertEquals(190 + 45, fields.get(1).y());
assertTrue(fields.get(1).y() + 40 <= 300);
assertEquals(List.of("AAAA", "BBBB", "CC"), ZplRenderer.lines("AAAA BBBBCC", 4, 3));
assertEquals(List.of("ab\\", "cd"), ZplRenderer.lines("ab\\cd", 3, 2));
}
/*******************************************************************************
** Text without the block fields is the single field it always was, and a
** backslash in block text is doubled so the printer prints it.
*******************************************************************************/
@Test
void plainTextIsUnchangedAndBlockTextEscapesBackslashes() throws Exception
{
String plain = render(scene(300, null, null, false), 300, TYPICAL);
Field field = fields(plain).getFirst();
assertEquals(null, field.block());
assertEquals(TYPICAL, field.text());
assertTrue(plain.contains("^FO36,211^A0N,40,20^FH_^FD_28_30_31_29"));
ObjectNode scene = scene(300, null, null, true);
ObjectNode text = (ObjectNode) scene.get("elements").get(1);
text.remove("valuePath");
text.put("value", "a\\b").put("align", "right");
Field right = fields(render(scene, 300, SHORT)).getFirst();
assertEquals("R", right.justification());
assertEquals("a\\\\b", right.text());
}
/*******************************************************************************
** Unknown alignment words, alignment without a fit box, a block that leaves the
** stock and a value that needs more lines than allowed all refuse.
*******************************************************************************/
@Test
void invalidAlignmentAndOverlongTextRefuse() throws Exception
{
ObjectNode sideways = scene(300, "middle", null, false);
assertEquals("INVALID_BARCODE_FIT_PROFILE", assertThrows(IllegalArgumentException.class, () -> render(sideways, 300, SHORT)).getMessage());
ObjectNode unfitted = scene(300, "center", null, false);
((ObjectNode) unfitted.get("elements").get(0)).put("scale", 2).remove("fit");
assertEquals("INVALID_BARCODE_FIT_PROFILE", assertThrows(IllegalArgumentException.class, () -> render(unfitted, 300, SHORT)).getMessage());
ObjectNode justified = scene(300, null, null, true);
((ObjectNode) justified.get("elements").get(1)).put("align", "justify");
assertEquals("INVALID_ZPL_TEXT", assertThrows(IllegalArgumentException.class, () -> render(justified, 300, SHORT)).getMessage());
ObjectNode wide = scene(300, null, null, true);
((ObjectNode) wide.get("elements").get(1)).put("blockWidth", 1165);
assertEquals("ELEMENT_OUTSIDE_STOCK", assertThrows(IllegalArgumentException.class, () -> render(wide, 300, SHORT)).getMessage());
ObjectNode oneLine = scene(300, null, null, true);
((ObjectNode) oneLine.get("elements").get(1)).put("maxLines", 1);
assertEquals("ELEMENT_OUTSIDE_STOCK", assertThrows(IllegalArgumentException.class, () -> render(oneLine, 300, "(01)00875011000222(11)201016(13)201017(15)221016(10)LOT1234567(21)FA16835041")).getMessage());
}
}