WIP: feat(renderer): align a fitted ZPL barcode inside its fit box; justified font 0 text blocks #11
No files matched your search
+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}
|
||||
|
||||
@@ -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