feat: add branded warehouse print diagnostics
Co-authored-by: Codex (GPT-6) <noreply@openai.com> Signed-off-by: James Maes <james@kof22.com>
This commit is contained in:
18 files changed
+1017
-34
No files matched your search
@@ -26,7 +26,7 @@ jobs:
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
repository: QRun-IO/qqq-print-protocol
|
||||
ref: a0de93b00e602abde9be77c1ec9103b670bf3c09
|
||||
ref: f4f977ca4aa6b749e6a52760eed5e53c99a7704d
|
||||
path: qqq-print-protocol
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# Changelog
|
||||
|
||||
## 0.1.3-rc.1 — 2026-10-30
|
||||
|
||||
- Shared QQQ-branded warehouse test pages and labels across native record/table actions and device requests.
|
||||
- Bundle licensed JetBrains Mono Bold; embed PDF glyphs and use bounded ZPL graphics for consistent identifier readability.
|
||||
- Add Letter/A4 and 4 × 6 profile options with explicit 203/300/600-dpi label resolution, compatible defaults and validation.
|
||||
- Add font/media, deterministic generation, ZPL bitmap reconstruction and operator action regression coverage.
|
||||
|
||||
|
||||
## 0.1.2-rc.1 — 2026-10-30
|
||||
|
||||
- Synchronize client-managed device/printer display names, descriptions and locations; preserve physical computer/queue names and stable identifiers.
|
||||
|
||||
@@ -4,3 +4,9 @@ Copyright 2026 Kingsrook, LLC
|
||||
This product contains QQQ 4.1+ printing metadata, queue services,
|
||||
device transport routes, demonstration hosts and compatibility tests.
|
||||
Licensed under the Apache License, Version 2.0.
|
||||
|
||||
Built-in diagnostic documents bundle JetBrains Mono Bold v2.304.
|
||||
Copyright 2020 The JetBrains Mono Project Authors.
|
||||
The font is licensed under the SIL Open Font License, Version 1.1;
|
||||
see src/main/resources/com/kingsrook/qbits/printing/fonts/OFL.txt.
|
||||
The QQQ logo is supplied by the QQQ framework, Copyright Kingsrook, LLC.
|
||||
@@ -143,3 +143,11 @@ This RC implements the happy path of the [QQQ dashboard standard](https://github
|
||||
The Windows companion can request test prints and explicit reprints through device-scoped authenticated routes. The same printer approval/exposure rules apply. Upgrade the QBit and connector together for these additive protocol features. Reprints create a new job; uncertain/spooled originals require duplicate-output consent and remain unchanged.
|
||||
|
||||
QQQ's **Print jobs** table and records expose **Print again** and **Cancel queued jobs** using native selection/review/result screens. Cancellation is allowed only before dispatch; later work must be inspected in the Windows queue. The actions require printing administration permission and the configured tenant context.
|
||||
|
||||
## Warehouse diagnostic pages
|
||||
|
||||
All built-in printer test actions share `PrintingTestDocuments`: QQQ branding, the existing QQQ logo, large warehouse-style identifiers and embedded JetBrains Mono Bold with a dotted zero and distinct 1/I/l. The font is included under SIL OFL 1.1; its license and provenance ship in the JAR. PDF embeds a font subset. ZPL renders the same glyphs into document-local, row-aligned graphics below Zebra's 99,999-byte per-field limit; it does not download or save a printer font.
|
||||
|
||||
Native record and selected-table actions expose a possible-value stock/resolution selector. Letter and A4 are PDF-only. Label profiles are 4 × 6 inches at 203, 300 or 600 dpi; PDF label output is always 101.6 × 152.4 mm. Select matching stock and printer resolution. Bulk actions apply the chosen profile to every selected destination, so group compatible printers. The document does not reconfigure Windows stock, calibrate the printer or save device configuration; ZPL width/length are transient format settings.
|
||||
|
||||
The device test-print API accepts optional `testProfile` with the same choices. Existing clients default to Letter PDF or 203-dpi 4 × 6 ZPL. Invalid profiles fail before enqueueing. Test actions use the ordinary durable queue and existing printer exposure/approval rules; discovery never automatically prints. A ZPL barcode, glyph comparisons and size/alignment marks support physical inspection. Rendering previews and spool acceptance do not prove paper or scanner quality.
|
||||
@@ -1,5 +1,11 @@
|
||||
# Session state — current checkpoint 2026-10-30
|
||||
|
||||
## Branded diagnostic documents — source ready, verification pending
|
||||
|
||||
Built-in test paths now share PrintingTestDocuments and TestPrintProfile. Native QQQ forms and device test requests choose Letter, A4 or 4 × 6-inch label profiles at 203/300/600 dpi; omitted profiles retain PDF Letter/ZPL 203 defaults. ZPL rejects page profiles. Documents use the QQQ logo and wordmark, bundled OFL JetBrains Mono Bold, character-pair checks and a 25.4 mm reference. PDF embeds vector text; ZPL carries a monochrome bitmap in whole-row graphic strips of at most 99,999 bytes each and native Code 128. No persistent font download is required. Identical PDF generation uses stable document IDs for idempotency; changed request content still conflicts.
|
||||
|
||||
Added generator/media/font/margin tests, per-strip byte/row/hex bounds and full bitmap reconstruction checks at every density and native/API parity/profile-validation tests. PrintingTestDocumentsExport writes local PDF/PNG/ZPL previews without contacting printers. The parent agent owns coordinated verification and physical acceptance. This source checkpoint claims no new test results or printed output.
|
||||
|
||||
2026-10-30 source0.1.2-rc.1: implemented client-authoritative Name/Description/Location synchronization, preserved computer/queue technical names, read-only native QQQ profile fields and friendly selector labels. Added additive schema backfill/widening, dispatch/history indexes, one-row payload-free claims and metadata-only keyset history pages. Added long-text, explicit-clear, migration, tenant and synthetic 10,000-row pagination/claim tests. Root verified22tests with zeroCheckstyle violations using mvnw.cmd verify. Protocol28definitions/21fixtures passed and CI pins a0de93b. Exact-source GitHub build41126545760 and security41126545795 passed for cfe8a5f. Prior verification below predates these changes. No physical prints were requested by this work.
|
||||
|
||||
Current source is prepared for **0.1.1-rc.1**. Latest Java verification passed **19 tests with zero Checkstyle violations**. The full audit follow-up implements device-scoped test-print/retry/cancel/queued-job routes and native **Print again** / **Cancel queued jobs** actions from job records and table selections. Native review uses named records, rechecks administrator/tenant/row access and current state, requires explicit duplicate-risk confirmation for uncertain/spooled output, and uses a stable request identity for retry replay. Retry creates a new job and preserves the original outcome. Queued cancellation is serialized against claim and cannot promise cancellation after dispatch.
|
||||
|
||||
@@ -33,3 +33,6 @@
|
||||
- [ ] Record final package hashes, signing status and applicable integration evidence before replacement release.
|
||||
- [ ] Run a fresh explicitly authorized physical acceptance job for the final package; Zebra remains deferred.
|
||||
- [ ] Qualify reboot/sign-in/service recovery and deployed-host behavior; no source-only or synthetic result substitutes for those checks.
|
||||
|
||||
- [x] Share branded diagnostic generators and explicit stock/resolution profiles across native QQQ and device test-print entrypoints.
|
||||
- [ ] Run coordinated branded-document tests, inspect exported layouts and confirm physical output/scanning; no new template print is yet qualified.
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent><groupId>com.kingsrook</groupId><artifactId>qbit-build-parent</artifactId><version>2.0.0</version><relativePath/></parent>
|
||||
<groupId>com.kingsrook.qbits</groupId>
|
||||
<artifactId>qbit-printing</artifactId>
|
||||
<version>0.1.2-rc.1</version>
|
||||
<version>0.1.3-rc.1</version>
|
||||
<name>QQQ Printing</name>
|
||||
<licenses><license><name>Apache License, Version 2.0</name><url>https://www.apache.org/licenses/LICENSE-2.0</url></license></licenses>
|
||||
<properties>
|
||||
|
||||
@@ -22,7 +22,6 @@ package com.kingsrook.qbits.printing;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
import java.util.HashMap;
|
||||
@@ -49,6 +48,9 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.permissions.QPermissionRules;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValue;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSourceType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QComponentType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendComponentMetaData;
|
||||
@@ -74,10 +76,11 @@ public class PrintingProcesses implements BackendStep
|
||||
public static void register(QInstance instance, QAppMetaData app)
|
||||
{
|
||||
PrintingJobProcesses.register(instance);
|
||||
registerProfiles(instance);
|
||||
add(instance, "printingEnrollDevice", "Enroll a print device", List.of(field("facilityId", "Facility"), field("deviceName", "Device name")));
|
||||
add(instance, "printingEnablePrinter", "Set printer approval", List.of(deviceField(), printerField(null), new QFieldMetaData("enabled", QFieldType.BOOLEAN).withLabel("Approved for printing").withIsRequired(true)));
|
||||
add(instance, "printingTestZpl", "Print a test label", List.of(deviceField(), printerField("ZPL")));
|
||||
add(instance, "printingTestPdf", "Print a test page", List.of(deviceField(), printerField("PDF")));
|
||||
add(instance, "printingTestZpl", "Print a test label", List.of(deviceField(), printerField("ZPL"), profileField(false)));
|
||||
add(instance, "printingTestPdf", "Print a test page", List.of(deviceField(), printerField("PDF"), profileField(true)));
|
||||
selected(instance, "printingTestSelectedPdf", "Print a test page", "Each selected PDF printer will receive one test page after you continue.");
|
||||
selected(instance, "printingTestSelectedZpl", "Print a test label", "Each selected ZPL printer will receive one test label after you continue.");
|
||||
selected(instance, "printingEnableSelectedPrinters", "Approve printers", "The selected printers will be approved for printing after you continue.");
|
||||
@@ -86,6 +89,40 @@ public class PrintingProcesses implements BackendStep
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Fixed native choices keep physical stock and resolution explicit.
|
||||
***************************************************************************/
|
||||
private static void registerProfiles(QInstance instance)
|
||||
{
|
||||
ArrayList<QPossibleValue<?>> pdf = new ArrayList<>();
|
||||
ArrayList<QPossibleValue<?>> zpl = new ArrayList<>();
|
||||
for(TestPrintProfile profile : TestPrintProfile.values())
|
||||
{
|
||||
QPossibleValue<String> option = new QPossibleValue<>(profile.name(), profile.getLabel());
|
||||
pdf.add(option);
|
||||
if(profile.getDpi() > 0)
|
||||
{
|
||||
zpl.add(option);
|
||||
}
|
||||
}
|
||||
instance.addPossibleValueSource(new QPossibleValueSource().withName("printing_testProfilePdf").withType(QPossibleValueSourceType.ENUM).withEnumValues(pdf));
|
||||
instance.addPossibleValueSource(new QPossibleValueSource().withName("printing_testProfileZpl").withType(QPossibleValueSourceType.ENUM).withEnumValues(zpl));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** The same profile applies to every selected printer, so group matching stock.
|
||||
***************************************************************************/
|
||||
private static QFieldMetaData profileField(boolean pdf)
|
||||
{
|
||||
return (field("testProfile", "Test stock / printer resolution (match every selected printer)")
|
||||
.withPossibleValueSourceName(pdf ? "printing_testProfilePdf" : "printing_testProfileZpl")
|
||||
.withDefaultValue(pdf ? TestPrintProfile.LETTER.name() : TestPrintProfile.LABEL_4X6_203.name()));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Shared administrator permission for printing apps and processes.
|
||||
***************************************************************************/
|
||||
@@ -156,6 +193,11 @@ public class PrintingProcesses implements BackendStep
|
||||
values.put(StreamedETLWithFrontendProcess.FIELD_PREVIEW_MESSAGE, preview);
|
||||
QProcessMetaData process = StreamedETLWithFrontendProcess.defineProcessMetaData(ExtractViaQueryStep.class, SelectedTransform.class, SelectedLoad.class, values)
|
||||
.withName(name).withLabel(label).withTableName(PrintingTables.PRINTER).withMinInputRecords(1).withPermissionRules(permissionRules());
|
||||
if(name.startsWith("printingTest"))
|
||||
{
|
||||
process.withStep(0, new QFrontendStepMetaData().withName("testProfile").withLabel("Choose matching stock and resolution")
|
||||
.withFormFields(List.of(profileField(name.endsWith("Pdf")))).withComponent(new QFrontendComponentMetaData().withType(QComponentType.EDIT_FORM)));
|
||||
}
|
||||
instance.addProcess(process);
|
||||
}
|
||||
|
||||
@@ -220,7 +262,7 @@ public class PrintingProcesses implements BackendStep
|
||||
case "printingTestZpl", "printingTestPdf" ->
|
||||
{
|
||||
QRecord printer = resolvePrinter(input.getValueString("printerId"), input.getValueString("deviceId"));
|
||||
String job = submitTest(printer, "printingTestPdf".equals(input.getProcessName()));
|
||||
String job = submitTest(printer, "printingTestPdf".equals(input.getProcessName()), input.getValueString("testProfile"));
|
||||
output.addValue("result", "Queued print job " + job + ". Check Print jobs for submission status.");
|
||||
}
|
||||
default -> throw new QException("Unknown printing action");
|
||||
@@ -256,10 +298,21 @@ public class PrintingProcesses implements BackendStep
|
||||
** Send one native test document through the normal durable queue.
|
||||
***************************************************************************/
|
||||
static String submitTest(QRecord printer, boolean pdf) throws QException
|
||||
{
|
||||
return (submitTest(printer, pdf, null));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** All built-in document entrypoints share profile validation and generation.
|
||||
***************************************************************************/
|
||||
static String submitTest(QRecord printer, boolean pdf, String profileName) throws QException
|
||||
{
|
||||
try
|
||||
{
|
||||
byte[] bytes = pdf ? testPdf() : "^XA^FO30,30^A0N,35,35^FDQQQ cloud printing test^FS^FO30,85^A0N,25,25^FDTest label - not for shipping^FS^XZ".getBytes(StandardCharsets.UTF_8);
|
||||
TestPrintProfile profile = TestPrintProfile.resolve(pdf ? "PDF" : "ZPL", profileName);
|
||||
byte[] bytes = pdf ? PrintingTestDocuments.pdf(profile) : PrintingTestDocuments.zpl(profile);
|
||||
JSONObject job = currentService().submit(new JSONObject().put("deviceId", printer.getValueString("device_id")).put("printerId", printer.getValueString("printer_id"))
|
||||
.put("title", "QQQ print test").put("format", pdf ? "PDF" : "ZPL").put("contentBase64", Base64.getEncoder().encodeToString(bytes)).put("idempotencyKey", "test:" + UUID.randomUUID()));
|
||||
return (job.getString("jobId"));
|
||||
@@ -295,6 +348,14 @@ public class PrintingProcesses implements BackendStep
|
||||
if(input.getProcessName().startsWith("printingTest"))
|
||||
{
|
||||
String format = input.getProcessName().endsWith("Pdf") ? "PDF" : "ZPL";
|
||||
try
|
||||
{
|
||||
TestPrintProfile.resolve(format, input.getValueString("testProfile"));
|
||||
}
|
||||
catch(PrintingException exception)
|
||||
{
|
||||
throw new QException(exception.getMessage(), exception);
|
||||
}
|
||||
if(!Integer.valueOf(1).equals(printer.getValueInteger("enabled")) || !Integer.valueOf(1).equals(printer.getValueInteger("available"))
|
||||
|| !new org.json.JSONArray(printer.getValueString("formats")).toList().contains(format))
|
||||
{
|
||||
@@ -344,8 +405,8 @@ public class PrintingProcesses implements BackendStep
|
||||
{
|
||||
switch(input.getProcessName())
|
||||
{
|
||||
case "printingTestSelectedPdf" -> submitTest(printer, true);
|
||||
case "printingTestSelectedZpl" -> submitTest(printer, false);
|
||||
case "printingTestSelectedPdf" -> submitTest(printer, true, input.getValueString("testProfile"));
|
||||
case "printingTestSelectedZpl" -> submitTest(printer, false, input.getValueString("testProfile"));
|
||||
case "printingEnableSelectedPrinters", "printingDisableSelectedPrinters" -> currentService().enablePrinter(printer.getValueString("device_id"), printer.getValueString("printer_id"), "printingEnableSelectedPrinters".equals(input.getProcessName()));
|
||||
default -> throw new QException("Unknown selected printing action");
|
||||
}
|
||||
@@ -378,26 +439,7 @@ public class PrintingProcesses implements BackendStep
|
||||
***************************************************************************/
|
||||
static byte[] testPdf() throws QException
|
||||
{
|
||||
try(org.apache.pdfbox.pdmodel.PDDocument document = new org.apache.pdfbox.pdmodel.PDDocument())
|
||||
{
|
||||
org.apache.pdfbox.pdmodel.PDPage page = new org.apache.pdfbox.pdmodel.PDPage();
|
||||
document.addPage(page);
|
||||
try(org.apache.pdfbox.pdmodel.PDPageContentStream content = new org.apache.pdfbox.pdmodel.PDPageContentStream(document, page))
|
||||
{
|
||||
content.beginText();
|
||||
content.setFont(org.apache.pdfbox.pdmodel.font.PDType1Font.HELVETICA, 24);
|
||||
content.newLineAtOffset(72, 700);
|
||||
content.showText("QQQ cloud printing test");
|
||||
content.endText();
|
||||
}
|
||||
java.io.ByteArrayOutputStream bytes = new java.io.ByteArrayOutputStream();
|
||||
document.save(bytes);
|
||||
return (bytes.toByteArray());
|
||||
}
|
||||
catch(java.io.IOException exception)
|
||||
{
|
||||
throw new QException("Could not generate test PDF", exception);
|
||||
}
|
||||
return (PrintingTestDocuments.pdf());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -453,11 +453,8 @@ public class PrintingService
|
||||
public JSONObject testPrint(String deviceId, JSONObject request) throws Exception
|
||||
{
|
||||
String format = required(request.optString("format"), "format");
|
||||
if(!Set.of("PDF", "ZPL").contains(format))
|
||||
{
|
||||
throw new PrintingException(400, "Only PDF and ZPL are supported");
|
||||
}
|
||||
byte[] bytes = "PDF".equals(format) ? PrintingProcesses.testPdf() : "^XA^FO30,30^A0N,35,35^FDQQQ cloud printing test^FS^FO30,85^A0N,25,25^FDTest label - not for shipping^FS^XZ".getBytes(StandardCharsets.UTF_8);
|
||||
TestPrintProfile profile = TestPrintProfile.resolve(format, request.has("testProfile") ? request.optString("testProfile") : null);
|
||||
byte[] bytes = "PDF".equals(format) ? PrintingTestDocuments.pdf(profile) : PrintingTestDocuments.zpl(profile);
|
||||
return (submit(new JSONObject().put("deviceId", deviceId).put("printerId", required(request.optString("printerId"), "printerId"))
|
||||
.put("title", "QQQ print test").put("format", format).put("contentBase64", Base64.getEncoder().encodeToString(bytes))
|
||||
.put("idempotencyKey", "device-test:" + deviceId + ":" + required(request.optString("requestId"), "requestId"))));
|
||||
|
||||
@@ -0,0 +1,380 @@
|
||||
/*
|
||||
* 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.printing;
|
||||
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.Color;
|
||||
import java.awt.Font;
|
||||
import java.awt.FontFormatException;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.RenderingHints;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HexFormat;
|
||||
import java.util.Set;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import org.apache.pdfbox.cos.COSArray;
|
||||
import org.apache.pdfbox.cos.COSName;
|
||||
import org.apache.pdfbox.cos.COSString;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.PDPage;
|
||||
import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
||||
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||
import org.apache.pdfbox.pdmodel.font.PDType0Font;
|
||||
import org.apache.pdfbox.pdmodel.graphics.image.LosslessFactory;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Shared diagnostic documents. Font data travels with each job, never as a
|
||||
** persistent printer download or an assumption about installed printer fonts.
|
||||
*******************************************************************************/
|
||||
public final class PrintingTestDocuments
|
||||
{
|
||||
static final String FONT_RESOURCE = "/com/kingsrook/qbits/printing/fonts/JetBrainsMono-Bold.ttf";
|
||||
private static final String LOGO_RESOURCE = "/com/kingsrook/qbits/printing/images/qqq-logo.png";
|
||||
static final String BARCODE_VALUE = "QQQ-TEST-123456";
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** One vector Letter page with an embedded, subsetted warehouse test font.
|
||||
***************************************************************************/
|
||||
public static byte[] pdf() throws QException
|
||||
{
|
||||
return (pdf(TestPrintProfile.LETTER));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** PDF media is explicit; label profiles retain their four-by-six-inch size.
|
||||
***************************************************************************/
|
||||
public static byte[] pdf(TestPrintProfile profile) throws QException
|
||||
{
|
||||
try(PDDocument document = new PDDocument(); InputStream fontStream = fontStream())
|
||||
{
|
||||
COSArray identifiers = new COSArray();
|
||||
identifiers.add(new COSString("QQQ-TEST-" + profile.name()));
|
||||
identifiers.add(new COSString("QQQ-TEST-" + profile.name()));
|
||||
document.getDocument().getTrailer().setItem(COSName.ID, identifiers);
|
||||
PDType0Font font = PDType0Font.load(document, fontStream, true);
|
||||
PDRectangle media = profile.getDpi() > 0 ? new PDRectangle(288, 432) : (profile == TestPrintProfile.A4 ? PDRectangle.A4 : PDRectangle.LETTER);
|
||||
PDPage page = new PDPage(media);
|
||||
document.addPage(page);
|
||||
try(PDPageContentStream content = new PDPageContentStream(document, page))
|
||||
{
|
||||
if(profile.getDpi() > 0)
|
||||
{
|
||||
pdfLabel(content, font);
|
||||
content.drawImage(LosslessFactory.createFromImage(document, logo()), 210, 348, 54, 54);
|
||||
}
|
||||
else
|
||||
{
|
||||
pdfText(content, font, 56, 48, 690, "QQQ");
|
||||
content.drawImage(LosslessFactory.createFromImage(document, logo()), media.getWidth() - 132, 660, 84, 84);
|
||||
pdfText(content, font, 18, 48, 650, "WAREHOUSE PRINT CHECK");
|
||||
pdfText(content, font, 14, 48, 620, "TEST PRINT - NOT POSTAGE");
|
||||
pdfText(content, font, 11, 48, 590, (profile == TestPrintProfile.A4 ? "A4 210 x 297 mm" : "LETTER 8.5 x 11 in") + " / ACTUAL SIZE");
|
||||
pdfText(content, font, 12, 48, 535, "READ EACH CHARACTER");
|
||||
pdfText(content, font, 32, 48, 490, "0O 1Il 5S 8B 2Z 6G");
|
||||
pdfText(content, font, 18, 48, 440, "BIN A01-I1 / SKU O0-5S8B");
|
||||
pdfText(content, font, 18, 48, 410, "ORDER QQQ-123456 / QTY 02");
|
||||
pdfText(content, font, 12, 48, 350, "Measure this line: 25.4 mm / 1 inch");
|
||||
content.setLineWidth(1);
|
||||
content.moveTo(48, 330);
|
||||
content.lineTo(120, 330);
|
||||
content.moveTo(48, 325);
|
||||
content.lineTo(48, 335);
|
||||
content.moveTo(120, 325);
|
||||
content.lineTo(120, 335);
|
||||
content.stroke();
|
||||
pdfGrayBars(content);
|
||||
pdfText(content, font, 10, 48, 195, "Check edges, solid fill and each gray level.");
|
||||
pdfText(content, font, 10, 48, 170, "Confirm paper output; SPOOLED means submitted only.");
|
||||
content.addRect(36, 36, media.getWidth() - 72, media.getHeight() - 72);
|
||||
content.stroke();
|
||||
}
|
||||
}
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
document.save(bytes);
|
||||
return (bytes.toByteArray());
|
||||
}
|
||||
catch(IOException exception)
|
||||
{
|
||||
throw new QException("Could not generate branded test PDF", exception);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Compatibility default for callers that have no configured label density.
|
||||
***************************************************************************/
|
||||
public static byte[] zpl() throws QException
|
||||
{
|
||||
return (zpl(203));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Reject page-only profiles even when invoked outside the HTTP entrypoint.
|
||||
***************************************************************************/
|
||||
public static byte[] zpl(TestPrintProfile profile) throws QException
|
||||
{
|
||||
return (zpl(profile.getDpi()));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** A four-by-six-inch label at the explicitly chosen printer resolution.
|
||||
** Text uses the bundled font rasterized to one bit; Code 128 stays native.
|
||||
***************************************************************************/
|
||||
public static byte[] zpl(int dpi) throws QException
|
||||
{
|
||||
BufferedImage image = labelImage(dpi);
|
||||
int rowBytes = (image.getWidth() + 7) / 8;
|
||||
byte[] bitmap = new byte[rowBytes * image.getHeight()];
|
||||
for(int y = 0; y < image.getHeight(); y++)
|
||||
{
|
||||
for(int x = 0; x < image.getWidth(); x++)
|
||||
{
|
||||
if((image.getRGB(x, y) & 0xffffff) == 0)
|
||||
{
|
||||
bitmap[y * rowBytes + x / 8] |= (byte) (0x80 >>> (x % 8));
|
||||
}
|
||||
}
|
||||
}
|
||||
int module = Math.max(2, dpi / 100);
|
||||
StringBuilder zpl = new StringBuilder("^XA^PW" + image.getWidth() + "^LL" + image.getHeight() + "^LH0,0");
|
||||
appendGraphicStrips(zpl, bitmap, rowBytes, image.getHeight());
|
||||
zpl.append("^FO").append(dots(24, dpi)).append(',').append(dots(276, dpi)).append("^BY").append(module)
|
||||
.append("^BCN,").append(dots(42, dpi)).append(",N,N,N^FD").append(BARCODE_VALUE).append("^FS^PQ1^XZ");
|
||||
return (zpl.toString().getBytes(StandardCharsets.US_ASCII));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Zebra limits each graphic field to 99,999 bytes. Whole-row strips preserve
|
||||
** padding and image coordinates without downloading persistent graphics.
|
||||
***************************************************************************/
|
||||
private static void appendGraphicStrips(StringBuilder zpl, byte[] bitmap, int rowBytes, int height)
|
||||
{
|
||||
int rowsPerStrip = 99999 / rowBytes;
|
||||
for(int firstRow = 0; firstRow < height; firstRow += rowsPerStrip)
|
||||
{
|
||||
int rows = Math.min(rowsPerStrip, height - firstRow);
|
||||
int length = rows * rowBytes;
|
||||
int offset = firstRow * rowBytes;
|
||||
zpl.append("^FO0,").append(firstRow).append("^GFA,").append(length).append(',').append(length).append(',').append(rowBytes).append(',')
|
||||
.append(HexFormat.of().withUpperCase().formatHex(bitmap, offset, offset + length)).append("^FS");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Deterministic image also supports inspection without contacting a printer.
|
||||
***************************************************************************/
|
||||
static BufferedImage labelImage(int dpi) throws QException
|
||||
{
|
||||
if(!Set.of(203, 300, 600).contains(dpi))
|
||||
{
|
||||
throw new IllegalArgumentException("Label resolution must be 203, 300 or 600 dpi");
|
||||
}
|
||||
try(InputStream stream = fontStream())
|
||||
{
|
||||
Font font = Font.createFont(Font.TRUETYPE_FONT, stream);
|
||||
BufferedImage image = new BufferedImage(dpi * 4, dpi * 6, BufferedImage.TYPE_BYTE_BINARY);
|
||||
Graphics2D graphics = image.createGraphics();
|
||||
try
|
||||
{
|
||||
graphics.setColor(Color.WHITE);
|
||||
graphics.fillRect(0, 0, image.getWidth(), image.getHeight());
|
||||
graphics.scale(dpi / 72.0, dpi / 72.0);
|
||||
graphics.setColor(Color.BLACK);
|
||||
graphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_OFF);
|
||||
graphics.drawRect(12, 12, 264, 408);
|
||||
graphics.drawImage(logo(), 210, 30, 54, 54, null);
|
||||
labelText(graphics, font, 43, 24, 58, "QQQ");
|
||||
labelText(graphics, font, 10, 24, 79, "WAREHOUSE PRINT CHECK");
|
||||
labelText(graphics, font, 12, 24, 101, "TEST PRINT - NOT POSTAGE");
|
||||
labelText(graphics, font, 9, 24, 119, "4 x 6 in / " + dpi + " dpi / 1 copy");
|
||||
graphics.drawLine(24, 130, 264, 130);
|
||||
labelText(graphics, font, 9, 24, 148, "READ EACH CHARACTER");
|
||||
labelText(graphics, font, 25, 24, 179, "0O 1Il 5S");
|
||||
labelText(graphics, font, 25, 24, 210, "8B 2Z 6G");
|
||||
labelText(graphics, font, 12, 24, 239, "BIN A01-I1 / SKU O0-5S8B");
|
||||
labelText(graphics, font, 12, 24, 258, "ORDER QQQ-123456 / QTY 02");
|
||||
labelText(graphics, font, 10, 24, 338, "SCAN: " + BARCODE_VALUE);
|
||||
graphics.drawLine(24, 365, 96, 365);
|
||||
graphics.drawLine(24, 361, 24, 369);
|
||||
graphics.drawLine(96, 361, 96, 369);
|
||||
labelText(graphics, font, 9, 106, 368, "25.4 mm / 1 inch");
|
||||
labelText(graphics, font, 8, 24, 398, "Confirm output, size and barcode scan.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
graphics.dispose();
|
||||
}
|
||||
return (image);
|
||||
}
|
||||
catch(IOException | FontFormatException exception)
|
||||
{
|
||||
throw new QException("Could not generate branded test label", exception);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Fail explicitly if packaging omitted the licensed font resource.
|
||||
***************************************************************************/
|
||||
private static InputStream fontStream() throws IOException
|
||||
{
|
||||
InputStream stream = PrintingTestDocuments.class.getResourceAsStream(FONT_RESOURCE);
|
||||
if(stream == null)
|
||||
{
|
||||
throw new IOException("Bundled test-document font is missing");
|
||||
}
|
||||
return (stream);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Composite transparent source artwork on white before monochrome printing.
|
||||
***************************************************************************/
|
||||
private static BufferedImage logo() throws IOException
|
||||
{
|
||||
try(InputStream stream = PrintingTestDocuments.class.getResourceAsStream(LOGO_RESOURCE))
|
||||
{
|
||||
if(stream == null)
|
||||
{
|
||||
throw new IOException("Bundled QQQ logo is missing");
|
||||
}
|
||||
BufferedImage source = ImageIO.read(stream);
|
||||
BufferedImage image = new BufferedImage(source.getWidth(), source.getHeight(), BufferedImage.TYPE_BYTE_BINARY);
|
||||
Graphics2D graphics = image.createGraphics();
|
||||
try
|
||||
{
|
||||
graphics.setColor(Color.WHITE);
|
||||
graphics.fillRect(0, 0, image.getWidth(), image.getHeight());
|
||||
graphics.drawImage(source, 0, 0, null);
|
||||
}
|
||||
finally
|
||||
{
|
||||
graphics.dispose();
|
||||
}
|
||||
return (image);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Convert layout points to printer dots without changing physical size.
|
||||
***************************************************************************/
|
||||
private static int dots(int points, int dpi)
|
||||
{
|
||||
return ((int) Math.round(points * dpi / 72.0));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Draw text using the same bundled face on both document formats.
|
||||
***************************************************************************/
|
||||
private static void labelText(Graphics2D graphics, Font font, int size, int x, int y, String text)
|
||||
{
|
||||
graphics.setFont(font.deriveFont((float) size));
|
||||
graphics.drawString(text, x, y);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Vector PDF text remains searchable and carries its own font subset.
|
||||
***************************************************************************/
|
||||
private static void pdfText(PDPageContentStream content, PDType0Font font, int size, int x, int y, String text) throws IOException
|
||||
{
|
||||
content.beginText();
|
||||
content.setFont(font, size);
|
||||
content.newLineAtOffset(x, y);
|
||||
content.showText(text);
|
||||
content.endText();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Five vector gray levels expose missing toner and driver conversion.
|
||||
***************************************************************************/
|
||||
private static void pdfGrayBars(PDPageContentStream content) throws IOException
|
||||
{
|
||||
for(int index = 0; index < 5; index++)
|
||||
{
|
||||
content.setNonStrokingColor(index / 4.0f);
|
||||
content.addRect(48 + index * 72, 240, 72, 48);
|
||||
content.fill();
|
||||
}
|
||||
content.setNonStrokingColor(0f);
|
||||
content.addRect(48, 240, 360, 48);
|
||||
content.stroke();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Compact label PDF keeps vector glyphs and explicit physical media bounds.
|
||||
***************************************************************************/
|
||||
private static void pdfLabel(PDPageContentStream content, PDType0Font font) throws IOException
|
||||
{
|
||||
pdfText(content, font, 43, 24, 374, "QQQ");
|
||||
pdfText(content, font, 10, 24, 353, "WAREHOUSE PRINT CHECK");
|
||||
pdfText(content, font, 12, 24, 331, "TEST PRINT - NOT POSTAGE");
|
||||
pdfText(content, font, 9, 24, 313, "4 x 6 in / PDF / ACTUAL SIZE");
|
||||
pdfText(content, font, 9, 24, 284, "READ EACH CHARACTER");
|
||||
pdfText(content, font, 25, 24, 253, "0O 1Il 5S");
|
||||
pdfText(content, font, 25, 24, 222, "8B 2Z 6G");
|
||||
pdfText(content, font, 12, 24, 193, "BIN A01-I1 / SKU O0-5S8B");
|
||||
pdfText(content, font, 12, 24, 174, "ORDER QQQ-123456 / QTY 02");
|
||||
pdfText(content, font, 11, 24, 132, "PDF FONT / SCALE CHECK");
|
||||
pdfText(content, font, 9, 106, 64, "25.4 mm / 1 inch");
|
||||
pdfText(content, font, 8, 24, 34, "Confirm output and measure the line.");
|
||||
content.setLineWidth(1);
|
||||
content.moveTo(24, 67);
|
||||
content.lineTo(96, 67);
|
||||
content.moveTo(24, 63);
|
||||
content.lineTo(24, 71);
|
||||
content.moveTo(96, 63);
|
||||
content.lineTo(96, 71);
|
||||
content.addRect(12, 12, 264, 408);
|
||||
content.stroke();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* 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.printing;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Explicit stock and resolution choices for built-in diagnostic documents.
|
||||
*******************************************************************************/
|
||||
public enum TestPrintProfile
|
||||
{
|
||||
LETTER("Letter - 8.5 x 11 in", 0),
|
||||
A4("A4 - 210 x 297 mm", 0),
|
||||
LABEL_4X6_203("4 x 6 in label - 203 dpi", 203),
|
||||
LABEL_4X6_300("4 x 6 in label - 300 dpi", 300),
|
||||
LABEL_4X6_600("4 x 6 in label - 600 dpi", 600);
|
||||
|
||||
private final String label;
|
||||
private final int dpi;
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Store a human label and printer density without guessing from a name.
|
||||
***************************************************************************/
|
||||
TestPrintProfile(String label, int dpi)
|
||||
{
|
||||
this.label = label;
|
||||
this.dpi = dpi;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Validate before enqueueing. Omitted profiles retain legacy defaults.
|
||||
***************************************************************************/
|
||||
public static TestPrintProfile resolve(String format, String value) throws PrintingException
|
||||
{
|
||||
if(!"PDF".equals(format) && !"ZPL".equals(format))
|
||||
{
|
||||
throw new PrintingException(400, "Only PDF and ZPL are supported");
|
||||
}
|
||||
TestPrintProfile profile;
|
||||
try
|
||||
{
|
||||
profile = value == null ? ("PDF".equals(format) ? LETTER : LABEL_4X6_203) : valueOf(value);
|
||||
}
|
||||
catch(IllegalArgumentException exception)
|
||||
{
|
||||
throw new PrintingException(400, "Unknown test profile");
|
||||
}
|
||||
if("ZPL".equals(format) && profile.dpi == 0)
|
||||
{
|
||||
throw new PrintingException(400, "ZPL test prints require a 4 x 6 label profile with the matching printer dpi");
|
||||
}
|
||||
return (profile);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Friendly choice label used by native QQQ forms.
|
||||
***************************************************************************/
|
||||
public String getLabel()
|
||||
{
|
||||
return (label);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Zero marks page profiles; label values are supported device resolutions.
|
||||
***************************************************************************/
|
||||
public int getDpi()
|
||||
{
|
||||
return (dpi);
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,93 @@
|
||||
Copyright 2020 The JetBrains Mono Project Authors (https://github.com/JetBrains/JetBrainsMono)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
https://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
@@ -0,0 +1,25 @@
|
||||
# Test-document font
|
||||
|
||||
`JetBrainsMono-Bold.ttf` is the unmodified static **JetBrains Mono Bold v2.304** font from JetBrains. It is bundled for the built-in PDF and rasterized ZPL test documents, so those paths use the same letterforms without relying on installed computer or printer fonts.
|
||||
|
||||
Copyright 2020 The JetBrains Mono Project Authors. The font is licensed under the **SIL Open Font License 1.1**, reproduced in [OFL.txt](OFL.txt); it is not covered by this repository's Apache license. Preserve that license and attribution when redistributing the font.
|
||||
|
||||
## Provenance
|
||||
|
||||
- [Official font design and character comparisons](https://www.jetbrains.com/lp/mono/)
|
||||
- [Official tagged font source](https://github.com/JetBrains/JetBrainsMono/blob/v2.304/fonts/ttf/JetBrainsMono-Bold.ttf)
|
||||
- [Official tagged license](https://github.com/JetBrains/JetBrainsMono/blob/v2.304/OFL.txt)
|
||||
- Acquired and inspected: 2026-10-30. No modifications or font conversions.
|
||||
- Font size: 277,828 bytes.
|
||||
- Font SHA-256: `5590990c82e097397517f275f430af4546e1c45cff408bde4255dad142479dcb`
|
||||
- License SHA-256: `30f0c136e3c88e422d0791acd97238870f9054a9729bc34cf2ff0d4ed8cac4ad`
|
||||
|
||||
## Rendering and readability
|
||||
|
||||
Visual inspection of the actual bundled font confirmed a dotted zero versus plain capital O, an angled top and baseline on digit 1, horizontal bars on capital I, and a curved foot on lowercase l. The 5/S, 2/Z, 8/B and 6/G pairs also have visibly different outlines. Inspection included nominal 12-point text rasterized at 203 and 300 dpi; physical printer acceptance remains a separate check. This does not guarantee error-free reading at every size or on damaged labels.
|
||||
|
||||
Use ordinary character drawing without programming ligatures for identifiers. Favor at least 12-point text for important warehouse identifiers, generous whitespace, and a larger explicit ambiguous-character proof row. Keep barcode generation separate from font rendering.
|
||||
|
||||
The file contains TrueType `glyf` outlines, has no variable-font `fvar` table, and its OS/2 `fsType` is zero. It therefore has the static TrueType structure expected by the project's PDFBox 2.x embedding path. Load the resource with `PDType0Font.load(document, inputStream, true)` to embed a subset. See the [official PDFBox 2.x API](https://pdfbox.apache.org/docs/2.0.13/javadocs/org/apache/pdfbox/pdmodel/font/PDType0Font.html). The resource inspection itself is not a substitute for the generator's PDFBox regression test.
|
||||
|
||||
For ZPL, load the same resource through `Font.createFont(Font.TRUETYPE_FONT, inputStream)`, derive the required pixel size from the selected printer dpi, render onto an opaque white background, and encode the monochrome result as document-local graphics. This preserves the distinctive glyphs without installing a font on the printer. Raster previews and physical output still need inspection at each supported resolution.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 42 KiB |
@@ -47,6 +47,7 @@ import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
@@ -58,6 +59,52 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
*******************************************************************************/
|
||||
class PrintingProcessesTest
|
||||
{
|
||||
/***************************************************************************
|
||||
** Native actions and device requests use identical bundled label documents.
|
||||
***************************************************************************/
|
||||
@Test
|
||||
void builtInTestEntryPointsShareProfilesAndRejectChangedIdempotentRequests() throws Exception
|
||||
{
|
||||
Fixture fixture = fixture();
|
||||
fixture.service().inventory(fixture.device(), new JSONArray().put(new JSONObject().put("printerId", "laser")
|
||||
.put("name", "Packing test printer").put("formats", new JSONArray(List.of("PDF", "ZPL")))));
|
||||
QRecord printer = PrintingProcesses.resolvePrinter(fixture.printerId(), fixture.device());
|
||||
String nativeJob = PrintingProcesses.submitTest(printer, false, "LABEL_4X6_300");
|
||||
JSONObject request = new JSONObject().put("printerId", "laser").put("format", "ZPL").put("testProfile", "LABEL_4X6_300").put("requestId", "label-profile-test");
|
||||
String deviceJob = fixture.service().testPrint(fixture.device(), request).getString("jobId");
|
||||
assertEquals(fixture.service().getJob(nativeJob).getString("contentSha256"), fixture.service().getJob(deviceJob).getString("contentSha256"));
|
||||
assertEquals(deviceJob, fixture.service().testPrint(fixture.device(), request).getString("jobId"));
|
||||
assertEquals(409, assertThrows(PrintingException.class, () -> fixture.service().testPrint(fixture.device(), request.put("testProfile", "LABEL_4X6_203"))).getStatus());
|
||||
assertEquals(nativeJob, fixture.service().claim(fixture.device()).getJSONObject("job").getString("jobId"));
|
||||
assertArrayEquals(PrintingTestDocuments.zpl(300), fixture.service().content(fixture.device(), nativeJob));
|
||||
assertEquals(400, assertThrows(PrintingException.class, () -> fixture.service().testPrint(fixture.device(), request.put("testProfile", "LETTER"))).getStatus());
|
||||
assertThrows(PrintingException.class, () -> fixture.service().testPrint(fixture.device(), request.put("testProfile", JSONObject.NULL)));
|
||||
assertThrows(PrintingException.class, () -> fixture.service().testPrint(fixture.device(), request.put("format", "RAW")));
|
||||
assertThrows(QException.class, () -> PrintingProcesses.submitTest(printer, false, "A4"));
|
||||
assertEquals(1, fixture.service().queuedJobs(fixture.device()).length());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Stock/resolution choices are present before both native review flows.
|
||||
***************************************************************************/
|
||||
@Test
|
||||
void nativeFormsOfferStockAndResolutionBeforeSelectedPrinterReview() throws Exception
|
||||
{
|
||||
Fixture fixture = fixture();
|
||||
QFrontendStepMetaData single = (QFrontendStepMetaData) fixture.instance().getProcess("printingTestZpl").getStep("details");
|
||||
assertEquals("printing_testProfileZpl", single.getFormField("testProfile").getPossibleValueSourceName());
|
||||
var selected = fixture.instance().getProcess("printingTestSelectedPdf");
|
||||
assertEquals("testProfile", selected.getStepList().getFirst().getName());
|
||||
QFrontendStepMetaData form = (QFrontendStepMetaData) selected.getStep("testProfile");
|
||||
assertEquals("printing_testProfilePdf", form.getFormField("testProfile").getPossibleValueSourceName());
|
||||
assertEquals(3, fixture.instance().getPossibleValueSource("printing_testProfileZpl").getEnumValues().size());
|
||||
assertEquals(5, fixture.instance().getPossibleValueSource("printing_testProfilePdf").getEnumValues().size());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Native record selection requires duplicate consent and preserves the source.
|
||||
***************************************************************************/
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* 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.printing;
|
||||
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.rendering.PDFRenderer;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Manual local preview export only; this utility never connects to a printer.
|
||||
*******************************************************************************/
|
||||
public final class PrintingTestDocumentsExport
|
||||
{
|
||||
/***************************************************************************
|
||||
** Write diagnostic artifacts to the caller's explicit preview directory.
|
||||
***************************************************************************/
|
||||
public static void main(String[] args) throws Exception
|
||||
{
|
||||
if(args.length != 1)
|
||||
{
|
||||
throw new IllegalArgumentException("Pass one local preview output directory");
|
||||
}
|
||||
Path directory = Path.of(args[0]).toAbsolutePath();
|
||||
Files.createDirectories(directory);
|
||||
for(TestPrintProfile profile : TestPrintProfile.values())
|
||||
{
|
||||
byte[] pdf = PrintingTestDocuments.pdf(profile);
|
||||
Files.write(directory.resolve(profile.name() + ".pdf"), pdf);
|
||||
try(PDDocument document = PDDocument.load(pdf))
|
||||
{
|
||||
ImageIO.write(new PDFRenderer(document).renderImageWithDPI(0, 144), "png", directory.resolve(profile.name() + "-pdf.png").toFile());
|
||||
}
|
||||
if(profile.getDpi() > 0)
|
||||
{
|
||||
Files.write(directory.resolve(profile.name() + ".zpl"), PrintingTestDocuments.zpl(profile));
|
||||
ImageIO.write(PrintingTestDocuments.labelImage(profile.getDpi()), "png", directory.resolve(profile.name() + "-raster.png").toFile());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
/*
|
||||
* 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.printing;
|
||||
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HexFormat;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||
import org.apache.pdfbox.text.PDFTextStripper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||
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;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Verify portable font/media content and bounded monochrome printer payloads.
|
||||
*******************************************************************************/
|
||||
class PrintingTestDocumentsTest
|
||||
{
|
||||
/***************************************************************************
|
||||
** Every PDF profile has searchable diagnostic text and an embedded font.
|
||||
***************************************************************************/
|
||||
@Test
|
||||
void pdfProfilesEmbedFontAndUsePhysicalMedia() throws Exception
|
||||
{
|
||||
for(TestPrintProfile profile : TestPrintProfile.values())
|
||||
{
|
||||
byte[] bytes = PrintingTestDocuments.pdf(profile);
|
||||
assertArrayEquals(bytes, PrintingTestDocuments.pdf(profile), "Identical requests must preserve the content hash");
|
||||
try(PDDocument document = PDDocument.load(bytes))
|
||||
{
|
||||
assertEquals(1, document.getNumberOfPages());
|
||||
PDRectangle size = document.getPage(0).getMediaBox();
|
||||
PDRectangle expected = profile.getDpi() > 0 ? new PDRectangle(288, 432) : (profile == TestPrintProfile.A4 ? PDRectangle.A4 : PDRectangle.LETTER);
|
||||
assertEquals(expected.getWidth(), size.getWidth(), 0.01);
|
||||
assertEquals(expected.getHeight(), size.getHeight(), 0.01);
|
||||
String text = new PDFTextStripper().getText(document);
|
||||
assertTrue(text.contains("QQQ"));
|
||||
assertTrue(text.contains("TEST PRINT - NOT POSTAGE"));
|
||||
assertTrue(text.contains("0O 1Il 5S"));
|
||||
assertTrue(text.contains("25.4 mm"));
|
||||
var resources = document.getPage(0).getResources();
|
||||
int fonts = 0;
|
||||
for(var name : resources.getFontNames())
|
||||
{
|
||||
fonts++;
|
||||
assertTrue(resources.getFont(name).isEmbedded());
|
||||
assertTrue(resources.getFont(name).getName().contains("JetBrainsMono"));
|
||||
}
|
||||
assertEquals(1, fonts);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** All supported densities preserve inches and carry valid bounded GFA data.
|
||||
***************************************************************************/
|
||||
@Test
|
||||
void labelDensitiesCarryExactBitmapAndNativeBarcodeWithoutPersistentDownloads() throws Exception
|
||||
{
|
||||
for(int dpi : new int[] {203, 300, 600})
|
||||
{
|
||||
byte[] bytes = PrintingTestDocuments.zpl(dpi);
|
||||
assertTrue(bytes.length < 3 * 1024 * 1024);
|
||||
String zpl = new String(bytes, StandardCharsets.US_ASCII);
|
||||
int width = dpi * 4;
|
||||
int height = dpi * 6;
|
||||
int rowBytes = (width + 7) / 8;
|
||||
assertTrue(zpl.startsWith("^XA^PW" + width + "^LL" + height + "^LH0,0"));
|
||||
byte[] bitmap = reconstructGraphicStrips(zpl, rowBytes, height);
|
||||
BufferedImage expected = PrintingTestDocuments.labelImage(dpi);
|
||||
for(int y = 0; y < height; y++)
|
||||
{
|
||||
for(int x = 0; x < rowBytes * 8; x++)
|
||||
{
|
||||
boolean black = (bitmap[y * rowBytes + x / 8] & (0x80 >>> (x % 8))) != 0;
|
||||
boolean expectedBlack = x < width && (expected.getRGB(x, y) & 0xffffff) == 0;
|
||||
assertEquals(expectedBlack, black, "Reconstructed raster and row padding must match");
|
||||
}
|
||||
}
|
||||
assertEquals(0, bitmap[0]);
|
||||
assertEquals(0, bitmap[bitmap.length - 1]);
|
||||
assertTrue(zpl.contains("^FD" + PrintingTestDocuments.BARCODE_VALUE + "^FS"));
|
||||
Matcher barcode = Pattern.compile("\\^FO(\\d+),(\\d+)\\^BY(\\d+)\\^BCN,(\\d+),N,N,N").matcher(zpl);
|
||||
assertTrue(barcode.find());
|
||||
int barcodeX = Integer.parseInt(barcode.group(1));
|
||||
int barcodeY = Integer.parseInt(barcode.group(2));
|
||||
int module = Integer.parseInt(barcode.group(3));
|
||||
int barcodeHeight = Integer.parseInt(barcode.group(4));
|
||||
int maximumModules = 11 * (PrintingTestDocuments.BARCODE_VALUE.length() + 2) + 13;
|
||||
assertTrue(barcodeX >= 10 * module);
|
||||
assertTrue(barcodeX + (maximumModules + 10) * module < width - dpi / 8);
|
||||
assertTrue(barcodeY > 258 * dpi / 72);
|
||||
assertTrue(barcodeY + barcodeHeight < 330 * dpi / 72);
|
||||
assertTrue(zpl.endsWith("^PQ1^XZ"));
|
||||
assertFalse(zpl.contains("~D"));
|
||||
assertFalse(zpl.contains("^A0"));
|
||||
}
|
||||
assertArrayEquals(PrintingTestDocuments.zpl(203), PrintingTestDocuments.zpl());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Parse actual wire fields and reject any missing, overlapping or oversized
|
||||
** strip before comparing the complete bitmap with the rendered source.
|
||||
***************************************************************************/
|
||||
private static byte[] reconstructGraphicStrips(String zpl, int rowBytes, int height)
|
||||
{
|
||||
Matcher matcher = Pattern.compile("\\^FO0,(\\d+)\\^GFA,(\\d+),(\\d+),(\\d+),([0-9A-F]+)\\^FS").matcher(zpl);
|
||||
byte[] bitmap = new byte[rowBytes * height];
|
||||
int nextRow = 0;
|
||||
int strips = 0;
|
||||
while(matcher.find())
|
||||
{
|
||||
strips++;
|
||||
int firstRow = Integer.parseInt(matcher.group(1));
|
||||
int length = Integer.parseInt(matcher.group(2));
|
||||
assertEquals(nextRow, firstRow);
|
||||
assertTrue(length > 0 && length <= 99999);
|
||||
assertEquals(length, Integer.parseInt(matcher.group(3)));
|
||||
assertEquals(rowBytes, Integer.parseInt(matcher.group(4)));
|
||||
assertEquals(0, length % rowBytes);
|
||||
assertEquals(length * 2, matcher.group(5).length());
|
||||
byte[] strip = HexFormat.of().parseHex(matcher.group(5));
|
||||
assertTrue(firstRow * rowBytes + strip.length <= bitmap.length);
|
||||
System.arraycopy(strip, 0, bitmap, firstRow * rowBytes, strip.length);
|
||||
nextRow += length / rowBytes;
|
||||
}
|
||||
assertEquals(height, nextRow);
|
||||
assertTrue(strips > 1);
|
||||
assertEquals(strips, zpl.split("\\^GFA,", -1).length - 1);
|
||||
return (bitmap);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Visible text and border fit inside quiet margins at each device density.
|
||||
***************************************************************************/
|
||||
@Test
|
||||
void rasterArtworkLeavesPhysicalMargins() throws Exception
|
||||
{
|
||||
for(int dpi : new int[] {203, 300, 600})
|
||||
{
|
||||
BufferedImage image = PrintingTestDocuments.labelImage(dpi);
|
||||
int black = 0;
|
||||
int margin = dpi / 8;
|
||||
for(int y = 0; y < image.getHeight(); y++)
|
||||
{
|
||||
for(int x = 0; x < image.getWidth(); x++)
|
||||
{
|
||||
if((image.getRGB(x, y) & 0xffffff) == 0)
|
||||
{
|
||||
black++;
|
||||
assertTrue(x >= margin && x < image.getWidth() - margin && y >= margin && y < image.getHeight() - margin);
|
||||
}
|
||||
}
|
||||
}
|
||||
assertTrue(black > dpi * dpi / 2);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Invalid combinations fail before a job can be created.
|
||||
***************************************************************************/
|
||||
@Test
|
||||
void profilesRejectWrongFormatStockAndDensity() throws Exception
|
||||
{
|
||||
assertEquals(TestPrintProfile.LETTER, TestPrintProfile.resolve("PDF", null));
|
||||
assertEquals(TestPrintProfile.LABEL_4X6_203, TestPrintProfile.resolve("ZPL", null));
|
||||
assertEquals(TestPrintProfile.A4, TestPrintProfile.resolve("PDF", "A4"));
|
||||
assertEquals(400, assertThrows(PrintingException.class, () -> TestPrintProfile.resolve("ZPL", "LETTER")).getStatus());
|
||||
assertThrows(PrintingException.class, () -> TestPrintProfile.resolve("ZPL", "A4"));
|
||||
assertThrows(PrintingException.class, () -> TestPrintProfile.resolve("PDF", "UNKNOWN"));
|
||||
assertThrows(PrintingException.class, () -> TestPrintProfile.resolve("RAW", null));
|
||||
assertThrows(IllegalArgumentException.class, () -> PrintingTestDocuments.zpl(200));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user