Compare commits

..
Author SHA1 Message Date
james.maes 7648868758 Merge pull request 'fix(postgres): accept full_page_writes off on Amazon Aurora PostgreSQL only (1.0 patch)' (#8) from fix/aurora-full-page-writes-1.0 into support/1.0
maven / build (push) Successful in 9m46s
build / maven (push) Successful in 9m46s
2026-11-10 13:00:22 +00:00
james.maesandClaude Fable 5.2 77c95b1f62 fix(postgres): accept full_page_writes off on Amazon Aurora PostgreSQL only
maven / build (push) Successful in 10m35s
build / maven (push) Successful in 10m35s
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:27 -06:00
15 changed files with 35 additions and 678 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@v1
uses: self:KofTwentyTwo/ci-workflows/.gitea/workflows/maven.yaml@main
secrets: inherit
+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.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>
<properties><qqq.version>4.1.0-RC.22</qqq.version><!-- Released sibling QBit coordinates from the Gitea Maven registry --><qbit-record-files.version>0.1.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.2.0-RC.1</qbit-printing.version><enforcer.skip>true</enforcer.skip></properties>
<qbit-printing.version>1.0.0</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.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>
<properties><qqq.version>4.1.0-RC.22</qqq.version><!-- Released sibling QBit coordinates from the Gitea Maven registry --><qbit-record-files.version>0.1.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>
+4 -17
View File
@@ -1,22 +1,9 @@
# 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
# Build only on an operator-approved, immutable existing renderer image.
# This file does not build/download the engine or confer isolation qualification.
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 --from=release /release/target/service.jar /remote/service.jar
COPY --from=release /release/target/lib/ /remote/lib/
COPY transport/target/qqq-remote-renderer-transport-0.1.0-SNAPSHOT.jar /remote/service.jar
COPY transport/target/lib/ /remote/lib/
COPY LICENSE NOTICE /usr/share/doc/qqq-remote-renderer/
USER 65532:65532
EXPOSE 18445
@@ -1,21 +0,0 @@
<?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.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>
<properties><qqq.version>4.1.0-RC.22</qqq.version><!-- Released sibling QBit coordinates from the Gitea Maven registry --><qbit-scripting.version>1.0.0</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
@@ -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.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>
<properties><qqq.version>4.1.0-RC.22</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>
+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.2.1</version><relativePath/></parent>
<parent><groupId>com.kingsrook</groupId><artifactId>qbit-build-parent</artifactId><version>2.1.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>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>
<properties><revision>0.1.0-SNAPSHOT</revision><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><enforcer.skip>true</enforcer.skip></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>
+15 -16
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 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
# 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
FROM ${JAVA_IMAGE}
# Reviewed runtime family: Temurin 21.0.13+9 on Ubuntu noble. The approved digest
@@ -37,9 +37,8 @@ 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
# 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
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
USER 65532:65532
ENTRYPOINT ["java", "-Xmx256m", "-XX:ActiveProcessorCount=1", "-Djava.awt.headless=true", "-Dpdfbox.fontcache=/tmp", "-jar", "/renderer/renderer.jar"]
-120
View File
@@ -1,120 +0,0 @@
# 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/`.
+1 -7
View File
@@ -84,13 +84,7 @@ 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.
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:
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:
```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
@@ -1,25 +0,0 @@
#!/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'
-21
View File
@@ -1,21 +0,0 @@
<?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>
@@ -101,7 +101,6 @@ 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))
{
@@ -135,8 +134,6 @@ 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);
@@ -260,115 +257,6 @@ 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.
*******************************************************************************/
@@ -434,28 +322,13 @@ public final class ZplRenderer
{
throw new IllegalArgumentException("INVALID_ZPL_TEXT");
}
TextBlock block = textBlock(element, x, width);
if(block == null)
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))
{
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");
}
escaped.append('_').append(HexFormat.of().withUpperCase().toHexDigits(value));
}
emit(output, "^FO" + x + "," + y + "^A0N," + fontHeight + "," + fontWidth + "^FH_^FD" + escaped + "^FS\n");
}
case "box" ->
{
@@ -476,20 +349,8 @@ public final class ZplRenderer
if("barcode".equals(type))
{
String symbology = symbology(element, input, true);
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));
}
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));
}
else
{
@@ -1,297 +0,0 @@
/*
* 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());
}
}