Compare commits
18
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
65344f5981
|
||
|
|
9a355670c0 | ||
|
|
228931a937
|
||
|
|
cfbf0744f9
|
||
|
|
1dc265647d | ||
|
|
e762dac5d5
|
||
|
|
e19feb01a8
|
||
|
|
d8a57607fd | ||
|
|
889296d8f5
|
||
|
|
e085859a25
|
||
|
|
4f850c2cf3
|
||
|
|
f9adf1d3d3 | ||
|
|
2dcee6fbea
|
||
|
|
34a3666bbf | ||
|
|
6a3153456e | ||
|
|
9073e9bf1c | ||
|
|
207e3ce034 | ||
|
|
063b1e8136
|
No files matched your search
@@ -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
|
||||
@@ -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.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>
|
||||
<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>
|
||||
|
||||
@@ -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.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>
|
||||
<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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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.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>
|
||||
<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
@@ -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.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>
|
||||
<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>
|
||||
|
||||
@@ -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
@@ -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"]
|
||||
@@ -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
@@ -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}
|
||||
|
||||
@@ -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'
|
||||
@@ -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>
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user