feat(renderer): scene 2 barcodes: whole-dot module fit, alignment in the box, decode tests (P3a) #18
No files matched your search
@@ -33,7 +33,7 @@ import com.kingsrook.qbits.documentgeneration.GenerationPorts.FailureCode;
|
||||
** | Renderer | Answers | Refusals it may name | Version 2 scene, render target, preview |
|
||||
** |----------|--------------------------------------------|-------------------------------|-----------------------------------------|
|
||||
** | 0.1.0 | the released fields only | the 13 released codes | refused: `RENDERER_TOO_OLD` |
|
||||
** | 0.2.0 | the same, with `diagnostics` (and `detail`) | those and the 15 scene codes | rendered |
|
||||
** | 0.2.0 | the same, with `diagnostics` (and `detail`) | those and the 17 scene codes | rendered |
|
||||
**
|
||||
** Each set is written out. A range of the enum would take in whatever is added
|
||||
** between its ends, and a worker would gain a refusal nobody decided it has.
|
||||
@@ -53,7 +53,8 @@ enum RendererVersion
|
||||
FailureCode.SCANNER_BARCODE_CHECKSUM_MISMATCH, FailureCode.UNSUPPORTED_BARCODE_OPTION,
|
||||
FailureCode.SCENE_INVALID, FailureCode.STOCK_MISMATCH, FailureCode.TARGET_REQUIRES_SCENE_2, FailureCode.ELEMENT_OUTSIDE_PRINTABLE_AREA, FailureCode.TEXT_DOES_NOT_FIT,
|
||||
FailureCode.VALUE_MISSING, FailureCode.VALUE_TOO_LONG, FailureCode.FONT_UNKNOWN, FailureCode.FONT_MISSING_GLYPH, FailureCode.RESERVED_FONT_NAME,
|
||||
FailureCode.BARCODE_DOES_NOT_FIT, FailureCode.GS1_INVALID, FailureCode.ASSET_UNKNOWN, FailureCode.PREVIEW_TOO_LARGE, FailureCode.RENDERER_TOO_OLD));
|
||||
FailureCode.BARCODE_DOES_NOT_FIT, FailureCode.GS1_INVALID, FailureCode.ASSET_UNKNOWN, FailureCode.PREVIEW_TOO_LARGE, FailureCode.RENDERER_TOO_OLD,
|
||||
FailureCode.VALUE_NOT_ENCODABLE, FailureCode.QUIET_ZONE_INTRUDED));
|
||||
|
||||
private final String wire;
|
||||
private final boolean structured;
|
||||
|
||||
@@ -60,6 +60,7 @@ final class SceneFacts
|
||||
"thickness", "fill",
|
||||
"value", "valuePath", "font", "size", "lineHeight", "fit", "maxLines", "shrinkTo", "overflow", "maxLength", "threshold", "reverse", "rotation",
|
||||
"symbology", "symbologyPath", "allowedSymbologies", "minModule", "maxModule", "quiet", "gs1", "hri", "scannerSymbologyPath", "aimIdentifierPath",
|
||||
"barcodeProfile", "minHeight",
|
||||
"asset", "mode");
|
||||
|
||||
private static final SceneFacts NONE = new SceneFacts(false, Set.of());
|
||||
|
||||
@@ -185,7 +185,7 @@ class BridgeTest
|
||||
{
|
||||
var released = java.util.EnumSet.range(GenerationPorts.FailureCode.BARCODE_FIT_TOO_SMALL, GenerationPorts.FailureCode.UNSUPPORTED_BARCODE_OPTION);
|
||||
assertEquals(13, released.size());
|
||||
assertEquals(28, GenerationPorts.FailureCode.values().length);
|
||||
assertEquals(30, GenerationPorts.FailureCode.values().length);
|
||||
for(var code : GenerationPorts.FailureCode.values())
|
||||
{
|
||||
var bridge = new IsolatedRendererBridge(config(directory), (actor, asset, maximum) -> new byte[0], (id, input, maximum) ->
|
||||
|
||||
@@ -60,7 +60,7 @@ class SceneFactsTest
|
||||
Set<String> declared = new TreeSet<>();
|
||||
names(RendererJson.JSON.readTree(Files.readAllBytes(Path.of("..", "..", "renderer", "schema", "zpl-scene-2.schema.json"))), declared);
|
||||
assertEquals(declared, new TreeSet<>(SceneFacts.VOCABULARY));
|
||||
assertEquals(46, SceneFacts.VOCABULARY.size());
|
||||
assertEquals(48, SceneFacts.VOCABULARY.size());
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -209,7 +209,9 @@ negative. An omitted `width` or `height` means the rest of the container on that
|
||||
|
||||
**Where the content sits in the box (`align`, `valign`).** `align` is `left`, `center` or `right`; `valign` is `top`,
|
||||
`middle` or `bottom`. They apply to text lines, to a fitted barcode whose size depends on its data, and to a scaled
|
||||
image. Slack is split with integer division, so a centred element is off by at most one dot.
|
||||
image. Slack is split with integer division, so a centred element is off by at most one dot. The defaults are `left`
|
||||
and `top`, except for a barcode, which sits in the middle of its box (`center`, `middle`) unless the scene says
|
||||
otherwise: what its box has to spare is then quiet zone, the same on both sides (decided 2026-11-12).
|
||||
|
||||
`rotation` is 0, 90, 180 or 270 degrees clockwise for text, barcodes and images. The box is given as it appears on
|
||||
the label, after rotation.
|
||||
@@ -464,14 +466,19 @@ A plain `value` in bracket syntax keeps working, so the first release does not d
|
||||
|
||||
- Bars are whole dots. The renderer picks the largest module, in dots, that fits the box on both axes including
|
||||
quiet zones, never smaller than `minModule` and never larger than `maxModule` if given.
|
||||
- `minModule` is physical. Defaults: 0.25 mm for linear symbologies and 0.375 mm for two-dimensional ones. That is
|
||||
2 and 3 dots at 203 dpi, 3 and 5 at 300 dpi, 6 and 9 at 600 dpi. These defaults are proposals to confirm on a
|
||||
printer (section 10.6).
|
||||
- `minModule` is physical. Without it the smallest module is that of the element's `barcodeProfile`, by symbology
|
||||
(the table below). The design's own 0.25 mm for linear symbologies and 0.375 mm for two-dimensional ones (2 and
|
||||
3 dots at 203 dpi, 3 and 5 at 300 dpi, 6 and 9 at 600 dpi) are proposals to confirm on a printer (section 10.6).
|
||||
- For linear symbologies the bar height fills the box height (less the human-readable line) and no longer scales
|
||||
with the module. In version 1 the height scales with the module, so a short box caps the module.
|
||||
- The fitted symbol is placed in its box by `align` and `valign`. Left over dots are blank.
|
||||
- `quiet` is modules on each side, with the existing minimums (10 linear, 4 matrix). Quiet zones are inside the
|
||||
box and are never cropped. The warning `QUIET_ZONE_INTRUDED` fires when another element's ink lands in one.
|
||||
with the module. In version 1 the height scales with the module, so a short box caps the module. The box must
|
||||
be as tall as the symbology asks of bars that wide; the scene may state its own `minHeight`, with a standing
|
||||
warning.
|
||||
- The fitted symbol is placed in its box by `align` and `valign`, which for a barcode default to the middle
|
||||
(section 2.4). Left over dots are blank.
|
||||
- `quiet` is modules on each side. Without it each symbology has its own quiet zones (10 and 10 for Code 128,
|
||||
Code 39, Code 93 and Interleaved 2 of 5; 11 and 7 for EAN-13; 7 and 7 for EAN-8; 9 and 9 for UPC-A; 9 and 7 for
|
||||
UPC-E; 4 on every side of a two-dimensional symbol). Quiet zones are inside the box and are never cropped. An
|
||||
element painted later whose ink would enter the symbol or a quiet zone is refused as `QUIET_ZONE_INTRUDED`.
|
||||
|
||||
### 4.4 When the data does not fit
|
||||
|
||||
@@ -501,6 +508,113 @@ rebuilds the bitmap from its graphic fields, and decodes it with ZXing (already
|
||||
payload, the symbology, the module size in dots and the blank margins. This runs at 203, 300 and 600 dpi for each
|
||||
fit-qualified symbology, for short, typical and longest values.
|
||||
|
||||
**Decided while building the barcode fit (P3a).**
|
||||
|
||||
- **Where the modules come from.** The symbol is taken from the version 1 encoder path at one dot to the module
|
||||
(`Barcodes.renderForFormat`, the call `BarcodeFit.fit` itself measures with), and `scene2/barcode/ModuleFit`
|
||||
chooses the module. `BarcodeFit.fit` is not called: it scales the bar height with the module and stops at a
|
||||
scale of 16, which is what 4.3 ends. `BarcodeFit.TYPES` remains the list of fit-qualified symbologies, and
|
||||
version 1 is untouched.
|
||||
- **The rule, in dots.** `module = min(maxModule, floor(box / modules))`, the modules counting both quiet zones;
|
||||
a two-dimensional symbol takes the smaller of its two axes. The symbol does not fit when that is under
|
||||
`minModule`. Both limits go up to a whole dot (`Length.module`), so a design that states the same length for
|
||||
both gets one module at every resolution; a maximum under the minimum is `SCENE_INVALID` at `maxModule`.
|
||||
Going up to a whole dot costs most for a two-dimensional symbol at 300 dpi, where 0.375 mm becomes 5 dots
|
||||
(0.423 mm).
|
||||
- **`pdf417` is fitted as a two-dimensional symbol** (both axes, 4 quiet modules, the 0.375 mm default), the
|
||||
grouping version 1 already uses for quiet zones. The default Data Matrix encoder may choose a rectangular
|
||||
symbol; it is fitted as it comes.
|
||||
- **The symbol is its own ground.** The fitted symbol's area, quiet zones included, is painted white and then
|
||||
the bars, so ink painted under it cannot close a space. The rest of the box is left as it was.
|
||||
- **Refusals that depend on the record.** A bound value, symbology or scanner profile that is absent is
|
||||
`VALUE_MISSING` with the pointer of the binding property. A symbology outside `allowedSymbologies`, and a
|
||||
scanner profile that does not name the symbology drawn, are `SCENE_INVALID` at that property. What the encoder
|
||||
says about a value is dropped. (The code of a value that cannot be encoded changed after the review, below.)
|
||||
- **The numbers of a refusal.** `BARCODE_DOES_NOT_FIT` carries `measured.modules`, `quietLeftModules`,
|
||||
`quietRightModules`, `widthDots` and `heightDots` (needed at the minimum) and `moduleDots` (the largest module
|
||||
that would fit), against `limit.widthDots`, `heightDots` and `minModuleDots`. For a symbol turned a quarter
|
||||
they are the symbol's own width and height.
|
||||
- **Left to P3b.** `gs1` and `hri` stay declared, and an element that states either is refused as
|
||||
`RENDERER_TOO_OLD`. A `gs1-128` `value` is in the square brackets the encoder reads; the bracketed form a
|
||||
person reads, in parentheses, is the caption's.
|
||||
|
||||
**Decided after the independent review of P3a.** A label is scanned, so the failure that matters most is a symbol
|
||||
that reads as something other than the value it was given. The review found four ways the encoder made one, and
|
||||
that nothing kept ink off a symbol.
|
||||
|
||||
- **No value holds an escape of the encoder.** The encoder reads `\<FNC1>` to `\<FNC4>` in a value as function
|
||||
characters. A record value could so make a GS1-128, with application identifiers of its own choosing, of a
|
||||
plain Code 128; set the reader-programming flag; or change a character; and QR drops the escape, so the symbol
|
||||
differs from the value. A value, literal or bound, that holds the two characters `\<` is refused in every
|
||||
symbology. A function character enters a symbol only through its symbology: `gs1-128` adds its own FNC1.
|
||||
- **Interleaved 2 of 5 of an odd number of digits is refused**, where the encoder put a zero in front.
|
||||
- **EAN and UPC values are the whole number.** The encoder wants the number without its check digit, works the
|
||||
digit out and pads a short number with zeros, so a mistyped number printed as a valid symbol for another
|
||||
product. A value is now 13 digits for `ean13`, 8 for `ean8`, 12 for `upca` and 8 for `upce`, check digit
|
||||
included; the check digit is verified and the encoder is given the digits before it. The short form the encoder
|
||||
takes is not accepted: nothing in this design asks for it, and it is the form that cannot be checked.
|
||||
- **The check digit of every GS1 key is verified** in a `gs1-128` element string. The encoder checks lengths and
|
||||
not check digits. The application identifiers are those the GS1 Syntax Dictionary marks with a check digit, as
|
||||
the bundled Zint 2.16.0 carries it (`gs1_lint.h`): 00, 01, 02, 03, 253, 255, 402, 410 to 417, 8003, 8006,
|
||||
8017, 8018 and 8026. The pair of check characters of 8013 and 8014 is not verified. The builder of section 4.2
|
||||
will meet the same rule here, not a second one.
|
||||
- **A code for a value that cannot be drawn.** `VALUE_NOT_ENCODABLE` joins the errors, at `value` or `valuePath`:
|
||||
for the four refusals above (`GS1_INVALID` for a `gs1-128`), for a value the symbology cannot hold, and for a
|
||||
value a scanner profile cannot reproduce. It carries numbers only: `measured.characters`; `limit.characters` or
|
||||
`limit.charactersMultipleOf`; `checkDigitMatches` 0 against 1; and for a GS1 key `measured.applicationIdentifier`,
|
||||
one of the fixed list above. `SCENE_INVALID` is no longer used for a value. A symbol of more modules than the
|
||||
encoder draws (4096) is `BARCODE_DOES_NOT_FIT`, with `measured.characters` against `limit.modules`.
|
||||
- **Quiet zones and the smallest module are a table by symbology**, in one place (`scene2/barcode/Symbology`),
|
||||
each number with its source. Quiet zones are the symbology's own (section 4.3). The smallest module is chosen
|
||||
by a named profile, `barcodeProfile`:
|
||||
|
||||
| `barcodeProfile` | EAN-13, EAN-8, UPC-A, UPC-E | GS1-128, Interleaved 2 of 5 | Code 128, Code 39, Code 93 | two-dimensional |
|
||||
|---|---|---|---|---|
|
||||
| `none` (the design's own) | 0.25 mm | 0.25 mm | 0.25 mm | 0.375 mm |
|
||||
| `gs1-general` (GS1, general retail point of sale) | 0.264 mm | 0.25 mm | 0.25 mm | 0.375 mm |
|
||||
| `gs1-logistics` (GS1, general distribution and logistic units) | 0.495 mm | 0.495 mm | 0.25 mm | 0.375 mm |
|
||||
| not stated | as `gs1-general` | as `gs1-logistics` | as `none` | as `none` |
|
||||
|
||||
With no profile stated a symbology takes the strictest GS1 rule written for its own use. 0.264 mm is 3, 4 and 7
|
||||
dots at 203, 300 and 600 dpi, and 0.495 mm is 4, 6 and 12. A scene may still state a smaller `minModule`, with
|
||||
the standing warning. **The consequence for a GS1-128 of a GTIN, a date and a serial number** (44 characters as
|
||||
a person reads them, 299 modules of bars and 319 with quiet zones): at the 4 dots of a logistic unit at 203 dpi
|
||||
it is 1276 dots, 160 mm, and fits no label 4 in wide (812 dots with no margins), nor the 6 in of 4 x 6 in stock
|
||||
(1218 dots). With no profile stated such a label is refused. Named `gs1-general` or `none` it is drawn at 2
|
||||
dots, 0.25 mm, 638 dots; 3 dots would need 957. Whether such a label is printed below the GS1 minimum for
|
||||
logistic units, on wider stock or as a two-dimensional symbol is a decision for whoever owns the label. The
|
||||
example of section 2.1 states `minModule: 0.25mm` and no profile: it is drawn as before and now carries
|
||||
`BARCODE_MODULE_BELOW_RECOMMENDED`; naming `gs1-general` is how such a scene says what it means.
|
||||
- **Bars have a least height.** A linear symbol filled whatever height its box had, down to one dot. Its bars
|
||||
must now be as tall as its symbology asks of bars that wide: 15% of the width of the bars and never under 5 mm
|
||||
for Code 128 and GS1-128 (ISO/IEC 15417, Annex G), and by the same rule for Code 39, Code 93 and Interleaved 2
|
||||
of 5, whose own specifications were not read; 69.24 modules for EAN-13, UPC-A and UPC-E and 55.24 for EAN-8
|
||||
(GS1 General Specifications, the nominal 22.85 mm and 18.23 mm at a 0.330 mm module). The height is part of the
|
||||
fit: a module that would make the symbol too wide for the height of its box is not chosen, so a short value in
|
||||
a wide box is not refused for being short, and a box too short at the smallest module is
|
||||
`BARCODE_DOES_NOT_FIT` with the height needed. The author who accepts less states `minHeight`, and the label
|
||||
then carries the warning `BARCODE_HEIGHT_BELOW_RECOMMENDED`. The 31.75 mm GS1 asks of a symbol on a logistic
|
||||
unit is not enforced by any profile: no label 1 in high could have it. The 44-character GS1-128 at 2 dots needs
|
||||
90 dots, 11.3 mm. An EAN-13 at 3 dots needs 208 dots, 26 mm, more than a label 1 in high has.
|
||||
- **Nothing is painted over a symbol.** `QUIET_ZONE_INTRUDED` is an error, raised by geometry when the scene is
|
||||
validated and when it is rendered: an element later in the list whose ink (its box; for an outline, its four
|
||||
bars; for a group, nothing) would enter the symbol or its quiet zones is refused, with the barcode's pointer
|
||||
and both sets of edges. A panel of reverse text can no longer turn a symbol white on black. An element painted
|
||||
before the barcode is cleared by the symbol's own ground and stays allowed. While a scene is validated and the
|
||||
record gives the value or the symbology, the symbol is not known and the barcode keeps its whole box.
|
||||
- **A barcode sits in the middle of its box** by default (section 2.4), unlike other elements.
|
||||
- **A declared length proves a bound barcode.** A barcode takes `maxLength`. For a linear symbology validation
|
||||
fits the widest symbol a value of that length can need (Code 128: 11 modules for each character and 35, for
|
||||
values of characters from space to tilde; the other widths are in the table) and refuses the scene when that
|
||||
does not fit; a longer value is `VALUE_TOO_LONG` when printing. The EAN and UPC symbols are one width and need
|
||||
no length. A literal value is fitted in every symbology the record may name. `FIT_NOT_PROVEN` is left for a
|
||||
bound value with no length, and for a two-dimensional symbol: the encoders choose among shapes (Data Matrix,
|
||||
PDF417) or change mode at a cost (QR, Aztec), so no value of a length is the largest symbol of that length,
|
||||
which tests hold them to. A two-dimensional symbol with a length is fitted for that many bytes, which refuses a
|
||||
box they cannot fit and proves no more. The widest GS1-128 of a length is one of letters, about 1.7 times the
|
||||
width of a label of digits, so a declared length rarely proves a GS1-128 written as a `value`; the builder of
|
||||
section 4.2, which knows each identifier, can.
|
||||
|
||||
## 5. Graphics
|
||||
|
||||
- **Image**: an asset (PNG or JPEG, as today) in a physical box. `fit` is `contain` (scale to the box, keep the
|
||||
@@ -784,8 +898,8 @@ diagnostic is closed vocabulary and numbers only:
|
||||
|
||||
| Severity | Codes (first release) |
|
||||
|---|---|
|
||||
| ERROR | `SCENE_INVALID` (with the JSON pointer of the offending property), `STOCK_MISMATCH`, `TARGET_REQUIRES_SCENE_2`, `ELEMENT_OUTSIDE_PRINTABLE_AREA`, `TEXT_DOES_NOT_FIT`, `VALUE_MISSING`, `VALUE_TOO_LONG`, `FONT_UNKNOWN`, `FONT_MISSING_GLYPH`, `RESERVED_FONT_NAME`, `BARCODE_DOES_NOT_FIT`, `GS1_INVALID`, `ASSET_UNKNOWN`, `PREVIEW_TOO_LARGE`, `RENDERER_TOO_OLD` |
|
||||
| WARNING | `TEXT_TRUNCATED`, `TEXT_SMALL_FOR_DPI`, `TEXT_LIGHT_FACE`, `FIT_NOT_PROVEN`, `BARCODE_MODULE_BELOW_RECOMMENDED`, `QUIET_ZONE_INTRUDED`, `ELEMENTS_OVERLAP`, `IMAGE_UPSCALED`, `DEVICE_UNIT_USED`, `PRINTER_FONT_NOT_SHOWN` |
|
||||
| ERROR | `SCENE_INVALID` (with the JSON pointer of the offending property), `STOCK_MISMATCH`, `TARGET_REQUIRES_SCENE_2`, `ELEMENT_OUTSIDE_PRINTABLE_AREA`, `TEXT_DOES_NOT_FIT`, `VALUE_MISSING`, `VALUE_TOO_LONG`, `FONT_UNKNOWN`, `FONT_MISSING_GLYPH`, `RESERVED_FONT_NAME`, `BARCODE_DOES_NOT_FIT`, `GS1_INVALID`, `ASSET_UNKNOWN`, `PREVIEW_TOO_LARGE`, `RENDERER_TOO_OLD`, `VALUE_NOT_ENCODABLE`, `QUIET_ZONE_INTRUDED` |
|
||||
| WARNING | `TEXT_TRUNCATED`, `TEXT_SMALL_FOR_DPI`, `TEXT_LIGHT_FACE`, `FIT_NOT_PROVEN`, `BARCODE_MODULE_BELOW_RECOMMENDED`, `BARCODE_HEIGHT_BELOW_RECOMMENDED`, `ELEMENTS_OVERLAP`, `IMAGE_UPSCALED`, `DEVICE_UNIT_USED`, `PRINTER_FONT_NOT_SHOWN` |
|
||||
| INFO | `TEXT_SHRUNK` (from and to size), `TEXT_WRAPPED` (lines), `BARCODE_MODULE` (dots and micrometres), `IMAGE_SCALED` |
|
||||
|
||||
Version 1 scenes keep their existing codes and the opaque `RENDER_FAILED`.
|
||||
|
||||
@@ -28,20 +28,31 @@ crashes:
|
||||
- **Version 2 scene parser.** The target is the worker's own routing, so a mutated source may also become a
|
||||
version 1 scene, which is then held to the version 1 property. An accepted version 2 scene, read by the
|
||||
second parser, has only the declared top-level properties, a physical design size, at most 1000 elements
|
||||
with unique ids of the fixed pattern, only `box`, `group` and `text` elements with only their declared
|
||||
properties (barcodes and images are refused until their packages exist), groups at most four deep, every
|
||||
with unique ids of the fixed pattern, only `box`, `group`, `text` and `barcode` elements with only their
|
||||
declared properties (images are refused until their package exists), groups at most four deep, every
|
||||
length a number and a unit, and overrides only for the three resolutions and for declared ids. A `text`
|
||||
element is read as it stands at each resolution (the defaults, then the element, then the overrides for that
|
||||
resolution): it names a bundled face, has a physical size from 1 to 1000 pt, exactly one of a literal value
|
||||
and a pointer into the record, and only fit, length, threshold, rotation, reverse and line height settings of
|
||||
the documented kinds and ranges. The corpus holds the three text cases (`text-faces`, `text-fit`,
|
||||
`text-turned`) beside the layout cases, and the spliced tokens include the text properties, a combining
|
||||
mark, a tab, half of a surrogate pair, a size of `1000pt` and the number `1e400`. About one version 2 input in
|
||||
the documented kinds and ranges. A `barcode` element is read the same way (the element, then the overrides):
|
||||
it states one fit-qualified symbology or a pointer with a list of distinct ones, exactly one of a literal
|
||||
value of 1 to 4000 characters that holds no encoder escape and a pointer into the record, a scanner profile
|
||||
whole or not at all, module sizes and a minimum height that are more than nothing, a quiet zone from the
|
||||
widest minimum of its symbologies to 64, a `maxLength` from 1 to 4000, one of the three named profiles, a
|
||||
quarter-turn rotation, and neither `gs1` nor `hri`. The corpus holds the three text cases (`text-faces`,
|
||||
`text-fit`, `text-turned`) and the three barcode cases (`barcode-linear`, `barcode-matrix`, `barcode-gs1`)
|
||||
beside the layout cases. The spliced tokens include the text properties, a combining mark, a tab, half of a
|
||||
surrogate pair, a size of `1000pt` and the number `1e400`; and the barcode properties, symbology names in and
|
||||
out of the fit-qualified set, the profile names, an element string, a GTIN, the encoder's escape for a
|
||||
function character and characters outside Latin-1. A barcode encoder's own exception is not a refusal: a
|
||||
mutated value must end as a `Refusal` or as a label. The campaign asserts that it rendered labels holding a
|
||||
barcode. About one version 2 input in
|
||||
four, chosen by its bytes so that a replay does the same, is rendered instead of validated: the label is read
|
||||
back into a page, must be only the header, uncompressed graphic fields and the end of a label, must put no ink
|
||||
outside the printable area, and validation of the same input must give the same answer (accepted with the same
|
||||
findings, or refused with the same diagnostic). The one difference allowed is `VALUE_MISSING`: a text bound to
|
||||
the record has no value in the campaign's empty record, and validation reads no record. A refusal is the scene's own `Refusal`, the exception that
|
||||
findings, or refused with the same diagnostic). The one difference allowed is `VALUE_MISSING`: a text or a
|
||||
barcode bound to the record has no value in the campaign's empty record, and validation reads no record. A
|
||||
refusal is the scene's own `Refusal`, the exception that
|
||||
carries a diagnostic, and nothing else: the routing turns unreadable JSON and an unknown schema into one. A
|
||||
plain `IllegalArgumentException` is a finding, because the package raises those for its own broken invariants
|
||||
(`CANVAS_BOUNDS`, `CANVAS_LIMIT`, `LENGTH_NOT_ABSOLUTE`, `LENGTH_NOT_PHYSICAL`), which no scene may reach; so is
|
||||
|
||||
@@ -122,7 +122,9 @@ public final class GenerationPorts
|
||||
GS1_INVALID,
|
||||
ASSET_UNKNOWN,
|
||||
PREVIEW_TOO_LARGE,
|
||||
RENDERER_TOO_OLD
|
||||
RENDERER_TOO_OLD,
|
||||
VALUE_NOT_ENCODABLE,
|
||||
QUIET_ZONE_INTRUDED
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
@@ -148,7 +150,7 @@ public final class GenerationPorts
|
||||
{
|
||||
private static final java.util.regex.Pattern POINTER = java.util.regex.Pattern.compile("(/(0|[1-9][0-9]{0,3}|[A-Za-z][A-Za-z0-9_-]{0,31})){0,16}");
|
||||
private static final java.util.Set<String> WARNINGS = java.util.Set.of("TEXT_TRUNCATED", "TEXT_SMALL_FOR_DPI", "TEXT_LIGHT_FACE", "FIT_NOT_PROVEN",
|
||||
"BARCODE_MODULE_BELOW_RECOMMENDED", "QUIET_ZONE_INTRUDED", "ELEMENTS_OVERLAP", "IMAGE_UPSCALED", "DEVICE_UNIT_USED", "PRINTER_FONT_NOT_SHOWN");
|
||||
"BARCODE_MODULE_BELOW_RECOMMENDED", "BARCODE_HEIGHT_BELOW_RECOMMENDED", "ELEMENTS_OVERLAP", "IMAGE_UPSCALED", "DEVICE_UNIT_USED", "PRINTER_FONT_NOT_SHOWN");
|
||||
private static final java.util.Set<String> INFORMATION = java.util.Set.of("TEXT_SHRUNK", "TEXT_WRAPPED", "BARCODE_MODULE", "IMAGE_SCALED");
|
||||
|
||||
/************************************************************************
|
||||
|
||||
@@ -122,12 +122,14 @@ final class GenerationPreviewWidget
|
||||
Map.entry("ASSET_UNKNOWN", "The scene names an asset the template does not include."),
|
||||
Map.entry("PREVIEW_TOO_LARGE", "The picture would be too large to draw."),
|
||||
Map.entry("RENDERER_TOO_OLD", "The renderer in use cannot draw this yet."),
|
||||
Map.entry("VALUE_NOT_ENCODABLE", "The value cannot be drawn as this barcode."),
|
||||
Map.entry("QUIET_ZONE_INTRUDED", "A later element is drawn over the barcode or its quiet zone."),
|
||||
Map.entry("TEXT_TRUNCATED", "The text was cut short."),
|
||||
Map.entry("TEXT_SMALL_FOR_DPI", "The text is small for this resolution."),
|
||||
Map.entry("TEXT_LIGHT_FACE", "A light face may print faintly at this size."),
|
||||
Map.entry("FIT_NOT_PROVEN", "No maximum length is declared, so the fit is not proven for every value."),
|
||||
Map.entry("BARCODE_MODULE_BELOW_RECOMMENDED", "The barcode's narrow bar is below the recommended width."),
|
||||
Map.entry("QUIET_ZONE_INTRUDED", "Something intrudes on the barcode's quiet zone."),
|
||||
Map.entry("BARCODE_HEIGHT_BELOW_RECOMMENDED", "The barcode's bars are shorter than recommended."),
|
||||
Map.entry("ELEMENTS_OVERLAP", "Elements overlap."),
|
||||
Map.entry("IMAGE_UPSCALED", "The image was enlarged and may look coarse."),
|
||||
Map.entry("DEVICE_UNIT_USED", "A device unit (dots) is used, so the layout depends on the resolution."),
|
||||
|
||||
@@ -144,7 +144,8 @@ class GenerationPortsContractTest
|
||||
"INVALID_BARCODE_FIT_REQUEST", "BARCODE_SYMBOLOGY_NOT_ALLOWED", "SYMBOLOGY_ALLOWLIST_REQUIRED", "SCANNER_BARCODE_PROFILE_UNSUPPORTED", "SCANNER_BARCODE_PAYLOAD_UNSUPPORTED",
|
||||
"SCANNER_BARCODE_CHECKSUM_REQUIRED", "SCANNER_BARCODE_CHECKSUM_MISMATCH", "UNSUPPORTED_BARCODE_OPTION"), names.subList(0, 13));
|
||||
assertEquals(List.of("SCENE_INVALID", "STOCK_MISMATCH", "TARGET_REQUIRES_SCENE_2", "ELEMENT_OUTSIDE_PRINTABLE_AREA", "TEXT_DOES_NOT_FIT", "VALUE_MISSING", "VALUE_TOO_LONG",
|
||||
"FONT_UNKNOWN", "FONT_MISSING_GLYPH", "RESERVED_FONT_NAME", "BARCODE_DOES_NOT_FIT", "GS1_INVALID", "ASSET_UNKNOWN", "PREVIEW_TOO_LARGE", "RENDERER_TOO_OLD"), names.subList(13, names.size()));
|
||||
"FONT_UNKNOWN", "FONT_MISSING_GLYPH", "RESERVED_FONT_NAME", "BARCODE_DOES_NOT_FIT", "GS1_INVALID", "ASSET_UNKNOWN", "PREVIEW_TOO_LARGE", "RENDERER_TOO_OLD",
|
||||
"VALUE_NOT_ENCODABLE", "QUIET_ZONE_INTRUDED"), names.subList(13, names.size()));
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
@@ -258,7 +259,7 @@ class GenerationPortsContractTest
|
||||
assertThrows(IllegalArgumentException.class, () -> new Diagnostic(code.name(), Severity.WARNING, null, null, null, null));
|
||||
assertThrows(IllegalArgumentException.class, () -> new Diagnostic(code.name(), Severity.INFO, null, null, null, null));
|
||||
}
|
||||
for(String code : List.of("TEXT_TRUNCATED", "TEXT_SMALL_FOR_DPI", "TEXT_LIGHT_FACE", "FIT_NOT_PROVEN", "BARCODE_MODULE_BELOW_RECOMMENDED", "QUIET_ZONE_INTRUDED", "ELEMENTS_OVERLAP",
|
||||
for(String code : List.of("TEXT_TRUNCATED", "TEXT_SMALL_FOR_DPI", "TEXT_LIGHT_FACE", "FIT_NOT_PROVEN", "BARCODE_MODULE_BELOW_RECOMMENDED", "BARCODE_HEIGHT_BELOW_RECOMMENDED", "ELEMENTS_OVERLAP",
|
||||
"IMAGE_UPSCALED", "DEVICE_UNIT_USED", "PRINTER_FONT_NOT_SHOWN"))
|
||||
{
|
||||
assertEquals(Severity.WARNING, new Diagnostic(code, Severity.WARNING, null, null, null, null).severity());
|
||||
|
||||
@@ -4,6 +4,8 @@ Catalog `qqq-barcodes-9`, fit profile `qqq-barcode-fit/1`. This is a new source
|
||||
|
||||
## Finite capability
|
||||
|
||||
This profile is what a version 1 scene and the PDF element use. A version 2 ZPL scene (`qqq-zpl-scene/2`) takes its symbols from the same encoders and the same thirteen symbologies but fits them by its own rule, in whole printer dots with a physical minimum module and bars the height of the box: see "ZPL scene 2" in [README.md](README.md).
|
||||
|
||||
Fit is supported for the default encoder profiles of `code128`, `code39`, `code93`, `itf`, `gs1-128`, `qr`, `ean13`, `ean8`, `upca`, `upce`, `datamatrix`, `pdf417` and `aztec`. Both PDF and ZPL use actual encoder rasters. The other 58 catalog IDs explicitly advertise `fit.supported=false`. This does not redefine the wider barcode requirement as completed.
|
||||
|
||||
Options must be absent or `{}`; binary payloads, structured append, color, ECI and option-dependent geometry are not fit-qualified. Existing explicit non-fit APIs retain their capabilities. Canonical `gs1-128` accepts the existing explicit GS1 syntax; scanner GS1 modes are deliberately not inferred from ordinary text or a family name.
|
||||
|
||||
@@ -129,7 +129,26 @@ A source with schema `qqq-zpl-scene/2` is a layout in physical units that render
|
||||
- **Fit.** In this order: lay the value out at `size` within `fit.maxLines` (default 1); if it does not fit, take the largest size, in steps of 0.25 pt down to `fit.shrinkTo`, at which it does (`TEXT_SHRUNK`); if none does, apply `fit.overflow`. `shrinkTo` defaults to 70% of the size and not below 5 pt. `overflow` is `refuse` (the default: `TEXT_DOES_NOT_FIT`, with the measured width, height, lines and size against the box) or `truncate`, which cuts at a character, ends with an ellipsis and warns `TEXT_TRUNCATED`. At each size a line is broken between words while that fits and at any character when it does not, so a value with no space wraps and a larger size is preferred to whole words (`TEXT_WRAPPED`). A member of `fit` that is wrong is `SCENE_INVALID` at that member (`/elements/3/fit/maxLines`). The size chosen is exactly the one a search of every quarter-point step gives, found without making that search, and all the text of one scene is held to a stated amount of work: 64 million steps, where a character visited while fitting is one step, a character to be drawn is 16 and a glyph the scene draws for the first time at a size is 4096. A scene that passes it is refused as `SCENE_INVALID` at the element that did, with `measured.textWorkSteps` against `limit.textWorkSteps`, the same whether it is validated or rendered and on every machine. That is about a second of a worker; a label of a hundred values takes a few percent of it.
|
||||
- **Proving the fit.** `maxLength` is the longest value in characters. When a scene is validated, that many of the face's widest character must fit whole, each taken to reach as far above, below and beside its advance as the furthest-reaching character of the face; otherwise the scene is refused as `TEXT_DOES_NOT_FIT`. The proof never cuts. An element with `overflow: truncate` whose stated length is not proven is still valid, since cutting is what its author chose, but every validation warns `FIT_NOT_PROVEN` with `measured.provenCharacters`, the longest length that is proven to fit whole, against `limit.characters`: longer values may be cut. A scene that passes cannot be refused at that render target because a value of up to that length does not fit, whichever of the face's characters it holds. The proof is therefore stricter than a typical value needs, and a box that is exactly `lines x lineHeight` high is not proven: leave room for an accent above a capital. When printing, a longer value is `VALUE_TOO_LONG` before any layout. A bound element without `maxLength` warns `FIT_NOT_PROVEN` with no number. Validation reads no record. The proof is for the render target of the request; validate at each resolution the document prints at.
|
||||
- **Legibility.** Glyphs are drawn antialiased and a dot is ink when its grey is darker than `threshold` (1 to 254, default 128), so a higher value prints heavier. `TEXT_SMALL_FOR_DPI` warns when the capitals are under 12 dots high at the render target (about 6 pt at 203 dpi), `TEXT_LIGHT_FACE` when a regular weight is used with capitals under 20 dots (about 10 pt at 203 dpi). A literal is judged at the size it is drawn at; a bound value, when validated, at the size its `maxLength` is proven at, which is the smallest any value is printed at (the floor when the proof fails and the element cuts), and at its stated size when it has no `maxLength`. An override is how a scene answers for one resolution: a bold face or a higher `threshold` at 203 dpi.
|
||||
- **`barcode` and `image`** are parsed and placed by renderer `0.2.0` and then refused as `RENDERER_TOO_OLD`, with the box they would have had, until their packages are in the renderer.
|
||||
- **`barcode`** draws one value in a fit-qualified symbology (`code128`, `code39`, `code93`, `itf`, `gs1-128`, `ean13`, `ean8`, `upca`, `upce`, `qr`, `datamatrix`, `pdf417`, `aztec`, default encoder options) into the picture of the label, in whole dots. It states `symbology`, or `symbologyPath` with the `allowedSymbologies` the record may name; and `value`, or `valuePath`. A `gs1-128` value is an element string with each application identifier in square brackets (`[01]00012345678905[21]TEST0001`); the encoder adds the leading FNC1 and the separators.
|
||||
- **Module.** The module, the narrowest bar, is the largest whole number of dots at which the symbol fits its box with its quiet zones and its bars are as tall as its symbology asks. It is never under `minModule` and never over `maxModule`; both go up to a whole dot. Without `minModule` the smallest module is what `barcodeProfile` gives the symbology:
|
||||
|
||||
| `barcodeProfile` | `ean13`, `ean8`, `upca`, `upce` | `gs1-128`, `itf` | `code128`, `code39`, `code93` | `qr`, `datamatrix`, `pdf417`, `aztec` |
|
||||
|---|---|---|---|---|
|
||||
| `none`: the design's own minimum | 0.25 mm | 0.25 mm | 0.25 mm | 0.375 mm |
|
||||
| `gs1-general`: GS1, general retail point of sale | 0.264 mm | 0.25 mm | 0.25 mm | 0.375 mm |
|
||||
| `gs1-logistics`: GS1, general distribution and logistic units | 0.495 mm | 0.495 mm | 0.25 mm | 0.375 mm |
|
||||
| not stated | as `gs1-general` | as `gs1-logistics` | as `none` | as `none` |
|
||||
|
||||
In dots at 203, 300 and 600 dpi: 0.25 mm is 2, 3 and 6; 0.264 mm is 3, 4 and 7; 0.375 mm is 3, 5 and 9; 0.495 mm is 4, 6 and 12. The GS1 numbers are those of the symbol specification tables of the GS1 General Specifications; 0.25 mm and 0.375 mm are this renderer's own and are to be confirmed by printing and scanning. Example: a 44-character GS1-128 (a GTIN, a date and a serial number) is 319 modules with its quiet zones. With no profile stated it is held to 4 dots at 203 dpi and needs 1276 dots, 160 mm: it fits no label 4 in wide (812 dots) and is refused. Named `gs1-general` or `none`, in a box of `90mm` (719 dots) its module is `719 / 319 = 2` dots (0.25 mm), the symbol is 638 dots, and in the middle of the box it leaves 40 and 41 dots.
|
||||
- **Quiet zones.** Each symbology has its own, in modules before the first bar and after the last: 10 and 10 for `code128`, `gs1-128`, `code39`, `code93` and `itf`; 11 and 7 for `ean13`; 7 and 7 for `ean8`; 9 and 9 for `upca`; 9 and 7 for `upce`; 4 on every side of a two-dimensional symbol. `quiet` states one number for both sides: at least the larger of the two for every symbology the element may draw, at most 64.
|
||||
- **Height.** The bars of a linear symbol take the whole height of the box across them, and that must be what the symbology asks of bars so wide: 15% of the width of the bars, first to last, and never under 5 mm, for `code128`, `gs1-128`, `code39`, `code93` and `itf` (ISO/IEC 15417, Annex G); 69.24 modules for `ean13`, `upca` and `upce` and 55.24 for `ean8` (GS1 General Specifications). A module that would make the symbol too wide for the height of its box is not chosen, and a box too short at the smallest module is `BARCODE_DOES_NOT_FIT`. The 44-character GS1-128 at 2 dots has bars 598 dots wide and needs 90 dots (11.3 mm at 203 dpi). An EAN-13 at 3 dots needs 208 dots (26 mm), more than a label 1 in high has. An author who accepts shorter bars states `minHeight`: the symbol is drawn, and the label carries the warning `BARCODE_HEIGHT_BELOW_RECOMMENDED` every time, with `measured.heightDots` against `limit.heightDots`.
|
||||
- **Place.** The fitted symbol sits in the middle of its box: for a barcode, and only for a barcode, `align` and `valign` default to `center` and `middle`, so what the box has to spare is quiet zone, the same on both sides. A scene may still state them; the odd dot goes to the right and below. The symbol paints its own white ground, quiet zones included, so ink painted before it cannot close a space. `rotation` turns it clockwise in a box given as it appears on the label; a quarter turn fits the symbol along the height of the box, and the widths and heights in its diagnostics are those of the symbol.
|
||||
- **Nothing over it.** An element later in the list whose ink would enter the symbol or its quiet zones is refused as `QUIET_ZONE_INTRUDED`, when the scene is validated as when it is rendered: `element` and `box` are the later element's, `pointer` is the barcode's, `measured` holds the edges of the ink (`leftDots`, `topDots`, `rightDots`, `bottomDots`) and `limit` the edges of what the barcode keeps. Ink is the box of the element, or the four bars of an outline; a group has none of its own. While the scene is only validated and the record gives the value or the symbology, the symbol is not known and the barcode keeps its whole box. Paint grounds and frames before a barcode, or beside it.
|
||||
- **Does not fit.** A value that does not fit at the minimum module is never drawn smaller. It is `BARCODE_DOES_NOT_FIT`, when the scene is validated as when it is rendered, with `measured.modules`, `measured.quietLeftModules` and `measured.quietRightModules`, `measured.widthDots` and `measured.heightDots` (the dots needed at the minimum), `measured.moduleDots` (the largest module that would fit, which may be 0) against `limit.widthDots` and `limit.heightDots` (the box) and `limit.minModuleDots`. A symbol of more modules than the encoder draws carries `measured.characters` against `limit.modules` of 4096. An author who accepts a smaller module states it (`"minModule": "1dot"`, in an override for the resolution it is meant for): the symbol is drawn and the label carries the warning `BARCODE_MODULE_BELOW_RECOMMENDED` every time. A drawn symbol reports `BARCODE_MODULE` with the same numbers and `measured.moduleMicrometres`.
|
||||
- **Values.** A bound value is text, or a number or true/false as JSON writes it; anything else, or nothing, is `VALUE_MISSING` at the pointer of the property that binds it (`valuePath`, `symbologyPath`, `scannerSymbologyPath`, `aimIdentifierPath`), and more than `maxLength` (or 4000) characters is `VALUE_TOO_LONG`. A label is scanned, so a symbol must never read as something other than its value, and four things the encoder would do are refused before it sees the value. (1) No value, literal or from the record, in any symbology, may hold the two characters `\<`: the encoder reads `\<FNC1>` to `\<FNC4>` as function characters, with which a record could make a GS1-128 of a plain Code 128, set the reader-programming flag or change a character. A function character enters a symbol only through its symbology: `gs1-128` adds its own FNC1. (2) `itf` takes an even number of digits; an odd number is never padded with a zero. (3) `ean13`, `ean8`, `upca` and `upce` take the whole number, check digit included (13, 8, 12 and 8 digits). The check digit is verified. A number without its check digit, or of any other length, is refused, never completed or padded. (4) In a `gs1-128` element string the check digit of every key is verified: application identifiers 00, 01, 02, 03, 253, 255, 402, 410 to 417, 8003, 8006, 8017, 8018 and 8026. A value refused by one of these, or that the symbology cannot hold, is `VALUE_NOT_ENCODABLE` (for `gs1-128`, `GS1_INVALID`) at `value` or `valuePath`, with numbers only and never the value: `measured.characters`; for an odd `itf`, `limit.charactersMultipleOf`; for a retail number, `limit.characters` and, when the check digit is wrong, `checkDigitMatches` 0 against 1; for a GS1 key, `measured.applicationIdentifier` and the same. A symbology the scene does not allow, and a scanner symbology and AIM identifier (`scannerSymbologyPath`, `aimIdentifierPath`, the version 1 scanner profile) that do not name the symbology drawn, are `SCENE_INVALID` at that property.
|
||||
- **Proving the fit.** Validation reads no record. A literal value is fitted when the scene is validated, in every symbology the record may name. For a value bound to the record, `maxLength` is the longest value in characters, and validation fits the widest symbol a value of that length can need in a linear symbology (Code 128: 11 modules for each character and 35; Code 39: 13 and 25; Code 93: 18 and 37; `itf`: 9 for each digit and 9; `gs1-128`: 11 for each character of the element string, brackets included, and 24), refusing the scene as `BARCODE_DOES_NOT_FIT` when that does not fit. For Code 128 and Code 93 the widest is a value of characters from space to tilde. The EAN and UPC symbols are one width and need no length. A two-dimensional symbol is fitted for that many bytes, which refuses a box they cannot fit and proves no more: another value of the same length can need a larger symbol, so its `FIT_NOT_PROVEN` stays. A bound value with no `maxLength`, in a symbology of more than one width, warns `FIT_NOT_PROVEN`. The widest GS1-128 of a length is one of letters, far wider than a label of digits: a 44-character element string is proven only in a box that holds 528 modules.
|
||||
- **Not yet.** `gs1` (an element string built from application identifiers) and `hri` (the human-readable line) are declared; an element that states either is refused as `RENDERER_TOO_OLD`.
|
||||
- **`image`** is parsed and placed by renderer `0.2.0` and then refused as `RENDERER_TOO_OLD`, with the box it would have had, until its package is in the renderer.
|
||||
- **`overrides`** replace properties of declared elements at the listed resolutions and are the only place a scene may depend on resolution. They are checked for all three resolutions whatever the request's target. **`samples`** are accepted and ignored by rendering.
|
||||
- **Output.** The whole label is composed on one 1-bit canvas and emitted as uncompressed `^GFA` fields: rows are cut into bands wherever eight or more blank rows separate ink, each band is trimmed to its inked columns, and a band is continued in a further field at 99,999 bytes. Only `^XA`, `^PW`, `^LL`, `^LH`, `^CI28`, `^FO`, `^GFA`, `^FS` and `^XZ` are emitted.
|
||||
- **Diagnostics.** A refusal is one diagnostic: `code`, `severity`, the `element` id, the element's `box` in dots from the top left of the printable area, and `measured` and `limit` as named integers. `SCENE_INVALID` also carries `pointer`, the JSON pointer of the offending place. A diagnostic names only an id the scene declares and points only through schema property names, array indexes and declared ids, so no record value, sample or unknown property name can leave the worker in one. A stock the printer language cannot address stays the opaque `RENDER_FAILED`, as for version 1.
|
||||
|
||||
@@ -76,8 +76,8 @@
|
||||
"y": {"$ref": "#/$defs/length", "default": "0mm", "description": "Distance inward from the anchored edge; on a centred axis a shift down, which may be negative."},
|
||||
"width": {"$ref": "#/$defs/size", "description": "Rounded down to whole dots. Omitted: the rest of the container."},
|
||||
"height": {"$ref": "#/$defs/size", "description": "Rounded down to whole dots. Omitted: the rest of the container."},
|
||||
"align": {"enum": ["left", "center", "right"], "default": "left"},
|
||||
"valign": {"enum": ["top", "middle", "bottom"], "default": "top"}
|
||||
"align": {"enum": ["left", "center", "right"], "description": "Where the content sits across its box. Default `left`; for a `barcode`, `center`."},
|
||||
"valign": {"enum": ["top", "middle", "bottom"], "description": "Where the content sits down its box. Default `top`; for a `barcode`, `middle`."}
|
||||
}
|
||||
},
|
||||
"element": {
|
||||
@@ -126,10 +126,32 @@
|
||||
},
|
||||
{
|
||||
"$ref": "#/$defs/placement",
|
||||
"description": "A barcode. Parsed and placed by renderer 0.2.0 and refused as RENDERER_TOO_OLD until the barcode package; the values of these properties are not yet read.",
|
||||
"description": "A barcode, drawn into the picture of the label in whole dots. The module (the narrowest bar) is the largest whole number of dots at which the symbol, quiet zones included, fits the box, never under `minModule` and never over `maxModule`; a value that does not fit at the minimum is BARCODE_DOES_NOT_FIT, with the modules and dots it needs and the dots the box has, and is never drawn smaller. A linear symbol is fitted along its width and its bars take the whole height of the box, which must be the height its symbology asks of bars that wide (see `minHeight`); a two-dimensional symbol (`qr`, `datamatrix`, `pdf417`, `aztec`) is fitted on both axes. The fitted symbol sits in the middle of its box unless `align` or `valign` says otherwise (for a barcode they default to `center` and `middle`, so what the box has to spare is quiet zone on both sides), and paints its own white ground, quiet zones included, so an element painted before it does no harm. Nothing may be painted over it: an element later in the list whose ink would enter the symbol or its quiet zones is refused as QUIET_ZONE_INTRUDED, with the pointer of the barcode (while a value or symbology bound to the record is not known, the barcode keeps its whole box). The renderer is the authority on values, so that a symbol never reads as something other than its value: no value may hold the two characters `\\<`, with which the encoder's escapes for function characters begin; `itf` takes an even number of digits and is never padded; `ean13`, `ean8`, `upca` and `upce` take the whole number with its check digit (13, 8, 12 and 8 digits), which is verified, and never a shorter one; the check digit of every GS1 key in a `gs1-128` element string is verified. A value that breaks one of these, or that the symbology cannot hold, is VALUE_NOT_ENCODABLE (GS1_INVALID for `gs1-128`) at the value, with numbers only and never the value. The renderer also refuses a quiet zone under the symbology's own and a maximum module under the minimum.",
|
||||
"unevaluatedProperties": false,
|
||||
"properties": {"type": {"const": "barcode"}, "symbology": {}, "symbologyPath": {}, "allowedSymbologies": {}, "value": {}, "valuePath": {}, "minModule": {}, "maxModule": {}, "quiet": {}, "gs1": {}, "hri": {},
|
||||
"scannerSymbologyPath": {}, "aimIdentifierPath": {}, "rotation": {"enum": [0, 90, 180, 270]}}
|
||||
"allOf": [
|
||||
{"oneOf": [{"required": ["symbology"], "not": {"anyOf": [{"required": ["symbologyPath"]}, {"required": ["allowedSymbologies"]}]}}, {"required": ["symbologyPath", "allowedSymbologies"], "not": {"required": ["symbology"]}}]},
|
||||
{"oneOf": [{"required": ["value"]}, {"required": ["valuePath"]}]},
|
||||
{"dependentRequired": {"scannerSymbologyPath": ["aimIdentifierPath"], "aimIdentifierPath": ["scannerSymbologyPath"]}}
|
||||
],
|
||||
"properties": {
|
||||
"type": {"const": "barcode"},
|
||||
"symbology": {"enum": ["code128", "code39", "code93", "itf", "gs1-128", "qr", "ean13", "ean8", "upca", "upce", "datamatrix", "pdf417", "aztec"], "description": "The symbology. These are the encoders qualified for fitting, with their default options."},
|
||||
"symbologyPath": {"type": "string", "pattern": "^/", "maxLength": 256, "description": "A JSON pointer into the record for the symbology, which must be one of `allowedSymbologies`. Text only; anything else is VALUE_MISSING."},
|
||||
"allowedSymbologies": {"type": "array", "minItems": 1, "uniqueItems": true, "items": {"enum": ["code128", "code39", "code93", "itf", "gs1-128", "qr", "ean13", "ean8", "upca", "upce", "datamatrix", "pdf417", "aztec"]}, "description": "The symbologies the record may name. Required with `symbologyPath` and not allowed without it."},
|
||||
"value": {"type": "string", "minLength": 1, "maxLength": 4000, "description": "The value itself, without the characters `\\<`. For `gs1-128` an element string with each application identifier in square brackets, `[01]00012345678905[21]TEST0001`: the encoder adds the leading FNC1 and the separator after a field of variable length, and the check digit of each key is verified. For `ean13`, `ean8`, `upca` and `upce` the whole number, check digit included: 13, 8, 12 and 8 digits. For `itf` an even number of digits."},
|
||||
"valuePath": {"type": "string", "pattern": "^/", "maxLength": 256, "description": "A JSON pointer into the record. Text is encoded as it is, a number or true/false as JSON writes it; anything else, and empty text, is VALUE_MISSING, and more than `maxLength` (or 4000) characters VALUE_TOO_LONG. The value is held to the same rules as `value`. No record is read when a scene is validated: see `maxLength`."},
|
||||
"minModule": {"$ref": "#/$defs/absolute", "description": "The smallest module, more than nothing, taken up to a whole dot. Default: what `barcodeProfile` gives the symbology. A stated minimum that is fewer dots than that is drawn, with the warning BARCODE_MODULE_BELOW_RECOMMENDED every time the scene is validated or rendered."},
|
||||
"maxModule": {"$ref": "#/$defs/absolute", "description": "The largest module, more than nothing, taken up to a whole dot as the minimum is, so the same length for both is one module at every resolution."},
|
||||
"quiet": {"type": "integer", "minimum": 4, "maximum": 64, "description": "Blank modules on each side, inside the box and part of the fit. Default: the symbology's own, which is 10 on each side of `code128`, `gs1-128`, `code39`, `code93` and `itf`; 11 before and 7 after `ean13`; 7 and 7 for `ean8`; 9 and 9 for `upca`; 9 and 7 for `upce`; and 4 on every side of a two-dimensional symbol. A stated number is that many on both sides, and may not be under the larger of the two for any symbology the element may draw."},
|
||||
"barcodeProfile": {"enum": ["none", "gs1-general", "gs1-logistics"], "description": "Which rule gives the smallest module when `minModule` is not stated. `none`: the design's own 0.25mm for a linear symbol and 0.375mm for a two-dimensional one (2 and 3 dots at 203 dpi, 3 and 5 at 300 dpi, 6 and 9 at 600 dpi). `gs1-general`: for `ean13`, `ean8`, `upca` and `upce` the GS1 minimum at general retail point of sale, 0.264mm (3, 4 and 7 dots); other symbologies as `none`. `gs1-logistics`: for `gs1-128`, `itf`, `ean13`, `ean8`, `upca` and `upce` the GS1 minimum in general distribution, 0.495mm (4, 6 and 12 dots); other symbologies as `none`. Default: the strictest GS1 rule written for the symbology's own use, which is `gs1-logistics` for `gs1-128` and `itf`, `gs1-general` for the EAN/UPC symbols and `none` for the rest. At 4 dots a GS1-128 of a GTIN, a date and a serial number is 160mm wide and fits no label 4in wide: such a label names `gs1-general` or `none`, or states `minModule`."},
|
||||
"minHeight": {"$ref": "#/$defs/absolute", "description": "For a linear symbol: the least height of its bars, more than nothing and taken up to a whole dot, in place of the rule of its symbology. That rule is 15% of the width of the bars, first to last, and never under 5mm for `code128`, `gs1-128`, `code39`, `code93` and `itf`; and 69.24 modules for `ean13`, `upca` and `upce`, 55.24 for `ean8`. A box shorter than the rule asks at the smallest module is BARCODE_DOES_NOT_FIT, and a module that would make the symbol too wide for the height of its box is not chosen. With `minHeight` stated, bars shorter than the symbology's rule are drawn, with the warning BARCODE_HEIGHT_BELOW_RECOMMENDED every time. Not read for a two-dimensional symbol."},
|
||||
"maxLength": {"type": "integer", "minimum": 1, "maximum": 4000, "description": "The longest value, in characters; a longer one is VALUE_TOO_LONG. For a value bound to the record in a linear symbology, validation fits the widest symbol a value of that length can need (for `code128` and `code93` a value of characters from space to tilde; for `gs1-128` the element string, brackets included), and a scene whose widest symbol does not fit is BARCODE_DOES_NOT_FIT. `ean13`, `ean8`, `upca` and `upce` are one width and need no length. A two-dimensional symbol is fitted for that many bytes, which refuses a box they cannot fit and proves no more, since another value of that length can need a larger symbol: its warning FIT_NOT_PROVEN stays. Without `maxLength`, a bound value in a symbology of more than one width warns FIT_NOT_PROVEN."},
|
||||
"gs1": {"description": "Reserved for an element string built from application identifiers. An element that states it is refused as RENDERER_TOO_OLD by this renderer."},
|
||||
"hri": {"description": "Reserved for the human-readable line. An element that states it is refused as RENDERER_TOO_OLD by this renderer."},
|
||||
"scannerSymbologyPath": {"type": "string", "pattern": "^/", "maxLength": 256, "description": "With `aimIdentifierPath`, JSON pointers to the symbology name and AIM identifier a scanner reported. They must name the symbology drawn (else SCENE_INVALID here), and the value is held to what that profile reproduces (else VALUE_NOT_ENCODABLE at the value). They are read when a label is rendered, not when a scene is validated."},
|
||||
"aimIdentifierPath": {"type": "string", "pattern": "^/", "maxLength": 256, "description": "See `scannerSymbologyPath`."},
|
||||
"rotation": {"enum": [0, 90, 180, 270], "description": "Clockwise. The box is given as it appears on the label; a symbol turned a quarter is fitted along the height of the box, and the widths and heights in its diagnostics are those of the symbol."}
|
||||
}
|
||||
},
|
||||
{
|
||||
"$ref": "#/$defs/placement",
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
package com.kingsrook.qbits.renderer.scene2;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import com.kingsrook.qbits.renderer.RenderInput;
|
||||
|
||||
@@ -137,20 +138,29 @@ final class BoxElement implements SceneElement
|
||||
|
||||
/***************************************************************************
|
||||
** Four bars inside the box, or the whole box when it is filled or when the
|
||||
** bars would meet.
|
||||
** bars would meet. An outline may so be drawn around a barcode that was
|
||||
** painted before it.
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public List<Frame> ink(Frame frame, int dpi)
|
||||
{
|
||||
int bar = bar(frame, dpi);
|
||||
if(bar < 0)
|
||||
{
|
||||
return (List.of(frame));
|
||||
}
|
||||
return (List.of(new Frame(frame.x(), frame.y(), frame.width(), bar), new Frame(frame.x(), frame.bottom() - bar, frame.width(), bar), new Frame(frame.x(), frame.y(), bar, frame.height()),
|
||||
new Frame(frame.right() - bar, frame.y(), bar, frame.height())));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Paint the ink of the box.
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public void paint(Canvas canvas, Frame frame, RenderInput input, Diagnostics diagnostics)
|
||||
{
|
||||
int bar = bar(frame, canvas.dpi());
|
||||
if(bar < 0)
|
||||
{
|
||||
canvas.fill(frame, true);
|
||||
return;
|
||||
}
|
||||
canvas.fill(new Frame(frame.x(), frame.y(), frame.width(), bar), true);
|
||||
canvas.fill(new Frame(frame.x(), frame.bottom() - bar, frame.width(), bar), true);
|
||||
canvas.fill(new Frame(frame.x(), frame.y(), bar, frame.height()), true);
|
||||
canvas.fill(new Frame(frame.right() - bar, frame.y(), bar, frame.height()), true);
|
||||
ink(frame, canvas.dpi()).forEach(part -> canvas.fill(part, true));
|
||||
}
|
||||
}
|
||||
@@ -40,8 +40,9 @@ import java.util.regex.Pattern;
|
||||
public final class Diagnostic
|
||||
{
|
||||
private static final Set<String> ERRORS = Set.of("SCENE_INVALID", "STOCK_MISMATCH", "TARGET_REQUIRES_SCENE_2", "ELEMENT_OUTSIDE_PRINTABLE_AREA", "TEXT_DOES_NOT_FIT", "VALUE_MISSING",
|
||||
"VALUE_TOO_LONG", "FONT_UNKNOWN", "FONT_MISSING_GLYPH", "RESERVED_FONT_NAME", "BARCODE_DOES_NOT_FIT", "GS1_INVALID", "ASSET_UNKNOWN", "PREVIEW_TOO_LARGE", "RENDERER_TOO_OLD");
|
||||
private static final Set<String> WARNINGS = Set.of("TEXT_TRUNCATED", "TEXT_SMALL_FOR_DPI", "TEXT_LIGHT_FACE", "FIT_NOT_PROVEN", "BARCODE_MODULE_BELOW_RECOMMENDED", "QUIET_ZONE_INTRUDED",
|
||||
"VALUE_TOO_LONG", "FONT_UNKNOWN", "FONT_MISSING_GLYPH", "RESERVED_FONT_NAME", "BARCODE_DOES_NOT_FIT", "GS1_INVALID", "ASSET_UNKNOWN", "PREVIEW_TOO_LARGE", "RENDERER_TOO_OLD",
|
||||
"VALUE_NOT_ENCODABLE", "QUIET_ZONE_INTRUDED");
|
||||
private static final Set<String> WARNINGS = Set.of("TEXT_TRUNCATED", "TEXT_SMALL_FOR_DPI", "TEXT_LIGHT_FACE", "FIT_NOT_PROVEN", "BARCODE_MODULE_BELOW_RECOMMENDED", "BARCODE_HEIGHT_BELOW_RECOMMENDED",
|
||||
"ELEMENTS_OVERLAP", "IMAGE_UPSCALED", "DEVICE_UNIT_USED", "PRINTER_FONT_NOT_SHOWN");
|
||||
private static final Set<String> INFORMATION = Set.of("TEXT_SHRUNK", "TEXT_WRAPPED", "BARCODE_MODULE", "IMAGE_SCALED");
|
||||
private static final Pattern NAME = Pattern.compile("[a-z][A-Za-z0-9]{0,31}");
|
||||
|
||||
@@ -60,6 +60,16 @@ public record Frame(int x, int y, int width, int height)
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Whether the two boxes share a dot.
|
||||
***************************************************************************/
|
||||
public boolean intersects(Frame other)
|
||||
{
|
||||
return (other.x < right() && x < other.right() && other.y < bottom() && y < other.bottom());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** The box as a diagnostic carries it: x, y, width and height.
|
||||
***************************************************************************/
|
||||
|
||||
@@ -83,6 +83,17 @@ final class GroupElement implements SceneElement
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** A group puts no ink anywhere; its children answer for theirs.
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public List<Frame> ink(Frame frame, int dpi)
|
||||
{
|
||||
return (List.of());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** A group has no ink of its own.
|
||||
***************************************************************************/
|
||||
|
||||
@@ -22,6 +22,7 @@ package com.kingsrook.qbits.renderer.scene2;
|
||||
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
@@ -155,7 +156,7 @@ public final class Scene2
|
||||
throw diagnostics.refuse("STOCK_MISMATCH", null, "/design", null, Map.of("widthMicrometres", stock.widthMicrons(), "heightMicrometres", stock.heightMicrons()),
|
||||
Map.of("widthMicrometres", scene.width().micrometres(), "heightMicrometres", scene.height().micrometres(), "toleranceMicrometres", STOCK_TOLERANCE_MICROMETRES));
|
||||
}
|
||||
walk(scene.elements(), new Layout(new Frame(0, 0, canvas.width(), canvas.height()), page.dpi()), canvas, paint, input, diagnostics);
|
||||
walk(scene.elements(), new Layout(new Frame(0, 0, canvas.width(), canvas.height()), page.dpi()), canvas, paint, input, diagnostics, new ArrayList<>());
|
||||
}
|
||||
catch(Refusal refusal)
|
||||
{
|
||||
@@ -167,12 +168,30 @@ public final class Scene2
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Elements in list order: place, hold the box to the printable area, check,
|
||||
** count the dots it would paint against what one scene may paint, paint
|
||||
** when asked, then the children inside the box. The dots are counted when a
|
||||
** scene is only validated too, so validation refuses what rendering would.
|
||||
** What an element painted earlier keeps clear of every later one: its
|
||||
** pointer in the scene and the area, in dots.
|
||||
***************************************************************************/
|
||||
private static void walk(List<SceneElement> elements, Layout layout, Canvas canvas, boolean paint, RenderInput input, Diagnostics diagnostics)
|
||||
private record Kept(String pointer, Frame area)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Elements in list order: place, hold the box to the printable area, check,
|
||||
** hold its ink off every barcode painted before it, count the dots it would
|
||||
** paint against what one scene may paint, paint when asked, then the
|
||||
** children inside the box. The dots are counted when a scene is only
|
||||
** validated too, so validation refuses what rendering would.
|
||||
**
|
||||
** A barcode painted later clears its own ground, so what lies under it does
|
||||
** no harm; ink painted over a barcode or its quiet zone would change what a
|
||||
** scanner reads, or stop it reading, and nothing would say so. The element
|
||||
** that would put it there is refused as `QUIET_ZONE_INTRUDED`, with the
|
||||
** pointer of the barcode, the edges of its own ink and the edges of the
|
||||
** area the barcode keeps.
|
||||
***************************************************************************/
|
||||
private static void walk(List<SceneElement> elements, Layout layout, Canvas canvas, boolean paint, RenderInput input, Diagnostics diagnostics, List<Kept> kept)
|
||||
{
|
||||
Frame printable = new Frame(0, 0, canvas.width(), canvas.height());
|
||||
for(SceneElement element : elements)
|
||||
@@ -192,17 +211,33 @@ public final class Scene2
|
||||
Map.of("leftDots", 0, "topDots", 0, "rightDots", printable.right(), "bottomDots", printable.bottom()));
|
||||
}
|
||||
element.check(frame, input, diagnostics);
|
||||
for(Frame ink : element.ink(frame, layout.dpi()))
|
||||
{
|
||||
for(Kept earlier : kept)
|
||||
{
|
||||
if(ink.intersects(earlier.area()))
|
||||
{
|
||||
throw diagnostics.refuse("QUIET_ZONE_INTRUDED", element.id(), earlier.pointer(), frame, Map.of("leftDots", ink.x(), "topDots", ink.y(), "rightDots", ink.right(), "bottomDots", ink.bottom()),
|
||||
Map.of("leftDots", earlier.area().x(), "topDots", earlier.area().y(), "rightDots", earlier.area().right(), "bottomDots", earlier.area().bottom()));
|
||||
}
|
||||
}
|
||||
}
|
||||
long dots = element.paintedDots(frame, layout.dpi());
|
||||
if(!canvas.admit(dots))
|
||||
{
|
||||
throw diagnostics.refuse("SCENE_INVALID", element.id(), diagnostics.pointer(element.id()), frame, Map.of("paintedPercent", canvas.paintedPercent(dots)),
|
||||
Map.of("paintedPercent", Canvas.MAXIMUM_COATS * 100));
|
||||
}
|
||||
Frame area = element.clear(frame, input, diagnostics);
|
||||
if(area != null)
|
||||
{
|
||||
kept.add(new Kept(diagnostics.pointer(element.id()), area));
|
||||
}
|
||||
if(paint)
|
||||
{
|
||||
element.paint(canvas, frame, input, diagnostics);
|
||||
}
|
||||
walk(element.children(), layout.within(frame), canvas, paint, input, diagnostics);
|
||||
walk(element.children(), layout.within(frame), canvas, paint, input, diagnostics, kept);
|
||||
}
|
||||
catch(Refusal refusal)
|
||||
{
|
||||
|
||||
@@ -68,6 +68,30 @@ public interface SceneElement
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Where in its box the element puts ink, as boxes that hold all of it: the
|
||||
** whole box unless the element knows less. An element painted after a
|
||||
** barcode may put none of these on the barcode.
|
||||
***************************************************************************/
|
||||
default List<Frame> ink(Frame frame, int dpi)
|
||||
{
|
||||
return (List.of(frame));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** The part of the label that elements painted after this one must leave
|
||||
** alone, or null: a barcode answers its symbol and quiet zones. Asked when
|
||||
** a scene is validated as when it is painted, after `check`.
|
||||
***************************************************************************/
|
||||
default Frame clear(Frame frame, RenderInput input, Diagnostics diagnostics)
|
||||
{
|
||||
return (null);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Draw the element in its box.
|
||||
***************************************************************************/
|
||||
|
||||
@@ -0,0 +1,682 @@
|
||||
/*
|
||||
* 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.scene2.barcode;
|
||||
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.kingsrook.qbits.renderer.Barcodes;
|
||||
import com.kingsrook.qbits.renderer.RenderInput;
|
||||
import com.kingsrook.qbits.renderer.ScannerSymbology;
|
||||
import com.kingsrook.qbits.renderer.Wire;
|
||||
import com.kingsrook.qbits.renderer.scene2.Canvas;
|
||||
import com.kingsrook.qbits.renderer.scene2.Diagnostics;
|
||||
import com.kingsrook.qbits.renderer.scene2.ElementSource;
|
||||
import com.kingsrook.qbits.renderer.scene2.Frame;
|
||||
import com.kingsrook.qbits.renderer.scene2.Layout;
|
||||
import com.kingsrook.qbits.renderer.scene2.Length;
|
||||
import com.kingsrook.qbits.renderer.scene2.Placement;
|
||||
import com.kingsrook.qbits.renderer.scene2.SceneElement;
|
||||
import uk.org.okapibarcode.backend.OkapiInputException;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A `barcode` element: one value in one of the fit-qualified symbologies,
|
||||
** drawn into the picture of the label. The encoder of version 1 gives the
|
||||
** bars as modules; this element lays out the quiet zones of the symbology
|
||||
** itself, `ModuleFit` chooses the module, a whole number of dots, and the
|
||||
** symbol is placed in the box by `align` and `valign`, in the middle unless
|
||||
** the scene says otherwise, and painted as a white ground and black bars, so
|
||||
** that nothing painted under it can close a space or a quiet zone. Nothing
|
||||
** may be painted over it: the scene refuses a later element that enters the
|
||||
** area `clear` answers.
|
||||
**
|
||||
** The value is a literal (`value`) or is read from the record by a JSON
|
||||
** pointer (`valuePath`). The symbology is stated, or read from the record
|
||||
** (`symbologyPath`) and held to the list the scene allows. A scanner profile
|
||||
** (`scannerSymbologyPath` with `aimIdentifierPath`) holds the value to what a
|
||||
** scanner reported, as in version 1. `Payload` holds every value to the rules
|
||||
** that keep a symbol from reading as something other than its value.
|
||||
**
|
||||
** Every refusal names the element and carries numbers only: modules, dots, a
|
||||
** count of characters. The value itself never enters a diagnostic or an
|
||||
** exception; what an encoder says about a value it rejects is dropped.
|
||||
**
|
||||
** Validation reads no record. It fits a literal value as it is, in every
|
||||
** symbology the record may name. For a value bound to the record it fits the
|
||||
** largest symbol a value of the declared `maxLength` can need, and warns
|
||||
** `FIT_NOT_PROVEN` when no length is declared or the symbology has no such
|
||||
** largest symbol, as no two-dimensional one has: that is then fitted for as
|
||||
** many bytes, which refuses a box they cannot fit and proves no more. A
|
||||
** symbology of one width needs no length.
|
||||
*******************************************************************************/
|
||||
final class BarcodeElement implements SceneElement
|
||||
{
|
||||
static final int MAXIMUM_CHARACTERS = 4000;
|
||||
static final int MAXIMUM_POINTER = 256;
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// the most modules across that the encoder path of version 1 draws (`BARCODE_BITMAP_LIMIT`) //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
static final int MAXIMUM_MODULES = 4096;
|
||||
|
||||
private static final int MICROMETRES_PER_INCH = 25_400;
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// one character that is one byte to every two-dimensional encoder here and that none of //
|
||||
// their denser modes holds (e with an acute accent, Latin-1 233): a value of them is the //
|
||||
// symbol of that many plain bytes //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
private static final String BYTE = Character.toString(0xE9);
|
||||
|
||||
private final String id;
|
||||
private final Placement placement;
|
||||
private final Placement.Align align;
|
||||
private final Placement.Valign valign;
|
||||
private final int rotation;
|
||||
private final List<Symbology> symbologies;
|
||||
private final String symbologyPath;
|
||||
private final String value;
|
||||
private final String valuePath;
|
||||
private final String scannerPath;
|
||||
private final String aimPath;
|
||||
private final Length minModule;
|
||||
private final Length maxModule;
|
||||
private final Length minHeight;
|
||||
private final Integer quiet;
|
||||
private final Integer maxLength;
|
||||
private final Symbology.Profile profile;
|
||||
private final ElementSource source;
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** One run of ink in a symbol, in modules from its first bar: `length`
|
||||
** modules from column `column` of row `row`.
|
||||
***************************************************************************/
|
||||
private record Run(int column, int row, int length)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** The bars of a symbol as the encoder gives them, without quiet zones:
|
||||
** `columns` modules across and, for a two-dimensional symbol, `rows` down.
|
||||
***************************************************************************/
|
||||
private record Bars(int columns, int rows, List<Run> runs)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** A fitted symbol on the label, in dots: the area it takes, quiet zones
|
||||
** included, and its bars.
|
||||
***************************************************************************/
|
||||
private record Symbol(Frame area, List<Frame> bars)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Read the element. A property that is not what the schema says is
|
||||
** `SCENE_INVALID` at that property.
|
||||
***************************************************************************/
|
||||
BarcodeElement(ElementSource source)
|
||||
{
|
||||
id = source.id();
|
||||
placement = source.placement();
|
||||
rotation = source.rotation();
|
||||
this.source = source;
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// a symbol sits in the middle of its box unless the scene says otherwise, so what the //
|
||||
// box has to spare is more quiet zone, the same on both sides //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
align = source.choice("align", Placement.Align.CENTER, Placement.Align::of);
|
||||
valign = source.choice("valign", Placement.Valign.MIDDLE, Placement.Valign::of);
|
||||
|
||||
JsonNode stated = source.value("symbology");
|
||||
JsonNode allowed = source.value("allowedSymbologies");
|
||||
symbologyPath = path(source, "symbologyPath");
|
||||
if((stated == null) == (symbologyPath == null))
|
||||
{
|
||||
throw source.invalid("symbology");
|
||||
}
|
||||
if(stated != null)
|
||||
{
|
||||
if(Symbology.of(stated.textValue()) == null)
|
||||
{
|
||||
throw source.invalid("symbology");
|
||||
}
|
||||
if(allowed != null)
|
||||
{
|
||||
throw source.invalid("allowedSymbologies");
|
||||
}
|
||||
symbologies = List.of(Symbology.of(stated.textValue()));
|
||||
}
|
||||
else
|
||||
{
|
||||
if(allowed == null || !allowed.isArray() || allowed.isEmpty())
|
||||
{
|
||||
throw source.invalid("allowedSymbologies");
|
||||
}
|
||||
List<Symbology> listed = new ArrayList<>();
|
||||
for(JsonNode entry : allowed)
|
||||
{
|
||||
Symbology named = Symbology.of(entry.textValue());
|
||||
if(named == null || listed.contains(named))
|
||||
{
|
||||
throw source.invalid("allowedSymbologies");
|
||||
}
|
||||
listed.add(named);
|
||||
}
|
||||
symbologies = List.copyOf(listed);
|
||||
}
|
||||
|
||||
JsonNode literal = source.value("value");
|
||||
valuePath = path(source, "valuePath");
|
||||
if((literal == null) == (valuePath == null))
|
||||
{
|
||||
throw source.invalid("value");
|
||||
}
|
||||
if(literal != null && (!literal.isTextual() || literal.textValue().isEmpty() || literal.textValue().length() > MAXIMUM_CHARACTERS))
|
||||
{
|
||||
throw source.invalid("value");
|
||||
}
|
||||
value = literal == null ? null : literal.textValue();
|
||||
|
||||
scannerPath = path(source, "scannerSymbologyPath");
|
||||
aimPath = path(source, "aimIdentifierPath");
|
||||
if((scannerPath == null) != (aimPath == null))
|
||||
{
|
||||
throw source.invalid(scannerPath == null ? "scannerSymbologyPath" : "aimIdentifierPath");
|
||||
}
|
||||
|
||||
minModule = more(source, "minModule");
|
||||
maxModule = more(source, "maxModule");
|
||||
minHeight = more(source, "minHeight");
|
||||
profile = source.choice("barcodeProfile", null, Symbology.Profile::of);
|
||||
int fewest = symbologies.stream().mapToInt(drawn -> Math.max(drawn.quietLeft(), drawn.quietRight())).max().orElseThrow();
|
||||
quiet = whole(source, "quiet", fewest, Symbology.MAXIMUM_QUIET_MODULES);
|
||||
maxLength = whole(source, "maxLength", 1, MAXIMUM_CHARACTERS);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** A JSON pointer into the record, or null when the element states none.
|
||||
***************************************************************************/
|
||||
private static String path(ElementSource source, String name)
|
||||
{
|
||||
JsonNode stated = source.value(name);
|
||||
if(stated == null)
|
||||
{
|
||||
return (null);
|
||||
}
|
||||
if(!stated.isTextual() || !stated.textValue().startsWith("/") || stated.textValue().length() > MAXIMUM_POINTER)
|
||||
{
|
||||
throw source.invalid(name);
|
||||
}
|
||||
return (stated.textValue());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** A module size or a bar height, more than nothing, or null when the
|
||||
** element states none.
|
||||
***************************************************************************/
|
||||
private static Length more(ElementSource source, String name)
|
||||
{
|
||||
Length stated = source.length(name, null, ElementSource.ABSOLUTE);
|
||||
if(stated != null && stated.thousandths() <= 0)
|
||||
{
|
||||
throw source.invalid(name);
|
||||
}
|
||||
return (stated);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** A whole number between two others, or null when the element states none.
|
||||
***************************************************************************/
|
||||
private static Integer whole(ElementSource source, String name, int least, int most)
|
||||
{
|
||||
JsonNode stated = source.value(name);
|
||||
if(stated == null)
|
||||
{
|
||||
return (null);
|
||||
}
|
||||
if(!stated.isIntegralNumber() || !stated.canConvertToInt() || stated.intValue() < least || stated.intValue() > most)
|
||||
{
|
||||
throw source.invalid(name);
|
||||
}
|
||||
return (stated.intValue());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Where the scene states a property, in the element or in an override: the
|
||||
** pointer of the refusal the source would raise for it. It is wanted for
|
||||
** the refusals that are known only once the record is read or the
|
||||
** resolution is chosen, which carry their own codes and numbers.
|
||||
***************************************************************************/
|
||||
private String pointer(String name)
|
||||
{
|
||||
return (source.invalid(name).diagnostic().pointer());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** The id the scene declares for the barcode.
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public String id()
|
||||
{
|
||||
return (id);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** The box of the element, by the standing rules.
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public Frame place(Layout layout)
|
||||
{
|
||||
return (layout.place(placement));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Everything that can refuse the barcode, with no ink: run when a scene is
|
||||
** validated and again before it is painted.
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public void check(Frame frame, RenderInput input, Diagnostics diagnostics)
|
||||
{
|
||||
compose(frame, input, diagnostics);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** What later elements must leave alone: the fitted symbol, quiet zones
|
||||
** included. Where the symbol is not known, because the scene is only being
|
||||
** validated and the record gives the value or the symbology, it is the
|
||||
** whole box, since the symbol may be anywhere in it.
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public Frame clear(Frame frame, RenderInput input, Diagnostics diagnostics)
|
||||
{
|
||||
Symbol symbol = compose(frame, input, diagnostics);
|
||||
return (symbol == null ? frame : symbol.area());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** A symbol paints its ground and then its bars, each at most its box.
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public long paintedDots(Frame frame, int dpi)
|
||||
{
|
||||
return (2L * frame.width() * frame.height());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Draw the fitted symbol: its area white, quiet zones included, then its
|
||||
** bars. The rest of the box is left as it was.
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public void paint(Canvas canvas, Frame frame, RenderInput input, Diagnostics diagnostics)
|
||||
{
|
||||
Symbol symbol = compose(frame, input, diagnostics);
|
||||
canvas.fill(symbol.area(), false);
|
||||
symbol.bars().forEach(bar -> canvas.fill(bar, true));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Resolve the symbology and the value, encode, fit and place. The answer is
|
||||
** what to draw, or null when the record gives the value or the symbology
|
||||
** and the scene is only being validated: every symbol the record could ask
|
||||
** for is then fitted as far as it is known.
|
||||
***************************************************************************/
|
||||
private Symbol compose(Frame frame, RenderInput input, Diagnostics diagnostics)
|
||||
{
|
||||
int dpi = input.targetDpi == null ? input.stock.dpi() : input.targetDpi;
|
||||
boolean validating = "VALIDATE".equals(input.operation);
|
||||
if(valuePath != null && symbologies.stream().anyMatch(drawn -> !drawn.fixed() && (maxLength == null || !drawn.provable())))
|
||||
{
|
||||
diagnostics.warn("FIT_NOT_PROVEN", id, null, frame, null, null);
|
||||
}
|
||||
if(validating && (symbologyPath != null || valuePath != null))
|
||||
{
|
||||
limits(symbologies, dpi, frame, diagnostics);
|
||||
for(Symbology drawn : symbologies)
|
||||
{
|
||||
if(valuePath == null)
|
||||
{
|
||||
String text = text(frame, input, diagnostics);
|
||||
fit(drawn, encode(drawn, payload(drawn, text, frame, null, diagnostics), text.length(), frame, diagnostics), dpi, frame, false, diagnostics);
|
||||
}
|
||||
else if(drawn.fixed() || maxLength != null)
|
||||
{
|
||||
fit(drawn, longest(drawn, frame, diagnostics), dpi, frame, false, diagnostics);
|
||||
}
|
||||
}
|
||||
return (null);
|
||||
}
|
||||
|
||||
Symbology symbology = symbology(frame, input, diagnostics);
|
||||
limits(List.of(symbology), dpi, frame, diagnostics);
|
||||
String text = text(frame, input, diagnostics);
|
||||
Bars bars = encode(symbology, payload(symbology, text, frame, validating ? null : input, diagnostics), text.length(), frame, diagnostics);
|
||||
ModuleFit.Fit fit = fit(symbology, bars, dpi, frame, true, diagnostics);
|
||||
|
||||
boolean upright = rotation % 180 == 0;
|
||||
boolean matrix = symbology.matrix();
|
||||
int before = quiet == null ? symbology.quietLeft() : quiet;
|
||||
Frame area = Layout.align(frame, upright ? fit.width() : fit.height(), upright ? fit.height() : fit.width(), align, valign);
|
||||
List<Frame> drawn = new ArrayList<>();
|
||||
for(Run run : bars.runs())
|
||||
{
|
||||
int x = (before + run.column()) * fit.module();
|
||||
int y = matrix ? (before + run.row()) * fit.module() : 0;
|
||||
int width = run.length() * fit.module();
|
||||
int height = matrix ? fit.module() : fit.height();
|
||||
drawn.add(switch(rotation)
|
||||
{
|
||||
case 90 -> new Frame(area.x() + fit.height() - y - height, area.y() + x, height, width);
|
||||
case 180 -> new Frame(area.x() + fit.width() - x - width, area.y() + fit.height() - y - height, width, height);
|
||||
case 270 -> new Frame(area.x() + y, area.y() + fit.width() - x - width, height, width);
|
||||
default -> new Frame(area.x() + x, area.y() + y, width, height);
|
||||
});
|
||||
}
|
||||
return (new Symbol(area, drawn));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** The smallest module a symbology is drawn at here, in dots: what the
|
||||
** scene states, else what the symbology's profile asks.
|
||||
***************************************************************************/
|
||||
private int minimum(Symbology symbology, int dpi)
|
||||
{
|
||||
return (minModule == null ? symbology.narrowest(profile, dpi) : minModule.module(dpi));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** What the scene's module limits come to at this resolution, for the
|
||||
** symbologies that may be drawn. A minimum the scene states below what one
|
||||
** of them asks for is the choice of its author and is drawn, with a
|
||||
** warning every time. A maximum goes up to a whole dot as the minimum
|
||||
** does, so stating the same length for both is one module at every
|
||||
** resolution; a maximum under the minimum is a mistake in the scene.
|
||||
***************************************************************************/
|
||||
private void limits(List<Symbology> drawn, int dpi, Frame frame, Diagnostics diagnostics)
|
||||
{
|
||||
int recommended = drawn.stream().mapToInt(symbology -> symbology.narrowest(profile, dpi)).max().orElseThrow();
|
||||
int minimum = minModule == null ? recommended : minModule.module(dpi);
|
||||
if(minimum < recommended)
|
||||
{
|
||||
diagnostics.warn("BARCODE_MODULE_BELOW_RECOMMENDED", id, null, frame, Map.of("minModuleDots", minimum), Map.of("minModuleDots", recommended));
|
||||
}
|
||||
if(maxModule != null && maxModule.module(dpi) < minimum)
|
||||
{
|
||||
throw diagnostics.refuse("SCENE_INVALID", id, pointer("maxModule"), frame, Map.of("maxModuleDots", maxModule.module(dpi)), Map.of("minModuleDots", minimum));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** The symbology to draw: the one the scene states, or the one the record
|
||||
** names, which must be one the scene allows.
|
||||
***************************************************************************/
|
||||
private Symbology symbology(Frame frame, RenderInput input, Diagnostics diagnostics)
|
||||
{
|
||||
if(symbologyPath == null)
|
||||
{
|
||||
return (symbologies.getFirst());
|
||||
}
|
||||
JsonNode named = input.context.at(symbologyPath);
|
||||
if(!named.isTextual())
|
||||
{
|
||||
throw diagnostics.refuse("VALUE_MISSING", id, pointer("symbologyPath"), frame, null, null);
|
||||
}
|
||||
Symbology symbology = Symbology.of(named.textValue());
|
||||
if(symbology == null || !symbologies.contains(symbology))
|
||||
{
|
||||
throw diagnostics.refuse("SCENE_INVALID", id, pointer("symbologyPath"), frame, null, null);
|
||||
}
|
||||
return (symbology);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** The value: the literal, or the text, number or true/false of the record
|
||||
** as JSON writes it, and no longer than the scene declares.
|
||||
***************************************************************************/
|
||||
private String text(Frame frame, RenderInput input, Diagnostics diagnostics)
|
||||
{
|
||||
String text = value;
|
||||
if(valuePath != null)
|
||||
{
|
||||
JsonNode bound = input.context.at(valuePath);
|
||||
if((!bound.isTextual() && !bound.isNumber() && !bound.isBoolean()) || bound.asText().isEmpty())
|
||||
{
|
||||
throw diagnostics.refuse("VALUE_MISSING", id, pointer("valuePath"), frame, null, null);
|
||||
}
|
||||
text = bound.asText();
|
||||
}
|
||||
int most = maxLength == null ? MAXIMUM_CHARACTERS : maxLength;
|
||||
if(text.length() > most)
|
||||
{
|
||||
throw diagnostics.refuse("VALUE_TOO_LONG", id, null, frame, Map.of("characters", text.length()), Map.of("characters", most));
|
||||
}
|
||||
return (text);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** What the encoder is given for a value, held to the rules of its
|
||||
** symbology and, when the scene names a scanner profile and a record is
|
||||
** being read, to that profile. A value that would not read back as itself
|
||||
** is refused at the property that gives it, with only its numbers. An
|
||||
** element string built from `gs1` entries would enter here.
|
||||
***************************************************************************/
|
||||
private String payload(Symbology symbology, String text, Frame frame, RenderInput input, Diagnostics diagnostics)
|
||||
{
|
||||
String code = symbology == Symbology.GS1_128 ? "GS1_INVALID" : "VALUE_NOT_ENCODABLE";
|
||||
String at = pointer(valuePath == null ? "value" : "valuePath");
|
||||
Payload.Checked checked = Payload.of(symbology, text);
|
||||
if(checked.encoded() == null)
|
||||
{
|
||||
throw diagnostics.refuse(code, id, at, frame, checked.measured(), checked.limit());
|
||||
}
|
||||
if(scannerPath == null || input == null)
|
||||
{
|
||||
return (checked.encoded());
|
||||
}
|
||||
JsonNode label = input.context.at(scannerPath);
|
||||
JsonNode identifier = input.context.at(aimPath);
|
||||
if(!label.isTextual() || !identifier.isTextual())
|
||||
{
|
||||
throw diagnostics.refuse("VALUE_MISSING", id, pointer(label.isTextual() ? "aimIdentifierPath" : "scannerSymbologyPath"), frame, null, null);
|
||||
}
|
||||
try
|
||||
{
|
||||
return (ScannerSymbology.payload(symbology.id(), text, label.textValue(), identifier.textValue()));
|
||||
}
|
||||
catch(IllegalArgumentException rejected)
|
||||
{
|
||||
////////////////////////////////////////////////////////////////////////////////////////
|
||||
// the closed codes of the profile: what the scanner reported does not name this //
|
||||
// symbology, or the value is not one the profile can hold //
|
||||
////////////////////////////////////////////////////////////////////////////////////////
|
||||
if("SCANNER_BARCODE_PROFILE_UNSUPPORTED".equals(rejected.getMessage()))
|
||||
{
|
||||
throw diagnostics.refuse("SCENE_INVALID", id, pointer("scannerSymbologyPath"), frame, null, null);
|
||||
}
|
||||
throw diagnostics.refuse(code, id, at, frame, Map.of("characters", text.length()), null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** The bars of a value, from the encoder of version 1 at one dot to the
|
||||
** module, with the quiet zones it was asked for taken off again. A symbol
|
||||
** of more modules than that encoder draws does not fit any box. Anything
|
||||
** else the encoder refuses is a value the symbology cannot hold; what it
|
||||
** says may quote the value, so nothing of it is kept: the refusal is the
|
||||
** count of characters.
|
||||
***************************************************************************/
|
||||
private Bars encode(Symbology symbology, String payload, int characters, Frame frame, Diagnostics diagnostics)
|
||||
{
|
||||
int margin = symbology.matrix() ? 4 : 10;
|
||||
BufferedImage modules;
|
||||
try
|
||||
{
|
||||
modules = Barcodes.renderForFormat("ZPL", symbology.id(), payload, 1, margin, Wire.JSON.createObjectNode());
|
||||
}
|
||||
catch(IllegalArgumentException | OkapiInputException rejected)
|
||||
{
|
||||
if("BARCODE_BITMAP_LIMIT".equals(rejected.getMessage()))
|
||||
{
|
||||
throw diagnostics.refuse("BARCODE_DOES_NOT_FIT", id, null, frame, Map.of("characters", characters), Map.of("modules", MAXIMUM_MODULES));
|
||||
}
|
||||
throw diagnostics.refuse(symbology == Symbology.GS1_128 ? "GS1_INVALID" : "VALUE_NOT_ENCODABLE", id, pointer(valuePath == null ? "value" : "valuePath"), frame, Map.of("characters", characters), null);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// a linear symbol is the runs of one row through its middle, since every bar is the //
|
||||
// whole height; a two-dimensional symbol is the runs of every row //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
boolean matrix = symbology.matrix();
|
||||
int columns = modules.getWidth() - 2 * margin;
|
||||
int rows = matrix ? modules.getHeight() - 2 * margin : 0;
|
||||
List<Run> runs = new ArrayList<>();
|
||||
int first = matrix ? margin : modules.getHeight() / 2;
|
||||
int last = matrix ? margin + rows - 1 : first;
|
||||
for(int row = first; row <= last; row++)
|
||||
{
|
||||
int start = -1;
|
||||
for(int column = 0; column <= columns; column++)
|
||||
{
|
||||
boolean ink = column < columns && (modules.getRGB(margin + column, row) & 0xFFFFFF) == 0;
|
||||
if(ink && start < 0)
|
||||
{
|
||||
start = column;
|
||||
}
|
||||
if(!ink && start >= 0)
|
||||
{
|
||||
runs.add(new Run(start, row - first, column - start));
|
||||
start = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return (new Bars(columns, rows, runs));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** The symbol to fit for a value that is not known yet. A linear symbology
|
||||
** has the largest a value of the declared length can need, by arithmetic. A
|
||||
** two-dimensional one is asked for that many bytes, which is not its
|
||||
** largest and so proves nothing; when it cannot hold them, the length the
|
||||
** scene declares does not fit.
|
||||
***************************************************************************/
|
||||
private Bars longest(Symbology symbology, Frame frame, Diagnostics diagnostics)
|
||||
{
|
||||
if(!symbology.matrix())
|
||||
{
|
||||
return (new Bars(symbology.widest(maxLength == null ? 0 : maxLength), 0, List.of()));
|
||||
}
|
||||
try
|
||||
{
|
||||
BufferedImage modules = Barcodes.renderForFormat("ZPL", symbology.id(), BYTE.repeat(maxLength), 1, 4, Wire.JSON.createObjectNode());
|
||||
return (new Bars(modules.getWidth() - 8, modules.getHeight() - 8, List.of()));
|
||||
}
|
||||
catch(IllegalArgumentException | OkapiInputException tooLong)
|
||||
{
|
||||
throw diagnostics.refuse("BARCODE_DOES_NOT_FIT", id, pointer("maxLength"), frame, Map.of("characters", maxLength), null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Fit a symbol of these bars to the box, with its quiet zones, and refuse
|
||||
** one that does not fit at the smallest module with the numbers that say
|
||||
** why. A symbol that is drawn reports the module chosen. Bars shorter than
|
||||
** the symbology asks are drawn only when the scene states its own
|
||||
** `minHeight`, and then with a warning every time.
|
||||
***************************************************************************/
|
||||
private ModuleFit.Fit fit(Symbology symbology, Bars bars, int dpi, Frame frame, boolean drawn, Diagnostics diagnostics)
|
||||
{
|
||||
int before = quiet == null ? symbology.quietLeft() : quiet;
|
||||
int after = quiet == null ? symbology.quietRight() : quiet;
|
||||
ModuleFit.Height asked = symbology.height(bars.columns(), dpi);
|
||||
ModuleFit.Height height = symbology.matrix() ? null : minHeight == null ? asked : new ModuleFit.Height(bars.columns(), minHeight.module(dpi), 0, 1);
|
||||
boolean upright = rotation % 180 == 0;
|
||||
ModuleFit.Request request = new ModuleFit.Request(before + bars.columns() + after, symbology.matrix() ? bars.rows() + 2 * before : 0, upright ? frame.width() : frame.height(),
|
||||
upright ? frame.height() : frame.width(), minimum(symbology, dpi), maxModule == null ? Integer.MAX_VALUE : maxModule.module(dpi), height);
|
||||
ModuleFit.Fit fit = ModuleFit.fit(request);
|
||||
Map<String, Integer> measured = Map.of("modules", request.columns(), "quietLeftModules", before, "quietRightModules", after, "moduleDots", fit.module(), "widthDots", fit.width(),
|
||||
"heightDots", fit.height(), "moduleMicrometres", Math.toIntExact(((long) fit.module() * MICROMETRES_PER_INCH + dpi / 2) / dpi));
|
||||
Map<String, Integer> limit = Map.of("widthDots", request.along(), "heightDots", request.across(), "minModuleDots", request.minimum());
|
||||
if(!fit.fits())
|
||||
{
|
||||
throw diagnostics.refuse("BARCODE_DOES_NOT_FIT", id, null, frame, measured, limit);
|
||||
}
|
||||
if(height != null && fit.height() < asked.at(fit.module()))
|
||||
{
|
||||
diagnostics.warn("BARCODE_HEIGHT_BELOW_RECOMMENDED", id, null, frame, Map.of("heightDots", fit.height()), Map.of("heightDots", asked.at(fit.module())));
|
||||
}
|
||||
if(drawn)
|
||||
{
|
||||
diagnostics.inform("BARCODE_MODULE", id, null, frame, measured, limit);
|
||||
}
|
||||
return (fit);
|
||||
}
|
||||
}
|
||||
@@ -30,32 +30,38 @@ import com.kingsrook.qbits.renderer.scene2.UnavailableElement;
|
||||
|
||||
/*******************************************************************************
|
||||
** Barcodes: a symbol fitted to its box in whole-dot modules, never below its
|
||||
** stated minimum.
|
||||
** stated minimum, and placed in the box by `align` and `valign`, in the
|
||||
** middle unless the scene says otherwise.
|
||||
**
|
||||
** This is the placeholder of the scene 2 core: a `barcode` element is parsed and
|
||||
** placed, then refused as `RENDERER_TOO_OLD`. The package that draws it
|
||||
** replaces this class and nothing else.
|
||||
** `gs1` (an element string built from application identifiers) and `hri` (the
|
||||
** human-readable line) are declared and not yet read: an element that states
|
||||
** either is parsed and placed, then refused as `RENDERER_TOO_OLD`, so it is
|
||||
** never drawn without what it asked for.
|
||||
*******************************************************************************/
|
||||
public final class BarcodeFactory implements ElementFactory
|
||||
{
|
||||
/***************************************************************************
|
||||
** The property names the design gives a `barcode` element. Their values are
|
||||
** not read yet.
|
||||
** The property names the design gives a `barcode` element.
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public Set<String> properties()
|
||||
{
|
||||
return (Set.of("symbology", "symbologyPath", "allowedSymbologies", "value", "valuePath", "minModule", "maxModule", "quiet", "gs1", "hri", "scannerSymbologyPath", "aimIdentifierPath", "rotation"));
|
||||
return (Set.of("symbology", "symbologyPath", "allowedSymbologies", "value", "valuePath", "minModule", "maxModule", "quiet", "gs1", "hri", "scannerSymbologyPath", "aimIdentifierPath", "rotation",
|
||||
"barcodeProfile", "minHeight", "maxLength"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** The element, placed and then refused by name.
|
||||
** The barcode, or the placeholder for one this renderer cannot draw yet.
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public SceneElement create(ElementSource source)
|
||||
{
|
||||
return (new UnavailableElement(source));
|
||||
if(source.value("gs1") != null || source.value("hri") != null)
|
||||
{
|
||||
return (new UnavailableElement(source));
|
||||
}
|
||||
return (new BarcodeElement(source));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
* 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.scene2.barcode;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The module of a barcode in its box. A module is the width of the narrowest
|
||||
** bar, and on a label composed as one picture it is a whole number of printer
|
||||
** dots. The rule: the module is the largest whole number of dots at which the
|
||||
** symbol, its quiet zones included, fits the box, never more than the stated
|
||||
** maximum, and the symbol does not fit when that is less than the minimum.
|
||||
**
|
||||
** A two-dimensional symbol is fitted on both axes. A linear symbol is fitted
|
||||
** along its width, and its bars take the whole height of the box; that height
|
||||
** must be what the symbology asks of bars as wide as these, so a module that
|
||||
** would make the symbol too wide for its height is not taken either. Integer
|
||||
** arithmetic only: no fraction of a dot takes part.
|
||||
**
|
||||
** The numbers of each symbology (quiet zones, narrowest bar, bar height) are
|
||||
** in `Symbology`.
|
||||
*******************************************************************************/
|
||||
final class ModuleFit
|
||||
{
|
||||
/***************************************************************************
|
||||
** Utility class.
|
||||
***************************************************************************/
|
||||
private ModuleFit()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** How tall the bars of a linear symbol must be: at least `least` dots, and
|
||||
** at least `numerator / denominator` of the width of its `bars` modules,
|
||||
** first bar to last. A numerator of nothing leaves only the length.
|
||||
***************************************************************************/
|
||||
record Height(int bars, int least, int numerator, int denominator)
|
||||
{
|
||||
/************************************************************************
|
||||
** The dots the bars must be tall when a module is this many dots, the
|
||||
** share of the width taken up to a whole dot.
|
||||
************************************************************************/
|
||||
int at(int module)
|
||||
{
|
||||
return (Math.max(least, Math.toIntExact(((long) bars * module * numerator + denominator - 1) / denominator)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** What is fitted: a symbol of `columns` modules across, quiet zones
|
||||
** included, in a box of `along` dots on the width of the symbol and
|
||||
** `across` dots on its height, with the module held between two whole
|
||||
** numbers of dots. A two-dimensional symbol is `rows` modules down, quiet
|
||||
** zones included, and has no `height`; a linear symbol has no rows and the
|
||||
** rule for the height of its bars.
|
||||
***************************************************************************/
|
||||
record Request(int columns, int rows, int along, int across, int minimum, int maximum, Height height)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** The answer. `module` is the largest module that fits, in dots, and may be
|
||||
** less than the minimum, or nothing. `width` and `height` are the dots the
|
||||
** symbol takes at that module when it fits, and the dots it would need at
|
||||
** the minimum when it does not: for a linear symbol that fits, the height
|
||||
** is the whole of the box across its bars.
|
||||
***************************************************************************/
|
||||
record Fit(int module, int width, int height, boolean fits)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Fit one symbol. The largest module is the box divided by the modules,
|
||||
** rounded down, so the symbol and its quiet zones never leave the box; for
|
||||
** a linear symbol it is also the largest at which the box is as tall as
|
||||
** bars of that width must be.
|
||||
***************************************************************************/
|
||||
static Fit fit(Request request)
|
||||
{
|
||||
int module = Math.min(request.maximum(), request.along() / request.columns());
|
||||
Height height = request.height();
|
||||
if(height == null)
|
||||
{
|
||||
module = Math.min(module, request.across() / request.rows());
|
||||
}
|
||||
else if(height.least() > request.across())
|
||||
{
|
||||
module = 0;
|
||||
}
|
||||
else if(height.numerator() > 0)
|
||||
{
|
||||
module = (int) Math.min(module, (long) request.across() * height.denominator() / ((long) height.bars() * height.numerator()));
|
||||
}
|
||||
boolean fits = module >= request.minimum();
|
||||
int drawn = fits ? module : request.minimum();
|
||||
int tall = height == null ? request.rows() * drawn : fits ? request.across() : height.at(drawn);
|
||||
return (new Fit(module, request.columns() * drawn, tall, fits));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
/*
|
||||
* 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.scene2.barcode;
|
||||
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** What the encoder is given for a value, or why the value is not drawn. A
|
||||
** barcode on a label is scanned, so the one failure that matters most is a
|
||||
** symbol that reads as something other than the value it was given. The
|
||||
** encoder would make one in four ways, and each is refused here before the
|
||||
** encoder sees the value:
|
||||
**
|
||||
** - It reads `\<FNC1>` to `\<FNC4>` in a value as function characters. A
|
||||
** record could so turn a plain Code 128 into a GS1-128 of its own choosing,
|
||||
** set the reader-programming flag, or change a character; and where a
|
||||
** symbology has no such character the escape is dropped, so the symbol
|
||||
** differs from the value. No value may hold the two characters that begin
|
||||
** an escape, in any symbology. A function character enters a symbol only
|
||||
** through its symbology: `gs1-128` adds its own FNC1.
|
||||
** - It pads Interleaved 2 of 5 of an odd number of digits with a zero in
|
||||
** front. An odd number of digits is not drawn.
|
||||
** - It takes an EAN or UPC value without its check digit, works the digit out
|
||||
** and pads a short value with zeros in front, so a mistyped number prints
|
||||
** as a valid symbol for another product. Here the value is the whole
|
||||
** number, check digit included (13 digits for `ean13`, 8 for `ean8`, 12
|
||||
** for `upca`, 8 for `upce`); the check digit is verified, and the encoder
|
||||
** is given the digits before it.
|
||||
** - It checks the lengths of a GS1 element string and not its check digits.
|
||||
** The key of each application identifier that ends in a check digit is
|
||||
** verified.
|
||||
**
|
||||
** A refusal carries numbers only: how many characters, which application
|
||||
** identifier of the fixed list below, and that a check digit does not match.
|
||||
** Nothing of the value is in it.
|
||||
*******************************************************************************/
|
||||
final class Payload
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// the two characters every escape of the encoder begins with (`Symbol.FNC1_STRING` and its //
|
||||
// three siblings of OkapiBarcode 0.5.7), refused whatever follows them //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
static final String ESCAPE = "\\<";
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// the application identifiers whose data begins with a key that ends in a GS1 check digit, //
|
||||
// each with where its key starts and how many digits it is. The list is the `csum` entries //
|
||||
// of the GS1 Syntax Dictionary as the bundled Zint 2.16.0 carries them //
|
||||
// (`backend/gs1_lint.h`, generated from gs1-syntax-dictionary.txt): SSCC (00), GTIN (01, 02, //
|
||||
// 03), GDTI (253), GCN (255), GSIN (402), the GLNs (410 to 417), GRAI (8003, after its //
|
||||
// leading zero), ITIP (8006, 8026) and GSRN (8017, 8018). The identifiers 8013 and 8014 end //
|
||||
// in a pair of check characters of another kind, which is not verified here. //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
private static final Map<String, int[]> KEYS = Map.ofEntries(Map.entry("00", new int[] {0, 18}), Map.entry("01", new int[] {0, 14}), Map.entry("02", new int[] {0, 14}), Map.entry("03", new int[] {0, 14}),
|
||||
Map.entry("253", new int[] {0, 13}), Map.entry("255", new int[] {0, 13}), Map.entry("402", new int[] {0, 17}), Map.entry("410", new int[] {0, 13}), Map.entry("411", new int[] {0, 13}),
|
||||
Map.entry("412", new int[] {0, 13}), Map.entry("413", new int[] {0, 13}), Map.entry("414", new int[] {0, 13}), Map.entry("415", new int[] {0, 13}), Map.entry("416", new int[] {0, 13}),
|
||||
Map.entry("417", new int[] {0, 13}), Map.entry("8003", new int[] {1, 13}), Map.entry("8006", new int[] {0, 14}), Map.entry("8026", new int[] {0, 14}), Map.entry("8017", new int[] {0, 18}),
|
||||
Map.entry("8018", new int[] {0, 18}));
|
||||
|
||||
private static final Map<Symbology, Integer> RETAIL = Map.of(Symbology.EAN13, 13, Symbology.EAN8, 8, Symbology.UPCA, 12, Symbology.UPCE, 8);
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Utility class.
|
||||
***************************************************************************/
|
||||
private Payload()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** The answer for one value: what the encoder is given, or, when that is
|
||||
** null, the numbers of the refusal.
|
||||
***************************************************************************/
|
||||
record Checked(String encoded, Map<String, Integer> measured, Map<String, Integer> limit)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Whether a value holds the beginning of an escape the encoder reads.
|
||||
***************************************************************************/
|
||||
static boolean escapes(String value)
|
||||
{
|
||||
return (value.contains(ESCAPE));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Hold a value to the rules of its symbology.
|
||||
***************************************************************************/
|
||||
static Checked of(Symbology symbology, String value)
|
||||
{
|
||||
Map<String, Integer> characters = Map.of("characters", value.length());
|
||||
if(escapes(value))
|
||||
{
|
||||
return (new Checked(null, characters, null));
|
||||
}
|
||||
if(symbology == Symbology.ITF && value.length() % 2 != 0)
|
||||
{
|
||||
return (new Checked(null, characters, Map.of("charactersMultipleOf", 2)));
|
||||
}
|
||||
Integer whole = RETAIL.get(symbology);
|
||||
if(whole != null)
|
||||
{
|
||||
if(value.length() != whole)
|
||||
{
|
||||
return (new Checked(null, characters, Map.of("characters", whole)));
|
||||
}
|
||||
String body = value.substring(0, whole - 1);
|
||||
if(!checked((symbology == Symbology.UPCE ? expanded(body) : body) + value.charAt(whole - 1)))
|
||||
{
|
||||
return (new Checked(null, Map.of("characters", value.length(), "checkDigitMatches", 0), Map.of("characters", whole, "checkDigitMatches", 1)));
|
||||
}
|
||||
return (new Checked(body, null, null));
|
||||
}
|
||||
if(symbology == Symbology.GS1_128)
|
||||
{
|
||||
for(String element : value.split("\\[", -1))
|
||||
{
|
||||
int close = element.indexOf(']');
|
||||
int[] key = close < 0 ? null : KEYS.get(element.substring(0, close));
|
||||
if(key != null && (element.length() < close + 1 + key[0] + key[1] || !checked(element.substring(close + 1 + key[0], close + 1 + key[0] + key[1]))))
|
||||
{
|
||||
return (new Checked(null, Map.of("characters", value.length(), "applicationIdentifier", Integer.parseInt(element.substring(0, close)), "checkDigitMatches", 0), Map.of("checkDigitMatches", 1)));
|
||||
}
|
||||
}
|
||||
}
|
||||
return (new Checked(value, null, null));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Whether text is digits that end in their own GS1 check digit: from the
|
||||
** right the digits weigh 1, 3, 1, 3 and so on, the check digit first, and
|
||||
** the weighted sum is a multiple of ten. It is the one rule of GTIN, SSCC,
|
||||
** GLN and the other keys, whatever their length.
|
||||
***************************************************************************/
|
||||
static boolean checked(String digits)
|
||||
{
|
||||
int sum = 0;
|
||||
for(int index = 0; index < digits.length(); index++)
|
||||
{
|
||||
char digit = digits.charAt(index);
|
||||
if(digit < '0' || digit > '9')
|
||||
{
|
||||
return (false);
|
||||
}
|
||||
sum += (digit - '0') * ((digits.length() - index) % 2 == 0 ? 3 : 1);
|
||||
}
|
||||
return (sum % 10 == 0);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** The eleven digits of the UPC-A number that the seven digits of a UPC-E
|
||||
** number stand for (the number system, then six): the check digit of a
|
||||
** UPC-E symbol is that of the longer number. The last of the six says
|
||||
** where the zeros were taken out.
|
||||
***************************************************************************/
|
||||
static String expanded(String body)
|
||||
{
|
||||
String six = body.substring(1);
|
||||
char last = six.charAt(5);
|
||||
String ten = switch(last)
|
||||
{
|
||||
case '0', '1', '2' -> six.substring(0, 2) + last + "0000" + six.substring(2, 5);
|
||||
case '3' -> six.substring(0, 3) + "00000" + six.substring(3, 5);
|
||||
case '4' -> six.substring(0, 4) + "00000" + six.charAt(4);
|
||||
default -> six.substring(0, 5) + "0000" + last;
|
||||
};
|
||||
return (body.charAt(0) + ten);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,320 @@
|
||||
/*
|
||||
* 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.scene2.barcode;
|
||||
|
||||
|
||||
import com.kingsrook.qbits.renderer.scene2.Length;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The rules of each fit-qualified symbology, in one reviewed place: its quiet
|
||||
** zones, the narrowest bar it is drawn at by default, how tall its bars must
|
||||
** be, and the widest symbol a value of a stated length can need. Every number
|
||||
** has its source beside it. A number that was chosen here, and is not taken
|
||||
** from a specification, says so.
|
||||
**
|
||||
** Sources, by the short names the table uses:
|
||||
**
|
||||
** - GS1: the GS1 General Specifications. Section 5.2.3.4 gives the quiet zones
|
||||
** of the EAN/UPC symbols in modules (EAN-13: 11 left, 7 right; EAN-8: 7 and
|
||||
** 7; UPC-A: 9 and 9; UPC-E: 9 and 7). Section 5.2.3.2 gives their height at
|
||||
** the nominal 0.330 mm module: 22.85 mm for EAN-13, UPC-A and UPC-E, which
|
||||
** is 69.24 modules, and 18.23 mm for EAN-8, 55.24 modules; the height scales
|
||||
** with the module. The symbol specification tables of section 5.12 give the
|
||||
** narrowest bar: 0.264 mm for EAN/UPC at general retail point of sale
|
||||
** (table 1) and 0.495 mm for ITF-14 and GS1-128 scanned in general
|
||||
** distribution, logistic units among them (table 5). EAN/UPC scanned in
|
||||
** general distribution is given the same 0.495 mm here, as the tables for
|
||||
** general distribution are remembered to; those were not read again.
|
||||
** - ISO/IEC 15417 (Code 128, and so GS1-128): a quiet zone of 10 modules on
|
||||
** each side, and in Annex G a bar height of at least 5 mm or 15% of the
|
||||
** length of the symbol, whichever is greater.
|
||||
** - ISO/IEC 16388 (Code 39), ISO/IEC 16390 (Interleaved 2 of 5) and the AIM
|
||||
** specification of Code 93: a quiet zone of 10 modules on each side. Their
|
||||
** bar heights were not read for this table; they are held to the rule of
|
||||
** ISO/IEC 15417, the one the same scanners are specified against.
|
||||
** - ISO/IEC 18004 (QR Code): a quiet zone of 4 modules on every side. Data
|
||||
** Matrix (ISO/IEC 16022) asks for 1, PDF417 (ISO/IEC 15438) for 2 and Aztec
|
||||
** Code (ISO/IEC 24778) for none; all three keep the 4 that version 1 gives
|
||||
** every two-dimensional symbol, which is more than each asks.
|
||||
** - design: section 4.3 of the layout design, 0.25 mm for a linear symbol and
|
||||
** 0.375 mm for a two-dimensional one. They are the design's own proposals,
|
||||
** to be confirmed by printing and scanning before a final release.
|
||||
**
|
||||
** Read again when this table was written: the 0.495 mm and 0.264 mm of GS1,
|
||||
** the quiet zones of EAN-13 and EAN-8, the EAN/UPC heights, and the 5 mm and
|
||||
** 15% of ISO/IEC 15417. The quiet zones of UPC-A and UPC-E and the 10 modules
|
||||
** of Code 39, Code 93 and Interleaved 2 of 5 are as remembered.
|
||||
**
|
||||
** The width of a symbol in modules was measured from the encoder and is held
|
||||
** to it by test: Code 128, and so GS1-128, is 11 modules for each symbol
|
||||
** character and 35 for its start, check and stop; Code 39 is 13 for each
|
||||
** character and 25; Code 93 is 9 for each symbol character, two of which a
|
||||
** lower-case letter needs, and 37; Interleaved 2 of 5 is 9 for each digit and
|
||||
** 9; the EAN/UPC symbols are 95, 67, 95 and 51 modules whatever their digits.
|
||||
*******************************************************************************/
|
||||
enum Symbology
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// the columns, in order: the id; whether the symbology is two-dimensional; the quiet zone in modules, //
|
||||
// left (every side of a two-dimensional symbol) and right; the narrowest bar in micrometres under the //
|
||||
// profiles `none` (design), `gs1-general` (GS1 table 1) and `gs1-logistics` (GS1 general distribution); //
|
||||
// the profile taken when the scene names none; the height of the bars as a share of their width //
|
||||
// (numerator, denominator) and as a least length in micrometres; the modules of the widest symbol for //
|
||||
// each character and besides //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
CODE128("code128", false, 10, 10, 250, 250, 250, Profile.NONE, 15, 100, 5000, 11, 35),
|
||||
CODE39("code39", false, 10, 10, 250, 250, 250, Profile.NONE, 15, 100, 5000, 13, 25),
|
||||
CODE93("code93", false, 10, 10, 250, 250, 250, Profile.NONE, 15, 100, 5000, 18, 37),
|
||||
ITF("itf", false, 10, 10, 250, 250, 495, Profile.GS1_LOGISTICS, 15, 100, 5000, 9, 9),
|
||||
GS1_128("gs1-128", false, 10, 10, 250, 250, 495, Profile.GS1_LOGISTICS, 15, 100, 5000, 11, 24),
|
||||
EAN13("ean13", false, 11, 7, 250, 264, 495, Profile.GS1_GENERAL, 6924, 9500, 0, 0, 95),
|
||||
EAN8("ean8", false, 7, 7, 250, 264, 495, Profile.GS1_GENERAL, 5524, 6700, 0, 0, 67),
|
||||
UPCA("upca", false, 9, 9, 250, 264, 495, Profile.GS1_GENERAL, 6924, 9500, 0, 0, 95),
|
||||
UPCE("upce", false, 9, 7, 250, 264, 495, Profile.GS1_GENERAL, 6924, 5100, 0, 0, 51),
|
||||
QR("qr", true, 4, 4, 375, 375, 375, Profile.NONE, 0, 1, 0, 0, 0),
|
||||
DATAMATRIX("datamatrix", true, 4, 4, 375, 375, 375, Profile.NONE, 0, 1, 0, 0, 0),
|
||||
PDF417("pdf417", true, 4, 4, 375, 375, 375, Profile.NONE, 0, 1, 0, 0, 0),
|
||||
AZTEC("aztec", true, 4, 4, 375, 375, 375, Profile.NONE, 0, 1, 0, 0, 0);
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// the most blank modules a scene may ask for on a side, the limit version 1 gives its encoders //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
static final int MAXIMUM_QUIET_MODULES = 64;
|
||||
|
||||
private final String id;
|
||||
private final boolean matrix;
|
||||
private final int quietLeft;
|
||||
private final int quietRight;
|
||||
private final int none;
|
||||
private final int general;
|
||||
private final int logistics;
|
||||
private final Profile standard;
|
||||
private final int heightNumerator;
|
||||
private final int heightDenominator;
|
||||
private final int heightMicrometres;
|
||||
private final int widestEach;
|
||||
private final int widestFixed;
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Which rule gives the narrowest bar a symbol is drawn at when the scene
|
||||
** states no `minModule`: none beyond the design's own, the GS1 rule for
|
||||
** general retail, or the GS1 rule for general distribution and logistic
|
||||
** units. A scene names one with `barcodeProfile`; without it each
|
||||
** symbology takes its `standard` profile, the strictest GS1 rule written
|
||||
** for that symbology's own use: general distribution for GS1-128 and
|
||||
** Interleaved 2 of 5 (the carrier of ITF-14), general retail for EAN/UPC.
|
||||
***************************************************************************/
|
||||
enum Profile
|
||||
{
|
||||
NONE("none"), GS1_GENERAL("gs1-general"), GS1_LOGISTICS("gs1-logistics");
|
||||
|
||||
private final String word;
|
||||
|
||||
|
||||
|
||||
/************************************************************************
|
||||
** The word a scene writes.
|
||||
************************************************************************/
|
||||
Profile(String word)
|
||||
{
|
||||
this.word = word;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/************************************************************************
|
||||
** The profile a scene word names, or null.
|
||||
************************************************************************/
|
||||
static Profile of(String word)
|
||||
{
|
||||
for(Profile profile : values())
|
||||
{
|
||||
if(profile.word.equals(word))
|
||||
{
|
||||
return (profile);
|
||||
}
|
||||
}
|
||||
return (null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** One row of the table.
|
||||
***************************************************************************/
|
||||
Symbology(String id, boolean matrix, int quietLeft, int quietRight, int none, int general, int logistics, Profile standard, int heightNumerator, int heightDenominator, int heightMicrometres,
|
||||
int widestEach, int widestFixed)
|
||||
{
|
||||
this.id = id;
|
||||
this.matrix = matrix;
|
||||
this.quietLeft = quietLeft;
|
||||
this.quietRight = quietRight;
|
||||
this.none = none;
|
||||
this.general = general;
|
||||
this.logistics = logistics;
|
||||
this.standard = standard;
|
||||
this.heightNumerator = heightNumerator;
|
||||
this.heightDenominator = heightDenominator;
|
||||
this.heightMicrometres = heightMicrometres;
|
||||
this.widestEach = widestEach;
|
||||
this.widestFixed = widestFixed;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** The symbology a scene or a record names, or null.
|
||||
***************************************************************************/
|
||||
static Symbology of(String id)
|
||||
{
|
||||
for(Symbology symbology : values())
|
||||
{
|
||||
if(symbology.id.equals(id))
|
||||
{
|
||||
return (symbology);
|
||||
}
|
||||
}
|
||||
return (null);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** The name a scene writes and the encoder is asked for.
|
||||
***************************************************************************/
|
||||
String id()
|
||||
{
|
||||
return (id);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Whether the symbology is two-dimensional: fitted on both axes, with a
|
||||
** quiet zone on all four sides and no bar height of its own.
|
||||
***************************************************************************/
|
||||
boolean matrix()
|
||||
{
|
||||
return (matrix);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** The blank modules before the first bar, and for a two-dimensional symbol
|
||||
** on every side.
|
||||
***************************************************************************/
|
||||
int quietLeft()
|
||||
{
|
||||
return (quietLeft);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** The blank modules after the last bar.
|
||||
***************************************************************************/
|
||||
int quietRight()
|
||||
{
|
||||
return (quietRight);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** The narrowest bar at a resolution, in dots, when the scene states no
|
||||
** `minModule`: the physical minimum of the profile, or of the symbology's
|
||||
** standard profile when the scene names none, taken up to a whole dot.
|
||||
***************************************************************************/
|
||||
int narrowest(Profile profile, int dpi)
|
||||
{
|
||||
int micrometres = switch(profile == null ? standard : profile)
|
||||
{
|
||||
case NONE -> none;
|
||||
case GS1_GENERAL -> general;
|
||||
case GS1_LOGISTICS -> logistics;
|
||||
};
|
||||
return (new Length(Length.Unit.MM, micrometres).module(dpi));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** How tall the bars of a linear symbol of `bars` modules, first bar to
|
||||
** last, must be at a resolution: the stated share of their own width and
|
||||
** never less than the stated length, which is taken up to a whole dot. A
|
||||
** two-dimensional symbol has no such rule.
|
||||
***************************************************************************/
|
||||
ModuleFit.Height height(int bars, int dpi)
|
||||
{
|
||||
return (new ModuleFit.Height(bars, heightMicrometres == 0 ? 0 : new Length(Length.Unit.MM, heightMicrometres).module(dpi), heightNumerator, heightDenominator));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** The most modules, first bar to last, that a value of up to `characters`
|
||||
** characters needs in a linear symbology. For Code 128 and Code 93 that is
|
||||
** a value of characters from space to tilde, each of which is at most one
|
||||
** symbol character of Code 128 and two of Code 93; Code 39 and Interleaved
|
||||
** 2 of 5 hold nothing else, and an odd digit of Interleaved 2 of 5 is
|
||||
** never drawn. A GS1-128 element string of that many characters, brackets
|
||||
** included, is at most one symbol character fewer than its characters:
|
||||
** its brackets are not drawn, and the leading FNC1 and the separators are
|
||||
** fewer than they. An EAN/UPC symbol is one width whatever it holds.
|
||||
***************************************************************************/
|
||||
int widest(int characters)
|
||||
{
|
||||
return (widestEach * (this == ITF ? characters - characters % 2 : characters) + widestFixed);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Whether every value has the same width, so that no length need be
|
||||
** declared to know it.
|
||||
***************************************************************************/
|
||||
boolean fixed()
|
||||
{
|
||||
return (!matrix && widestEach == 0);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Whether the largest symbol of a declared length is known, which it is by
|
||||
** the arithmetic of `widest` for a linear symbology and for no
|
||||
** two-dimensional one. The default `datamatrix` and `pdf417` encoders
|
||||
** choose among shapes, so a shorter value may be the wider; and the `qr`
|
||||
** and `aztec` encoders change mode within a value, at a cost that makes
|
||||
** some values larger than as many plain bytes. So no one value of a length
|
||||
** stands for the rest. Each of these statements is held to the encoder by
|
||||
** test.
|
||||
***************************************************************************/
|
||||
boolean provable()
|
||||
{
|
||||
return (!matrix);
|
||||
}
|
||||
}
|
||||
@@ -83,11 +83,15 @@ class MutationCampaignTest
|
||||
// having reached the canvas and the emitter //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||
private static final AtomicInteger LABELS = new AtomicInteger();
|
||||
private static final AtomicInteger BARCODES = new AtomicInteger();
|
||||
|
||||
private static final Pattern LENGTH = Pattern.compile("-?[0-9]{1,5}(\\.[0-9]{1,3})?(mm|in|pt|%|dot)");
|
||||
private static final Pattern ELEMENT_ID = Pattern.compile("[A-Za-z][A-Za-z0-9_-]{0,31}");
|
||||
private static final Pattern TYPE_SIZE = Pattern.compile("([0-9]{1,5}(?:\\.[0-9]{1,3})?)(mm|in|pt)");
|
||||
private static final Set<String> FACES = Set.of("qqq-sans", "qqq-sans-bold", "qqq-condensed-bold", "qqq-mono", "qqq-mono-bold");
|
||||
private static final Pattern MODULE = Pattern.compile("[0-9]{1,5}(\\.[0-9]{1,3})?(mm|in|pt)|[0-9]{1,5}dot");
|
||||
private static final Pattern NOTHING = Pattern.compile("[0.]+[a-z]+");
|
||||
private static final Set<String> FIT_QUALIFIED = Set.of("code128", "code39", "code93", "itf", "gs1-128", "qr", "ean13", "ean8", "upca", "upce", "datamatrix", "pdf417", "aztec");
|
||||
|
||||
|
||||
|
||||
@@ -310,24 +314,28 @@ class MutationCampaignTest
|
||||
** second parser, either a version 1 scene (as above) or a version 2 scene
|
||||
** that keeps every rule this checks on its own: only the declared top-level
|
||||
** properties, a physical design size, at most 1000 elements with unique ids
|
||||
** of the fixed pattern, only boxes, groups and text (the types this renderer
|
||||
** draws), groups at most four deep, every length a number and a unit, every
|
||||
** text in a bundled face at a physical size with one value and only
|
||||
** documented fit, length and legibility settings at each resolution, and
|
||||
** overrides only for the three resolutions and for declared ids. About one
|
||||
** version 2 input in four is rendered instead of validated: its label must
|
||||
** then be the commands and graphic fields a version 2 label is made of, with
|
||||
** no ink outside the printable area, and validation must give the same
|
||||
** answer (but for a text bound to the record, which has no value here). Every other input is refused with a `Refusal`, which carries a
|
||||
** of the fixed pattern, only boxes, groups, text and barcodes (the types
|
||||
** this renderer draws), groups at most four deep, every length a number and
|
||||
** a unit, every text in a bundled face at a physical size with one value
|
||||
** and only documented fit, length and legibility settings at each
|
||||
** resolution, every barcode in a fit-qualified symbology with one value and
|
||||
** only documented module, quiet zone, height, length, profile and rotation
|
||||
** settings at each resolution, and overrides only for the three resolutions
|
||||
** and for declared ids. About one version 2 input in four is rendered
|
||||
** instead of validated: its label must then be the commands and graphic
|
||||
** fields a version 2 label is made of, with no ink outside the printable
|
||||
** area, and validation must give the same answer (but for a text or a
|
||||
** barcode bound to the record, which has no value here). Every other input
|
||||
** is refused with a `Refusal`, which carries a
|
||||
** diagnostic (or, for a version 1 source, with that parser's own refusal).
|
||||
** Any other exception, an `IllegalArgumentException` of the package's own
|
||||
** invariants among them, is a finding.
|
||||
** invariants or of a barcode encoder among them, is a finding.
|
||||
***************************************************************************/
|
||||
@Test
|
||||
void theVersionTwoSceneParserAcceptsOnlyVersionTwoScenes() throws Exception
|
||||
{
|
||||
List<byte[]> corpus = new ArrayList<>();
|
||||
for(String name : List.of("anchors", "rules", "groups", "text-faces", "text-fit", "text-turned"))
|
||||
for(String name : List.of("anchors", "rules", "groups", "text-faces", "text-fit", "text-turned", "barcode-linear", "barcode-matrix", "barcode-gs1"))
|
||||
{
|
||||
try(InputStream stream = MutationCampaignTest.class.getResourceAsStream("/scene2/cases/" + name + ".json"))
|
||||
{
|
||||
@@ -338,16 +346,22 @@ class MutationCampaignTest
|
||||
"\"children\"", "\"group\"", "\"box\"", "\"text\"", "\"barcode\"", "\"image\"", "\"id\"", "\"type\"", "\"anchor\"", "\"thickness\"", "\"fill\"", "\"rotation\"", "\"center\"", "true", "false", "null",
|
||||
"\"defaults\"", "\"font\"", "\"size\"", "\"value\"", "\"valuePath\"", "\"lineHeight\"", "\"fit\"", "\"maxLines\"", "\"shrinkTo\"", "\"overflow\"", "\"truncate\"", "\"refuse\"", "\"maxLength\"", "\"threshold\"",
|
||||
"\"reverse\"", "\"qqq-sans\"", "\"qqq-mono-bold\"", "\"qqq-\"", "\"/record/name\"", "90", "180", "270", "1000pt", "0.5", "1e400", "\\u0301", "\\t", "\\uD83D",
|
||||
"\"symbology\"", "\"symbologyPath\"", "\"allowedSymbologies\"", "\"minModule\"", "\"maxModule\"", "\"quiet\"", "\"gs1\"", "\"hri\"", "\"scannerSymbologyPath\"", "\"aimIdentifierPath\"",
|
||||
"\"code128\"", "\"gs1-128\"", "\"itf\"", "\"ean13\"", "\"qr\"", "\"pdf417\"", "\"codabar\"", "\"/record/code\"", "\"[01]00012345678905\"", "\"1dot\"", "[21]", "\\u00e9", "\\u2603", "4", "9", "64", "65",
|
||||
"\"barcodeProfile\"", "\"gs1-logistics\"", "\"gs1-general\"", "\"none\"", "\"minHeight\"", "\\\\<FNC1>", "\\\\<", "\"0012345678905\"", "7", "11",
|
||||
"mm", "in", "pt", "%", "dot", "-", ".", "203", "300", "600", "99999999999", "4294967499", "{", "}", "[", "]", ":", ",", "\\u0000"));
|
||||
int iterations = MutationCampaign.iterations(ITERATIONS);
|
||||
for(long seed : MutationCampaign.seeds(SEEDS))
|
||||
{
|
||||
LABELS.set(0);
|
||||
BARCODES.set(0);
|
||||
MutationCampaign.Result result = campaign.run(seed, iterations, MutationCampaignTest::checkLabelSource, MutationCampaignTest::labelRefusal);
|
||||
assertEquals(List.of(), result.findings(), "replay with -Dfuzz.seed=" + seed + " -Dfuzz.iterations=" + iterations);
|
||||
assertFalse(result.accepted() == 0 || result.refused() == 0, "the campaign must reach both acceptance and refusal: " + result);
|
||||
assertTrue(LABELS.get() > 0, "the campaign must render labels as well as validate scenes: " + result);
|
||||
System.out.println("version 2 campaign, seed " + seed + ": " + result.accepted() + " accepted (" + LABELS.get() + " labels rendered and read back), " + result.refused() + " refused");
|
||||
assertTrue(BARCODES.get() > 0, "the campaign must render labels that hold a barcode: " + result);
|
||||
System.out.println("version 2 campaign, seed " + seed + ": " + result.accepted() + " accepted (" + LABELS.get() + " labels rendered and read back, " + BARCODES.get() + " of them with a barcode), "
|
||||
+ result.refused() + " refused");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -411,14 +425,15 @@ class MutationCampaignTest
|
||||
}
|
||||
assertEquals(answer.get("diagnostics"), RendererMain.process(SceneFixture.request("VALIDATE", stock, source)).get("diagnostics"), "rendered what validation finds otherwise");
|
||||
LABELS.incrementAndGet();
|
||||
BARCODES.addAndGet(String.valueOf(answer.get("diagnostics")).contains("code=BARCODE_MODULE,") ? 1 : 0);
|
||||
}
|
||||
}
|
||||
catch(Refusal refusal)
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////////////////////
|
||||
// a text bound to the record has no value here, since the campaign's record is //
|
||||
// empty: that is the one refusal a render gives that validation, which reads no //
|
||||
// record, cannot //
|
||||
// a text or a barcode bound to the record has no value here, since the campaign's //
|
||||
// record is empty: that is the one refusal a render gives that validation, which //
|
||||
// reads no record, cannot //
|
||||
///////////////////////////////////////////////////////////////////////////////////////
|
||||
if(render && !"VALUE_MISSING".equals(refusal.diagnostic().code()))
|
||||
{
|
||||
@@ -473,13 +488,15 @@ class MutationCampaignTest
|
||||
String id = element.path("id").textValue();
|
||||
assertTrue(id != null && ELEMENT_ID.matcher(id).matches() && ids.add(id), () -> "accepted element id " + element.path("id"));
|
||||
String type = element.path("type").textValue();
|
||||
assertTrue("box".equals(type) || "group".equals(type) || "text".equals(type), () -> "accepted element type " + element.path("type"));
|
||||
assertTrue("box".equals(type) || "group".equals(type) || "text".equals(type) || "barcode".equals(type), () -> "accepted element type " + element.path("type"));
|
||||
for(String name : List.of("x", "y", "width", "height", "thickness"))
|
||||
{
|
||||
assertTrue(!element.has(name) || (element.get(name).isTextual() && LENGTH.matcher(element.get(name).textValue()).matches()), () -> "accepted " + name + " " + element.get(name));
|
||||
}
|
||||
Set<String> allowed = "box".equals(type) ? Set.of("id", "type", "anchor", "x", "y", "width", "height", "align", "valign", "thickness", "fill")
|
||||
: "text".equals(type) ? Set.of("id", "type", "anchor", "x", "y", "width", "height", "align", "valign", "value", "valuePath", "font", "size", "lineHeight", "fit", "maxLength", "threshold", "reverse", "rotation")
|
||||
: "barcode".equals(type) ? Set.of("id", "type", "anchor", "x", "y", "width", "height", "align", "valign", "symbology", "symbologyPath", "allowedSymbologies", "value", "valuePath", "minModule", "maxModule", "quiet",
|
||||
"scannerSymbologyPath", "aimIdentifierPath", "rotation", "barcodeProfile", "minHeight", "maxLength")
|
||||
: Set.of("id", "type", "anchor", "x", "y", "width", "height", "align", "valign", "children");
|
||||
element.fieldNames().forEachRemaining(name -> assertTrue(allowed.contains(name), () -> "accepted " + type + " property " + name));
|
||||
if("group".equals(type))
|
||||
@@ -491,6 +508,85 @@ class MutationCampaignTest
|
||||
{
|
||||
assertVersionTwoText(element, scene);
|
||||
}
|
||||
if("barcode".equals(type))
|
||||
{
|
||||
assertVersionTwoBarcode(element, scene);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** The independent reading of an accepted barcode element, as it stands at
|
||||
** each resolution: the element, then the overrides for that resolution in
|
||||
** order. It states one fit-qualified symbology, or a pointer into the
|
||||
** record with a list of distinct fit-qualified symbologies; exactly one of
|
||||
** a literal value of 1 to 4000 characters and a pointer; a scanner
|
||||
** symbology and identifier together or not at all; module sizes and a bar
|
||||
** height that are more than nothing in a physical unit or dots; a quiet
|
||||
** zone of at least the widest that any symbology it may draw asks for (11
|
||||
** modules for EAN-13, 10 for the other linear symbols but 9 for the UPCs
|
||||
** and 7 for EAN-8, 4 for a two-dimensional one) and at most 64; a longest
|
||||
** length of 1 to 4000; one of the three profiles; and a quarter-turn
|
||||
** rotation. An element that asks for `gs1` or `hri` is never accepted, nor
|
||||
** at the resolution the campaign renders at, where the value is read, is a
|
||||
** literal that holds the beginning of an escape of the encoder.
|
||||
***************************************************************************/
|
||||
private static void assertVersionTwoBarcode(JsonNode element, JsonNode scene)
|
||||
{
|
||||
for(int dpi : List.of(203, 300, 600))
|
||||
{
|
||||
ObjectNode stated = INDEPENDENT.createObjectNode();
|
||||
element.properties().forEach(property -> stated.set(property.getKey(), property.getValue()));
|
||||
for(JsonNode override : scene.path("overrides"))
|
||||
{
|
||||
boolean applies = false;
|
||||
for(JsonNode resolution : override.path("when").path("dpi"))
|
||||
{
|
||||
applies |= resolution.intValue() == dpi;
|
||||
}
|
||||
if(applies)
|
||||
{
|
||||
override.path("set").path(element.path("id").textValue()).properties().forEach(property -> stated.set(property.getKey(), property.getValue()));
|
||||
}
|
||||
}
|
||||
String about = "accepted barcode " + element.path("id") + " at " + dpi + " dpi with ";
|
||||
assertFalse(stated.has("gs1") || stated.has("hri"), () -> about + "gs1 or hri");
|
||||
assertTrue(stated.has("symbology") != stated.has("symbologyPath"), () -> about + "both or neither of symbology and symbologyPath");
|
||||
List<String> drawn = new ArrayList<>();
|
||||
if(stated.has("symbology"))
|
||||
{
|
||||
assertTrue(stated.get("symbology").isTextual() && FIT_QUALIFIED.contains(stated.get("symbology").textValue()) && !stated.has("allowedSymbologies"), () -> about + "symbology " + stated.get("symbology"));
|
||||
drawn.add(stated.get("symbology").textValue());
|
||||
}
|
||||
else
|
||||
{
|
||||
assertTrue(stated.path("allowedSymbologies").isArray() && !stated.path("allowedSymbologies").isEmpty(), () -> about + "allowedSymbologies " + stated.path("allowedSymbologies"));
|
||||
for(JsonNode allowed : stated.path("allowedSymbologies"))
|
||||
{
|
||||
assertTrue(allowed.isTextual() && FIT_QUALIFIED.contains(allowed.textValue()) && !drawn.contains(allowed.textValue()), () -> about + "allowed symbology " + allowed);
|
||||
drawn.add(allowed.textValue());
|
||||
}
|
||||
}
|
||||
assertTrue(stated.has("value") != stated.has("valuePath"), () -> about + "both or neither of value and valuePath");
|
||||
assertTrue(!stated.has("value") || (stated.get("value").isTextual() && !stated.get("value").textValue().isEmpty() && stated.get("value").textValue().length() <= 4000), () -> about + "a value that is not text");
|
||||
assertTrue(stated.has("scannerSymbologyPath") == stated.has("aimIdentifierPath"), () -> about + "half a scanner profile");
|
||||
for(String path : List.of("symbologyPath", "valuePath", "scannerSymbologyPath", "aimIdentifierPath"))
|
||||
{
|
||||
assertTrue(!stated.has(path) || (stated.get(path).isTextual() && stated.get(path).textValue().startsWith("/") && stated.get(path).textValue().length() <= 256), () -> about + path + " " + stated.get(path));
|
||||
}
|
||||
assertFalse(dpi == 203 && stated.path("value").asText().contains(String.valueOf((char) 92) + "<"), () -> about + "an escape of the encoder in its value");
|
||||
assertTrue(whole(stated.path("maxLength"), 1, 4000), () -> about + "maxLength " + stated.path("maxLength"));
|
||||
assertTrue(!stated.has("barcodeProfile") || Set.of("none", "gs1-general", "gs1-logistics").contains(String.valueOf(stated.get("barcodeProfile").textValue())), () -> about + "barcodeProfile " + stated.get("barcodeProfile"));
|
||||
for(String module : List.of("minModule", "maxModule", "minHeight"))
|
||||
{
|
||||
String size = stated.path(module).textValue();
|
||||
assertTrue(!stated.has(module) || (size != null && MODULE.matcher(size).matches() && !NOTHING.matcher(size).matches()), () -> about + module + " " + stated.get(module));
|
||||
}
|
||||
int fewest = drawn.stream().mapToInt(symbology -> Map.of("ean13", 11, "upca", 9, "upce", 9, "ean8", 7, "qr", 4, "datamatrix", 4, "pdf417", 4, "aztec", 4).getOrDefault(symbology, 10)).max().orElseThrow();
|
||||
assertTrue(!stated.has("quiet") || (stated.get("quiet").isInt() && stated.get("quiet").intValue() >= fewest && stated.get("quiet").intValue() <= 64), () -> about + "quiet " + stated.get("quiet"));
|
||||
assertTrue(!stated.has("rotation") || (stated.get("rotation").isInt() && Set.of(0, 90, 180, 270).contains(stated.get("rotation").intValue())), () -> about + "rotation " + stated.get("rotation"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -128,13 +128,14 @@ class DiagnosticsTest
|
||||
{
|
||||
Diagnostics diagnostics = new Diagnostics(true);
|
||||
for(String code : List.of("SCENE_INVALID", "STOCK_MISMATCH", "TARGET_REQUIRES_SCENE_2", "ELEMENT_OUTSIDE_PRINTABLE_AREA", "TEXT_DOES_NOT_FIT", "VALUE_MISSING", "VALUE_TOO_LONG", "FONT_UNKNOWN",
|
||||
"FONT_MISSING_GLYPH", "RESERVED_FONT_NAME", "BARCODE_DOES_NOT_FIT", "GS1_INVALID", "ASSET_UNKNOWN", "PREVIEW_TOO_LARGE", "RENDERER_TOO_OLD"))
|
||||
"FONT_MISSING_GLYPH", "RESERVED_FONT_NAME", "BARCODE_DOES_NOT_FIT", "GS1_INVALID", "ASSET_UNKNOWN", "PREVIEW_TOO_LARGE", "RENDERER_TOO_OLD",
|
||||
"VALUE_NOT_ENCODABLE", "QUIET_ZONE_INTRUDED"))
|
||||
{
|
||||
assertEquals(code, diagnostics.refuse(code, null, null, null, null, null).getMessage());
|
||||
assertThrows(IllegalStateException.class, () -> diagnostics.warn(code, null, null, null, null, null));
|
||||
assertThrows(IllegalStateException.class, () -> diagnostics.inform(code, null, null, null, null, null));
|
||||
}
|
||||
for(String code : List.of("TEXT_TRUNCATED", "TEXT_SMALL_FOR_DPI", "TEXT_LIGHT_FACE", "FIT_NOT_PROVEN", "BARCODE_MODULE_BELOW_RECOMMENDED", "QUIET_ZONE_INTRUDED", "ELEMENTS_OVERLAP", "IMAGE_UPSCALED",
|
||||
for(String code : List.of("TEXT_TRUNCATED", "TEXT_SMALL_FOR_DPI", "TEXT_LIGHT_FACE", "FIT_NOT_PROVEN", "BARCODE_MODULE_BELOW_RECOMMENDED", "BARCODE_HEIGHT_BELOW_RECOMMENDED", "ELEMENTS_OVERLAP", "IMAGE_UPSCALED",
|
||||
"DEVICE_UNIT_USED", "PRINTER_FONT_NOT_SHOWN"))
|
||||
{
|
||||
diagnostics.warn(code, null, null, null, null, null);
|
||||
|
||||
@@ -508,10 +508,14 @@ class Scene2Test
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Barcodes and images are parsed and placed by this renderer and then
|
||||
** refused by name, with the box they would have had. The design's own
|
||||
** example scene is the case: its barcode is refused, and its caption, a text
|
||||
** bound to the record, is refused for the value the empty record lacks.
|
||||
** An image is parsed and placed by this renderer and then refused by name,
|
||||
** with the box it would have had: text and barcodes are drawn, images not
|
||||
** yet. The design's own example scene with a logo beside it is the case.
|
||||
** Validated, the logo is its first error. Rendered with an empty record,
|
||||
** its barcode is refused first, for the value the record lacks, and its
|
||||
** caption for the same; the barcode's standing warnings come with it: no
|
||||
** length is declared, and the 0.25 mm the example states is under the
|
||||
** 0.495 mm of a GS1-128 on a logistic unit.
|
||||
***************************************************************************/
|
||||
@Test
|
||||
void anElementThisRendererCannotDrawIsRefusedByName() throws Exception
|
||||
@@ -520,17 +524,21 @@ class Scene2Test
|
||||
+ "\"samples\":{\"typical\":{\"record\":{\"humanReadable\":\"(01)00012345678905(10)L240611A\",\"rendererValue\":\"[01]00012345678905[10]L240611A\"}}},"
|
||||
+ "\"elements\":[{\"id\":\"code\",\"type\":\"barcode\",\"symbology\":\"gs1-128\",\"valuePath\":\"/record/rendererValue\",\"anchor\":\"top-center\",\"y\":\"0.5mm\",\"width\":\"94mm\",\"height\":\"11mm\","
|
||||
+ "\"align\":\"center\",\"valign\":\"middle\",\"minModule\":\"0.25mm\",\"quiet\":10},"
|
||||
+ "{\"id\":\"logo\",\"type\":\"image\",\"asset\":\"logo\",\"width\":\"0.5mm\",\"height\":\"10mm\"},"
|
||||
+ "{\"id\":\"caption\",\"type\":\"text\",\"valuePath\":\"/record/humanReadable\",\"anchor\":\"bottom-center\",\"y\":\"0.5mm\",\"width\":\"94mm\",\"height\":\"6.5mm\","
|
||||
+ "\"align\":\"center\",\"valign\":\"middle\",\"size\":\"11pt\",\"maxLength\":48,\"fit\":{\"maxLines\":2,\"shrinkTo\":\"7pt\",\"overflow\":\"refuse\"}}],"
|
||||
+ "\"overrides\":[{\"when\":{\"dpi\":[203]},\"set\":{\"caption\":{\"threshold\":144}}}]}";
|
||||
Map<String, Object> code = Map.of("code", "RENDERER_TOO_OLD", "severity", "ERROR", "element", "code", "box", List.of(6, 4, 751, 87));
|
||||
Map<String, Object> code = Map.of("code", "VALUE_MISSING", "severity", "ERROR", "element", "code", "pointer", "/elements/0/valuePath", "box", List.of(6, 4, 751, 87));
|
||||
Map<String, Object> caption = Map.of("code", "VALUE_MISSING", "severity", "ERROR", "element", "caption", "box", List.of(6, 100, 751, 51));
|
||||
assertEquals(code, refused(example));
|
||||
Map<String, Object> logo = Map.of("code", "RENDERER_TOO_OLD", "severity", "ERROR", "element", "logo", "box", List.of(0, 0, 3, 79));
|
||||
assertEquals(logo, refused(example));
|
||||
assertEquals(code, SceneFixture.refusal(() -> RendererMain.process(SceneFixture.request("RENDER", example))));
|
||||
|
||||
Map<String, Object> answer = RendererMain.process(SceneFixture.request("RENDER", example).put("diagnostics", "COLLECT"));
|
||||
assertEquals("REFUSED", answer.get("outcome"));
|
||||
assertEquals(List.of(code, caption), answer.get("diagnostics"));
|
||||
assertEquals(List.of(Map.of("code", "FIT_NOT_PROVEN", "severity", "WARNING", "element", "code", "box", List.of(6, 4, 751, 87)),
|
||||
Map.of("code", "BARCODE_MODULE_BELOW_RECOMMENDED", "severity", "WARNING", "element", "code", "box", List.of(6, 4, 751, 87), "measured", Map.of("minModuleDots", 2), "limit", Map.of("minModuleDots", 4)),
|
||||
code, logo, caption), answer.get("diagnostics"));
|
||||
assertFalse(answer.containsKey("base64"));
|
||||
|
||||
assertEquals(Map.of("code", "RENDERER_TOO_OLD", "severity", "ERROR", "element", "logo", "box", List.of(0, 0, 79, 79)),
|
||||
@@ -575,8 +583,8 @@ class Scene2Test
|
||||
{
|
||||
String source = SceneFixture.scene("{\"id\":\"good\",\"type\":\"box\",\"x\":\"5dot\",\"width\":\"16dot\",\"height\":\"4dot\",\"fill\":true},"
|
||||
+ "{\"id\":\"wide\",\"type\":\"box\",\"x\":\"90mm\",\"width\":\"10mm\",\"height\":\"5mm\",\"thickness\":\"1pt\"},"
|
||||
+ "{\"id\":\"g\",\"type\":\"group\",\"width\":\"0.1mm\",\"children\":[{\"id\":\"unseen\",\"type\":\"barcode\",\"value\":\"TEST-0001\"}]},"
|
||||
+ "{\"id\":\"words\",\"type\":\"barcode\",\"value\":\"TEST-0001\",\"y\":\"10mm\",\"height\":\"5mm\"},"
|
||||
+ "{\"id\":\"g\",\"type\":\"group\",\"width\":\"0.1mm\",\"children\":[{\"id\":\"unseen\",\"type\":\"image\",\"asset\":\"logo\"}]},"
|
||||
+ "{\"id\":\"words\",\"type\":\"image\",\"asset\":\"logo\",\"y\":\"10mm\",\"height\":\"5mm\"},"
|
||||
+ "{\"id\":\"also\",\"type\":\"box\",\"anchor\":\"bottom-left\",\"width\":\"8dot\",\"height\":\"2dot\",\"fill\":true}");
|
||||
List<Map<String, Object>> expected = List.of(
|
||||
Map.of("code", "DEVICE_UNIT_USED", "severity", "WARNING", "element", "good", "pointer", "/elements/0/x"),
|
||||
|
||||
@@ -0,0 +1,359 @@
|
||||
/*
|
||||
* 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.scene2.barcode;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.IntFunction;
|
||||
import java.util.function.UnaryOperator;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import com.google.zxing.BarcodeFormat;
|
||||
import com.google.zxing.Result;
|
||||
import com.google.zxing.ResultMetadataType;
|
||||
import com.kingsrook.qbits.renderer.RendererMain;
|
||||
import com.kingsrook.qbits.renderer.Wire;
|
||||
import com.kingsrook.qbits.renderer.scene2.Frame;
|
||||
import com.kingsrook.qbits.renderer.scene2.Refusal;
|
||||
import com.kingsrook.qbits.renderer.scene2.Scene2;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Decode tests: every fit-qualified symbology at 203, 300 and 600 dpi, for a
|
||||
** short, a typical and the longest value its box holds. Each label is the
|
||||
** emitted ZPL, its page rebuilt from the graphic fields and read by ZXing:
|
||||
** the content, the symbology, the module in dots and the blank margins are
|
||||
** all taken from what a printer would be sent. Code 128 is read three ways,
|
||||
** since its encoder changes code set with the content: letters, digits alone
|
||||
** (pairs of digits in code set C, of an even and of an odd count) and mixed
|
||||
** content. A retail symbol is given its whole number, check digit included,
|
||||
** and reads back as that number.
|
||||
*******************************************************************************/
|
||||
class BarcodeDecodeTest
|
||||
{
|
||||
private static final String LETTERS = "TEST0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
private static final String DIGITS = "0012345678";
|
||||
private static final String MIXED = "Ab-1234567/cd 890.XY12z";
|
||||
private static final String GTIN = "[01]00012345678905[11]260101";
|
||||
private static final String UNIT = "00012345678901234";
|
||||
private static final char SEPARATOR = (char) 29;
|
||||
private static final List<Integer> DESIGN = List.of(2, 3, 6);
|
||||
private static final List<Integer> RETAIL = List.of(3, 4, 7);
|
||||
private static final List<Integer> DISTRIBUTION = List.of(4, 6, 12);
|
||||
private static final List<Integer> MATRIX = List.of(3, 5, 9);
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** One symbology: how it is read back, its box, the profile the scene
|
||||
** names, the quiet zones and narrowest bars it must then have (the bars in
|
||||
** dots at 203, 300 and 600 dpi), the values to try by length (the lengths
|
||||
** it is tried at lie between `fewest` and `most` in steps of `step`) and
|
||||
** what a scanner reads for a value.
|
||||
***************************************************************************/
|
||||
private record Profile(String name, String symbology, BarcodeFormat format, String width, String height, String profile, int quietLeft, int quietRight, List<Integer> narrowest, IntFunction<String> value,
|
||||
UnaryOperator<String> decoded, int fewest, int typical, int most, int step)
|
||||
{
|
||||
/************************************************************************
|
||||
** The name of the profile in a test report.
|
||||
************************************************************************/
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return (name);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** The first characters of a repeated alphabet.
|
||||
***************************************************************************/
|
||||
private static String repeated(String alphabet, int length)
|
||||
{
|
||||
return (alphabet.repeat(length / alphabet.length() + 1).substring(0, length));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** A GS1 element string in the brackets the encoder reads: a GTIN, a date,
|
||||
** then a serial number of up to 20 characters, and past 20 a lot as well,
|
||||
** which is of variable length and so is followed by a separator.
|
||||
***************************************************************************/
|
||||
private static String elementString(int length)
|
||||
{
|
||||
return (GTIN + (length > 20 ? "[10]" + repeated(LETTERS, length - 20) : "") + "[21]" + repeated(LETTERS, Math.min(length, 20)));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** What a scanner reads from a GS1-128: the `]C1` identifier for the leading
|
||||
** FNC1, the identifiers and their data without brackets, and a separator
|
||||
** where a field of variable length is followed by another.
|
||||
***************************************************************************/
|
||||
private static String scanned(String elementString)
|
||||
{
|
||||
String[] fields = elementString.substring(1).split("\\[", -1);
|
||||
StringBuilder scanned = new StringBuilder("]C1");
|
||||
for(int index = 0; index < fields.length; index++)
|
||||
{
|
||||
scanned.append(fields[index].replace("]", ""));
|
||||
if(fields[index].startsWith("10]") && index < fields.length - 1)
|
||||
{
|
||||
scanned.append(SEPARATOR);
|
||||
}
|
||||
}
|
||||
return (scanned.toString());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** The thirteen fit-qualified symbologies. The retail symbols have one
|
||||
** length and are tall, as their specification asks; their check digits
|
||||
** here were worked out by hand. A GS1-128 is read under both of its
|
||||
** profiles: as a serial shipping container code at the 0.495 mm of a
|
||||
** logistic unit, which is all that fits 90 mm at that module, and as a
|
||||
** GTIN with a date and a serial number at the design's 0.25 mm.
|
||||
***************************************************************************/
|
||||
private static List<Profile> profiles()
|
||||
{
|
||||
UnaryOperator<String> same = value -> value;
|
||||
return (List.of(
|
||||
new Profile("code128", "code128", BarcodeFormat.CODE_128, "90mm", "12mm", null, 10, 10, DESIGN, length -> repeated(LETTERS, length), same, 1, 12, 80, 1),
|
||||
new Profile("code128 of digits", "code128", BarcodeFormat.CODE_128, "90mm", "12mm", null, 10, 10, DESIGN, length -> repeated(DIGITS, length), same, 1, 13, 120, 1),
|
||||
new Profile("code128 of mixed content", "code128", BarcodeFormat.CODE_128, "90mm", "12mm", null, 10, 10, DESIGN, length -> repeated(MIXED, length), same, 1, 23, 80, 1),
|
||||
new Profile("code39", "code39", BarcodeFormat.CODE_39, "90mm", "12mm", null, 10, 10, DESIGN, length -> repeated(LETTERS, length), same, 1, 10, 80, 1),
|
||||
new Profile("code93", "code93", BarcodeFormat.CODE_93, "90mm", "12mm", null, 10, 10, DESIGN, length -> repeated(LETTERS, length), same, 1, 10, 80, 1),
|
||||
new Profile("itf", "itf", BarcodeFormat.ITF, "90mm", "12mm", null, 10, 10, DISTRIBUTION, length -> repeated(DIGITS, length), same, 6, 14, 120, 2),
|
||||
new Profile("itf of no profile", "itf", BarcodeFormat.ITF, "90mm", "12mm", "none", 10, 10, DESIGN, length -> repeated(DIGITS, length), same, 6, 14, 120, 2),
|
||||
new Profile("gs1-128 of a logistic unit", "gs1-128", BarcodeFormat.CODE_128, "90mm", "13mm", null, 10, 10, DISTRIBUTION, length -> "[00]" + UNIT + BarcodeFixture.checkDigit(UNIT), BarcodeDecodeTest::scanned, 1, 1, 1, 1),
|
||||
new Profile("gs1-128", "gs1-128", BarcodeFormat.CODE_128, "90mm", "12mm", "gs1-general", 10, 10, DESIGN, BarcodeDecodeTest::elementString, BarcodeDecodeTest::scanned, 1, 8, 40, 1),
|
||||
new Profile("ean13", "ean13", BarcodeFormat.EAN_13, "45mm", "27mm", null, 11, 7, RETAIL, length -> "0012345678905", same, 13, 13, 13, 1),
|
||||
new Profile("ean8", "ean8", BarcodeFormat.EAN_8, "45mm", "27mm", null, 7, 7, RETAIL, length -> "12345670", same, 8, 8, 8, 1),
|
||||
new Profile("upca", "upca", BarcodeFormat.UPC_A, "45mm", "27mm", null, 9, 9, RETAIL, length -> "012345678905", same, 12, 12, 12, 1),
|
||||
new Profile("upce", "upce", BarcodeFormat.UPC_E, "45mm", "27mm", null, 9, 7, RETAIL, length -> "04252614", same, 8, 8, 8, 1),
|
||||
new Profile("ean13 in general distribution", "ean13", BarcodeFormat.EAN_13, "60mm", "36mm", "gs1-logistics", 11, 7, DISTRIBUTION, length -> "4006381333931", same, 13, 13, 13, 1),
|
||||
new Profile("qr", "qr", BarcodeFormat.QR_CODE, "40mm", "40mm", null, 4, 4, MATRIX, length -> repeated(LETTERS, length), same, 1, 40, 2000, 1),
|
||||
new Profile("datamatrix", "datamatrix", BarcodeFormat.DATA_MATRIX, "40mm", "40mm", null, 4, 4, MATRIX, length -> repeated(LETTERS, length), same, 1, 40, 2000, 1),
|
||||
new Profile("pdf417", "pdf417", BarcodeFormat.PDF_417, "90mm", "30mm", null, 4, 4, MATRIX, length -> repeated(LETTERS, length), same, 1, 40, 1000, 1),
|
||||
new Profile("aztec", "aztec", BarcodeFormat.AZTEC, "40mm", "40mm", null, 4, 4, MATRIX, length -> repeated(LETTERS, length), same, 1, 40, 2000, 1)));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** A barcode in the middle of the label. Nothing says where the symbol sits
|
||||
** in its box, so it sits in the middle of that too.
|
||||
***************************************************************************/
|
||||
private static String element(Profile profile, String value)
|
||||
{
|
||||
ObjectNode element = Wire.JSON.createObjectNode().put("id", "code").put("type", "barcode").put("symbology", profile.symbology()).put("value", value);
|
||||
if(profile.profile() != null)
|
||||
{
|
||||
element.put("barcodeProfile", profile.profile());
|
||||
}
|
||||
return (element.put("anchor", "center").put("width", profile.width()).put("height", profile.height()).toString());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Whether a value of this length is accepted at every resolution.
|
||||
***************************************************************************/
|
||||
private static boolean fits(Profile profile, int length) throws Exception
|
||||
{
|
||||
for(int dpi : Scene2.RESOLUTIONS)
|
||||
{
|
||||
try
|
||||
{
|
||||
RendererMain.process(BarcodeFixture.request("VALIDATE", dpi, element(profile, profile.value().apply(length)), Map.of()));
|
||||
}
|
||||
catch(Refusal refusal)
|
||||
{
|
||||
return (false);
|
||||
}
|
||||
}
|
||||
return (true);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** The longest value of a profile that every resolution accepts, found by
|
||||
** halving between a length that fits and one that does not.
|
||||
***************************************************************************/
|
||||
private static int longest(Profile profile) throws Exception
|
||||
{
|
||||
int fits = 0;
|
||||
int refused = (profile.most() - profile.fewest()) / profile.step() + 1;
|
||||
assertTrue(fits(profile, profile.fewest()), profile + " must hold its shortest value");
|
||||
while(refused - fits > 1)
|
||||
{
|
||||
int middle = (fits + refused) / 2;
|
||||
if(fits(profile, profile.fewest() + middle * profile.step()))
|
||||
{
|
||||
fits = middle;
|
||||
}
|
||||
else
|
||||
{
|
||||
refused = middle;
|
||||
}
|
||||
}
|
||||
return (profile.fewest() + fits * profile.step());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Every profile.
|
||||
***************************************************************************/
|
||||
static List<Profile> everyProfile()
|
||||
{
|
||||
return (profiles());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** A short, a typical and the longest value at each resolution: the label
|
||||
** decodes to the value, in the symbology asked for, at a whole-dot module
|
||||
** that is the largest the box holds and never under the minimum of its
|
||||
** profile, with the quiet zones of its symbology blank, the symbol in the
|
||||
** middle of its box within one dot and, for a linear symbol, bars as tall
|
||||
** as its symbology asks.
|
||||
***************************************************************************/
|
||||
@ParameterizedTest(name = "{0}")
|
||||
@MethodSource("everyProfile")
|
||||
void theLabelDecodesToItsValueAtEveryResolution(Profile profile) throws Exception
|
||||
{
|
||||
int longest = longest(profile);
|
||||
if(longest < profile.most())
|
||||
{
|
||||
int longer = longest + profile.step();
|
||||
List<String> codes = new ArrayList<>();
|
||||
for(int dpi : Scene2.RESOLUTIONS)
|
||||
{
|
||||
try
|
||||
{
|
||||
RendererMain.process(BarcodeFixture.request("RENDER", dpi, element(profile, profile.value().apply(longer)), Map.of()));
|
||||
}
|
||||
catch(Refusal refusal)
|
||||
{
|
||||
codes.add(refusal.diagnostic().code());
|
||||
}
|
||||
}
|
||||
assertTrue(codes.contains("BARCODE_DOES_NOT_FIT"), profile + " one step past its longest value: " + codes);
|
||||
}
|
||||
Symbology rules = Symbology.of(profile.symbology());
|
||||
StringBuilder report = new StringBuilder(profile.name());
|
||||
for(int length : List.of(profile.fewest(), profile.typical(), longest))
|
||||
{
|
||||
String value = profile.value().apply(length);
|
||||
for(int dpi : Scene2.RESOLUTIONS)
|
||||
{
|
||||
String about = profile + " of " + length + " characters at " + dpi + " dpi";
|
||||
Map<String, Object> answer = BarcodeFixture.answer(BarcodeFixture.request("RENDER", dpi, element(profile, value), Map.of()));
|
||||
Map<String, Object> chosen = BarcodeFixture.finding(answer, "BARCODE_MODULE");
|
||||
boolean matrix = List.of("qr", "datamatrix", "pdf417", "aztec").contains(profile.symbology());
|
||||
int module = BarcodeFixture.number(chosen, "measured", "moduleDots");
|
||||
int modules = BarcodeFixture.number(chosen, "measured", "modules");
|
||||
int quietLeft = BarcodeFixture.number(chosen, "measured", "quietLeftModules");
|
||||
int quietRight = BarcodeFixture.number(chosen, "measured", "quietRightModules");
|
||||
int width = BarcodeFixture.number(chosen, "measured", "widthDots");
|
||||
int height = BarcodeFixture.number(chosen, "measured", "heightDots");
|
||||
Frame box = BarcodeFixture.box(chosen, dpi);
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
// the module: whole dots, at least the minimum for the resolution, and the largest //
|
||||
// the box holds, since one dot more would not fit or would ask for taller bars //
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
int minimum = profile.narrowest().get(Scene2.RESOLUTIONS.indexOf(dpi));
|
||||
int bars = modules - quietLeft - quietRight;
|
||||
assertEquals(List.of(profile.quietLeft(), profile.quietRight()), List.of(quietLeft, quietRight), about);
|
||||
assertEquals(minimum, BarcodeFixture.number(chosen, "limit", "minModuleDots"), about);
|
||||
assertTrue(module >= minimum, about + ": module " + module);
|
||||
assertEquals(modules * module, width, about);
|
||||
assertTrue(modules * (module + 1) > box.width() || (matrix ? height / module * (module + 1) > box.height() : rules.height(bars, dpi).at(module + 1) > box.height()), about + ": a larger module fits");
|
||||
assertTrue(width <= box.width() && height <= box.height(), about + ": the symbol leaves its box");
|
||||
assertTrue(matrix || rules.height(bars, dpi).at(module) <= box.height(), about + ": bars too short");
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
// the picture: ink only inside the box less the quiet zones, the symbol with its //
|
||||
// quiet zones in the middle within one dot, and every bar and space a whole number //
|
||||
// of modules //
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
boolean[][] page = BarcodeFixture.page(answer);
|
||||
BarcodeFixture.Ink ink = BarcodeFixture.ink(page);
|
||||
int left = ink.left() - box.x();
|
||||
int right = box.right() - 1 - ink.right();
|
||||
int top = ink.top() - box.y();
|
||||
int bottom = box.bottom() - 1 - ink.bottom();
|
||||
assertTrue(left >= quietLeft * module && right >= quietRight * module, about + ": quiet zones " + left + " and " + right);
|
||||
boolean edged = !"aztec".equals(profile.symbology());
|
||||
assertTrue(left >= (box.width() - width) / 2 + quietLeft * module && (!edged || left == (box.width() - width) / 2 + quietLeft * module), about + ": left margin " + left);
|
||||
if(matrix)
|
||||
{
|
||||
assertTrue(top >= quietLeft * module && bottom >= quietLeft * module, about + ": quiet zones " + top + " and " + bottom);
|
||||
assertTrue(top >= (box.height() - height) / 2 + quietLeft * module && (!edged || top == (box.height() - height) / 2 + quietLeft * module), about + ": top margin " + top);
|
||||
}
|
||||
else
|
||||
{
|
||||
assertTrue(Math.abs((left - quietLeft * module) - (right - quietRight * module)) <= 1, about + ": margins " + left + " and " + right);
|
||||
assertEquals(box.height(), ink.height(), about);
|
||||
assertEquals(0, top, about);
|
||||
List<Integer> runs = BarcodeFixture.runs(page[(ink.top() + ink.bottom()) / 2]);
|
||||
assertEquals(module, runs.stream().mapToInt(Integer::intValue).min().orElseThrow(), about);
|
||||
assertEquals(bars * module, runs.stream().mapToInt(Integer::intValue).sum(), about);
|
||||
}
|
||||
for(int y = ink.top(); y <= ink.bottom(); y++)
|
||||
{
|
||||
for(int run : BarcodeFixture.runs(page[y]))
|
||||
{
|
||||
assertEquals(0, run % module, about + ": a run of " + run + " dots in row " + y);
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
// the content, read by a decoder that shares nothing with the encoder //
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
boolean gs1 = "gs1-128".equals(profile.symbology());
|
||||
Result decoded = BarcodeFixture.decode(page, profile.format(), gs1);
|
||||
assertEquals(profile.format(), decoded.getBarcodeFormat(), about);
|
||||
assertEquals(profile.decoded().apply(value), decoded.getText(), about);
|
||||
if(gs1)
|
||||
{
|
||||
assertEquals("]C1", decoded.getResultMetadata().get(ResultMetadataType.SYMBOLOGY_IDENTIFIER), about);
|
||||
assertTrue(decoded.getText().startsWith("]C1"), about);
|
||||
}
|
||||
report.append(' ').append(length).append('@').append(dpi).append('=').append(module);
|
||||
}
|
||||
}
|
||||
System.out.println("decoded " + report);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,310 @@
|
||||
/*
|
||||
* 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.scene2.barcode;
|
||||
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
import java.util.EnumMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import com.google.zxing.BarcodeFormat;
|
||||
import com.google.zxing.BinaryBitmap;
|
||||
import com.google.zxing.DecodeHintType;
|
||||
import com.google.zxing.MultiFormatReader;
|
||||
import com.google.zxing.Result;
|
||||
import com.google.zxing.client.j2se.BufferedImageLuminanceSource;
|
||||
import com.google.zxing.common.HybridBinarizer;
|
||||
import com.kingsrook.qbits.renderer.RendererMain;
|
||||
import com.kingsrook.qbits.renderer.Wire;
|
||||
import com.kingsrook.qbits.renderer.scene2.Frame;
|
||||
import com.kingsrook.qbits.renderer.scene2.SceneFixture;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Shared test support for barcodes in version 2 scenes. Every test here looks
|
||||
** at the label a printer would be sent: the page is rebuilt from the graphic
|
||||
** fields of the emitted ZPL and, where a test decodes, read by ZXing, a
|
||||
** decoder that shares nothing with the encoder.
|
||||
*******************************************************************************/
|
||||
final class BarcodeFixture
|
||||
{
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// a 4 x 2 in label with 3 mm margins: room for a long linear symbol and for a large matrix //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
static final int WIDTH = 101_600;
|
||||
static final int HEIGHT = 50_800;
|
||||
static final int MARGIN = 3000;
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Utility class.
|
||||
***************************************************************************/
|
||||
private BarcodeFixture()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** The ink of a page: the first and last inked column and row, and the
|
||||
** narrowest and widest run of ink along the row through the middle.
|
||||
***************************************************************************/
|
||||
record Ink(int left, int top, int right, int bottom)
|
||||
{
|
||||
/************************************************************************
|
||||
** Columns of ink, first to last.
|
||||
************************************************************************/
|
||||
int width()
|
||||
{
|
||||
return (right - left + 1);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/************************************************************************
|
||||
** Rows of ink, first to last.
|
||||
************************************************************************/
|
||||
int height()
|
||||
{
|
||||
return (bottom - top + 1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** A request for a scene of these elements on the 4 x 2 in label, at a
|
||||
** render target, with a record.
|
||||
***************************************************************************/
|
||||
static ObjectNode request(String operation, int dpi, String elements, Map<String, Object> record)
|
||||
{
|
||||
String source = "{\"schema\":\"qqq-zpl-scene/2\",\"design\":{\"width\":\"4in\",\"height\":\"2in\"},\"elements\":[" + elements + "]}";
|
||||
ObjectNode request = SceneFixture.request(operation, SceneFixture.stock(WIDTH, HEIGHT, MARGIN, 203), source).put("targetDpi", dpi);
|
||||
request.set("context", Wire.JSON.valueToTree(Map.of("record", record)));
|
||||
return (request);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** The GS1 check digit of the digits before it, worked as the General
|
||||
** Specifications describe it: the digit nearest the check digit is
|
||||
** multiplied by 3, the next by 1, and so on, and the check digit takes the
|
||||
** sum up to a multiple of ten.
|
||||
***************************************************************************/
|
||||
static char checkDigit(String body)
|
||||
{
|
||||
int sum = 0;
|
||||
int weight = 3;
|
||||
for(int index = body.length() - 1; index >= 0; index--)
|
||||
{
|
||||
sum += Character.digit(body.charAt(index), 10) * weight;
|
||||
weight = 4 - weight;
|
||||
}
|
||||
return ((char) ('0' + (10 - sum % 10) % 10));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** The first column and row of the printable area on the page, in dots: the
|
||||
** margin rounded up.
|
||||
***************************************************************************/
|
||||
static int origin(int dpi)
|
||||
{
|
||||
return ((MARGIN * dpi + 25_399) / 25_400);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** The answer of the worker to a request that must succeed.
|
||||
***************************************************************************/
|
||||
static Map<String, Object> answer(JsonNode request) throws Exception
|
||||
{
|
||||
Map<String, Object> answer = RendererMain.process(request);
|
||||
assertEquals("OK", answer.get("outcome"));
|
||||
return (answer);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** The page a printer would draw from an answer.
|
||||
***************************************************************************/
|
||||
static boolean[][] page(Map<String, Object> answer)
|
||||
{
|
||||
return (SceneFixture.raster(Base64.getDecoder().decode((String) answer.get("base64"))));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** The one finding of a code in an answer.
|
||||
***************************************************************************/
|
||||
@SuppressWarnings("unchecked")
|
||||
static Map<String, Object> finding(Map<String, Object> answer, String code)
|
||||
{
|
||||
List<Map<String, Object>> found = ((List<Map<String, Object>>) answer.get("diagnostics")).stream().filter(finding -> code.equals(finding.get("code"))).toList();
|
||||
assertEquals(1, found.size(), code);
|
||||
return (found.getFirst());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** A named number of a finding, from `measured` or `limit`.
|
||||
***************************************************************************/
|
||||
@SuppressWarnings("unchecked")
|
||||
static int number(Map<String, Object> finding, String part, String name)
|
||||
{
|
||||
return (((Map<String, Integer>) finding.get(part)).get(name));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** The box of a finding, moved from the printable area onto the page.
|
||||
***************************************************************************/
|
||||
@SuppressWarnings("unchecked")
|
||||
static Frame box(Map<String, Object> finding, int dpi)
|
||||
{
|
||||
List<Integer> box = (List<Integer>) finding.get("box");
|
||||
return (new Frame(box.get(0) + origin(dpi), box.get(1) + origin(dpi), box.get(2), box.get(3)));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Where the ink of a page is.
|
||||
***************************************************************************/
|
||||
static Ink ink(boolean[][] page)
|
||||
{
|
||||
int left = Integer.MAX_VALUE;
|
||||
int top = Integer.MAX_VALUE;
|
||||
int right = -1;
|
||||
int bottom = -1;
|
||||
for(int y = 0; y < page.length; y++)
|
||||
{
|
||||
for(int x = 0; x < page[y].length; x++)
|
||||
{
|
||||
if(page[y][x])
|
||||
{
|
||||
left = Math.min(left, x);
|
||||
top = Math.min(top, y);
|
||||
right = Math.max(right, x);
|
||||
bottom = Math.max(bottom, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
return (new Ink(left, top, right, bottom));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** The lengths of the runs of ink and of blank between the first and last
|
||||
** ink of one row, in order, starting with ink.
|
||||
***************************************************************************/
|
||||
static List<Integer> runs(boolean[] row)
|
||||
{
|
||||
List<Integer> runs = new ArrayList<>();
|
||||
int first = -1;
|
||||
int last = -1;
|
||||
for(int x = 0; x < row.length; x++)
|
||||
{
|
||||
if(row[x])
|
||||
{
|
||||
first = first < 0 ? x : first;
|
||||
last = x;
|
||||
}
|
||||
}
|
||||
int start = first;
|
||||
for(int x = first + 1; first >= 0 && x <= last + 1; x++)
|
||||
{
|
||||
if(x > last || row[x] != row[x - 1])
|
||||
{
|
||||
runs.add(x - start);
|
||||
start = x;
|
||||
}
|
||||
}
|
||||
return (runs);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** One column of a page as a row, for a symbol turned a quarter.
|
||||
***************************************************************************/
|
||||
static boolean[] column(boolean[][] page, int x)
|
||||
{
|
||||
boolean[] column = new boolean[page.length];
|
||||
for(int y = 0; y < page.length; y++)
|
||||
{
|
||||
column[y] = page[y][x];
|
||||
}
|
||||
return (column);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Read the one symbol on a page with ZXing. A GS1 symbol is read as one, so
|
||||
** its FNC1 comes back as the `]C1` identifier and as separators. (ZXing
|
||||
** also reads a Code 128 check character whose value is that of FNC1 as one
|
||||
** more separator at the end; a GS1 value chosen for a test must not have
|
||||
** that check character. `[10]LOT[21]X` is one that does.)
|
||||
***************************************************************************/
|
||||
static Result decode(boolean[][] page, BarcodeFormat format, boolean gs1) throws Exception
|
||||
{
|
||||
BufferedImage picture = new BufferedImage(page[0].length, page.length, BufferedImage.TYPE_BYTE_GRAY);
|
||||
for(int y = 0; y < page.length; y++)
|
||||
{
|
||||
for(int x = 0; x < page[y].length; x++)
|
||||
{
|
||||
picture.setRGB(x, y, page[y][x] ? 0x000000 : 0xFFFFFF);
|
||||
}
|
||||
}
|
||||
Map<DecodeHintType, Object> hints = new EnumMap<>(DecodeHintType.class);
|
||||
hints.put(DecodeHintType.POSSIBLE_FORMATS, List.of(format));
|
||||
hints.put(DecodeHintType.TRY_HARDER, true);
|
||||
if(gs1)
|
||||
{
|
||||
hints.put(DecodeHintType.ASSUME_GS1, true);
|
||||
}
|
||||
if(format == BarcodeFormat.QR_CODE || format == BarcodeFormat.DATA_MATRIX)
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
// read the modules straight off the grid: the page is a rendering, with no skew or //
|
||||
// noise, and this reads a matrix of two dots to the module that the detector for //
|
||||
// photographs does not find //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
hints.put(DecodeHintType.PURE_BARCODE, true);
|
||||
}
|
||||
return (new MultiFormatReader().decode(new BinaryBitmap(new HybridBinarizer(new BufferedImageLuminanceSource(picture))), hints));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
/*
|
||||
* 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.scene2.barcode;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.SplittableRandom;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import com.google.zxing.BarcodeFormat;
|
||||
import com.google.zxing.NotFoundException;
|
||||
import com.kingsrook.qbits.renderer.RendererMain;
|
||||
import com.kingsrook.qbits.renderer.Wire;
|
||||
import com.kingsrook.qbits.renderer.scene2.Frame;
|
||||
import com.kingsrook.qbits.renderer.scene2.Refusal;
|
||||
import com.kingsrook.qbits.renderer.scene2.Scene2;
|
||||
import com.kingsrook.qbits.renderer.scene2.SceneFixture;
|
||||
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.assertTrue;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Properties of a fitted barcode, over seeded symbologies, values, boxes,
|
||||
** resolutions, profiles, alignments and rotations, each read from the label a
|
||||
** printer would be sent. No clock and a fixed seed, so a failure replays.
|
||||
*******************************************************************************/
|
||||
class BarcodePropertyTest
|
||||
{
|
||||
private static final String LETTERS = "TEST0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
private static final List<String> SYMBOLOGIES = List.of("code128", "code39", "itf", "gs1-128", "qr", "datamatrix");
|
||||
private static final Map<String, BarcodeFormat> FORMATS = Map.of("code128", BarcodeFormat.CODE_128, "code39", BarcodeFormat.CODE_39, "itf", BarcodeFormat.ITF, "gs1-128", BarcodeFormat.CODE_128,
|
||||
"qr", BarcodeFormat.QR_CODE, "datamatrix", BarcodeFormat.DATA_MATRIX);
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** A picture turned a quarter clockwise.
|
||||
***************************************************************************/
|
||||
private static boolean[][] turned(boolean[][] picture)
|
||||
{
|
||||
boolean[][] turned = new boolean[picture[0].length][picture.length];
|
||||
for(int y = 0; y < turned.length; y++)
|
||||
{
|
||||
for(int x = 0; x < turned[y].length; x++)
|
||||
{
|
||||
turned[y][x] = picture[picture.length - 1 - x][y];
|
||||
}
|
||||
}
|
||||
return (turned);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Property: a barcode is either drawn or refused as not fitting, and
|
||||
**
|
||||
** - drawn, its module is a whole number of dots, never under the minimum,
|
||||
** and under what its profile asks only when the scene states a smaller
|
||||
** minimum, which is then warned of; every bar and space is a whole
|
||||
** number of modules; the symbol and its quiet zones are inside the box;
|
||||
** the alignment puts what is left over where it says, in the middle when
|
||||
** it says nothing, and the middle leaves equal margins within one dot; a
|
||||
** linear symbol takes the whole of the box across its bars, which are as
|
||||
** tall as its symbology asks unless the scene states its own height,
|
||||
** which is then warned of; and the label decodes to the value;
|
||||
** - refused, the numbers say why: the dots needed at the minimum module are
|
||||
** more than the box has on the axis that is short.
|
||||
***************************************************************************/
|
||||
@Test
|
||||
void aBarcodeIsDrawnInWholeDotsInsideItsBoxOrRefusedWithItsNumbers() throws Exception
|
||||
{
|
||||
SplittableRandom random = new SplittableRandom(20261114L);
|
||||
int drawn = 0;
|
||||
int refused = 0;
|
||||
int below = 0;
|
||||
int low = 0;
|
||||
for(int iteration = 0; iteration < 320; iteration++)
|
||||
{
|
||||
String symbology = SYMBOLOGIES.get(random.nextInt(SYMBOLOGIES.size()));
|
||||
boolean matrix = "qr".equals(symbology) || "datamatrix".equals(symbology);
|
||||
int length = matrix ? random.nextInt(1, 61) : random.nextInt(3, 11) * 2;
|
||||
String text = LETTERS.repeat(2).substring(0, length);
|
||||
String value = switch(symbology)
|
||||
{
|
||||
case "itf" -> "0012345678".repeat(2).substring(0, length);
|
||||
case "gs1-128" -> "[01]00012345678905[21]" + text.substring(0, length / 2);
|
||||
default -> text;
|
||||
};
|
||||
String scanned = "gs1-128".equals(symbology) ? "]C1" + "0100012345678905" + "21" + text.substring(0, length / 2) : value;
|
||||
int dpi = Scene2.RESOLUTIONS.get(random.nextInt(3));
|
||||
int rotation = random.nextInt(4) * 90;
|
||||
boolean upright = rotation % 180 == 0;
|
||||
boolean stated = random.nextInt(4) == 0;
|
||||
boolean shorter = random.nextInt(4) == 0;
|
||||
String profile = List.of("", "none", "gs1-general", "gs1-logistics").get(random.nextInt(4));
|
||||
String align = List.of("", "left", "center", "right").get(random.nextInt(4));
|
||||
String valign = List.of("", "top", "middle", "bottom").get(random.nextInt(4));
|
||||
int along = random.nextInt(12_000, upright ? 94_000 : 43_000);
|
||||
int across = random.nextInt(4000, shorter ? 12_000 : upright ? 43_000 : 94_000);
|
||||
ObjectNode element = Wire.JSON.createObjectNode().put("id", "code").put("type", "barcode").put("symbology", symbology).put("value", value).put("anchor", "center");
|
||||
element.put("width", (upright ? along : across) / 1000.0 + "mm").put("height", (upright ? across : along) / 1000.0 + "mm").put("rotation", rotation);
|
||||
if(stated)
|
||||
{
|
||||
element.put("minModule", "1dot");
|
||||
}
|
||||
if(shorter)
|
||||
{
|
||||
element.put("minHeight", "3mm");
|
||||
}
|
||||
if(!profile.isEmpty())
|
||||
{
|
||||
element.put("barcodeProfile", profile);
|
||||
}
|
||||
if(!align.isEmpty())
|
||||
{
|
||||
element.put("align", align);
|
||||
}
|
||||
if(!valign.isEmpty())
|
||||
{
|
||||
element.put("valign", valign);
|
||||
}
|
||||
String about = "iteration " + iteration + ": " + element + " at " + dpi + " dpi";
|
||||
Symbology rules = Symbology.of(symbology);
|
||||
int recommended = rules.narrowest(Symbology.Profile.of(profile), dpi);
|
||||
int quiet = matrix ? 4 : 10;
|
||||
|
||||
Map<String, Object> answer;
|
||||
try
|
||||
{
|
||||
answer = BarcodeFixture.answer(BarcodeFixture.request("RENDER", dpi, element.toString(), Map.of()));
|
||||
}
|
||||
catch(Refusal refusal)
|
||||
{
|
||||
refused++;
|
||||
Map<String, Object> wire = refusal.diagnostic().wire();
|
||||
assertEquals("BARCODE_DOES_NOT_FIT", wire.get("code"), about);
|
||||
int minimum = BarcodeFixture.number(wire, "limit", "minModuleDots");
|
||||
assertEquals(stated ? 1 : recommended, minimum, about);
|
||||
assertTrue(BarcodeFixture.number(wire, "measured", "moduleDots") < minimum, about);
|
||||
assertEquals(BarcodeFixture.number(wire, "measured", "modules") * minimum, BarcodeFixture.number(wire, "measured", "widthDots"), about);
|
||||
assertTrue(BarcodeFixture.number(wire, "measured", "widthDots") > BarcodeFixture.number(wire, "limit", "widthDots")
|
||||
|| BarcodeFixture.number(wire, "measured", "heightDots") > BarcodeFixture.number(wire, "limit", "heightDots"), about + ": " + wire);
|
||||
assertEquals(wire, SceneFixture.refusal(() -> RendererMain.process(BarcodeFixture.request("VALIDATE", dpi, element.toString(), Map.of()))), about);
|
||||
continue;
|
||||
}
|
||||
drawn++;
|
||||
Map<String, Object> chosen = BarcodeFixture.finding(answer, "BARCODE_MODULE");
|
||||
int module = BarcodeFixture.number(chosen, "measured", "moduleDots");
|
||||
assertTrue(module >= (stated ? 1 : recommended), about + ": module " + module);
|
||||
assertEquals(stated, ((List<?>) answer.get("diagnostics")).stream().anyMatch(finding -> "BARCODE_MODULE_BELOW_RECOMMENDED".equals(((Map<?, ?>) finding).get("code"))), about);
|
||||
below += module < recommended ? 1 : 0;
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////
|
||||
// the symbol with its quiet zones, as the label shows it: the ink, widened by the //
|
||||
// quiet zones on the axes the module was fitted on //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////
|
||||
boolean[][] page = BarcodeFixture.page(answer);
|
||||
Frame box = BarcodeFixture.box(chosen, dpi);
|
||||
BarcodeFixture.Ink ink = BarcodeFixture.ink(page);
|
||||
int marginX = matrix || upright ? quiet * module : 0;
|
||||
int marginY = matrix || !upright ? quiet * module : 0;
|
||||
Frame area = new Frame(ink.left() - marginX, ink.top() - marginY, ink.width() + 2 * marginX, ink.height() + 2 * marginY);
|
||||
assertTrue(box.contains(area), about + ": " + area + " leaves " + box);
|
||||
int symbolWidth = BarcodeFixture.number(chosen, "measured", "widthDots");
|
||||
int symbolHeight = BarcodeFixture.number(chosen, "measured", "heightDots");
|
||||
assertEquals(List.of(upright ? symbolWidth : symbolHeight, upright ? symbolHeight : symbolWidth), List.of(area.width(), area.height()), about);
|
||||
assertEquals(BarcodeFixture.number(chosen, "measured", "modules") * module, symbolWidth, about);
|
||||
int spareX = box.width() - area.width();
|
||||
int spareY = box.height() - area.height();
|
||||
assertEquals(Map.of("left", 0, "", spareX / 2, "center", spareX / 2, "right", spareX).get(align), area.x() - box.x(), about);
|
||||
assertEquals(Map.of("top", 0, "", spareY / 2, "middle", spareY / 2, "bottom", spareY).get(valign), area.y() - box.y(), about);
|
||||
assertTrue(!List.of("", "center").contains(align) || Math.abs((area.x() - box.x()) - (box.right() - area.right())) <= 1, about);
|
||||
assertTrue(!List.of("", "middle").contains(valign) || Math.abs((area.y() - box.y()) - (box.bottom() - area.bottom())) <= 1, about);
|
||||
assertFalse(!matrix && (upright ? spareY : spareX) != 0, about + ": the bars do not take the whole box");
|
||||
if(!matrix)
|
||||
{
|
||||
int asked = rules.height(BarcodeFixture.number(chosen, "measured", "modules") - 2 * quiet, dpi).at(module);
|
||||
boolean warned = ((List<?>) answer.get("diagnostics")).stream().anyMatch(finding -> "BARCODE_HEIGHT_BELOW_RECOMMENDED".equals(((Map<?, ?>) finding).get("code")));
|
||||
assertEquals(symbolHeight < asked, warned, about + ": bars of " + symbolHeight + " dots where " + asked + " are asked");
|
||||
assertTrue(shorter || !warned, about);
|
||||
low += warned ? 1 : 0;
|
||||
}
|
||||
for(int y = ink.top(); y <= ink.bottom(); y++)
|
||||
{
|
||||
for(int run : BarcodeFixture.runs(page[y]))
|
||||
{
|
||||
assertEquals(0, (matrix || upright ? run : 0) % module, about + ": a run of " + run + " dots in row " + y);
|
||||
}
|
||||
}
|
||||
for(int x = ink.left(); x <= ink.right(); x++)
|
||||
{
|
||||
for(int run : BarcodeFixture.runs(BarcodeFixture.column(page, x)))
|
||||
{
|
||||
assertEquals(0, (matrix || !upright ? run : 0) % module, about + ": a run of " + run + " dots in column " + x);
|
||||
}
|
||||
}
|
||||
|
||||
boolean[][] symbol = new boolean[area.height()][area.width()];
|
||||
for(int y = 0; y < symbol.length; y++)
|
||||
{
|
||||
System.arraycopy(page[area.y() + y], area.x(), symbol[y], 0, area.width());
|
||||
}
|
||||
for(int turn = rotation; turn % 360 != 0; turn += 90)
|
||||
{
|
||||
symbol = turned(symbol);
|
||||
}
|
||||
try
|
||||
{
|
||||
assertEquals(scanned, BarcodeFixture.decode(symbol, FORMATS.get(symbology), "gs1-128".equals(symbology)).getText(), about);
|
||||
}
|
||||
catch(NotFoundException unread)
|
||||
{
|
||||
throw new AssertionError(about + ": module " + module + ", the label does not decode", unread);
|
||||
}
|
||||
}
|
||||
assertTrue(drawn > 80 && refused > 40 && below > 5 && low > 2, drawn + " drawn, " + refused + " refused, " + below + " below the module asked, " + low + " with bars shorter than asked");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
/*
|
||||
* 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.scene2.barcode;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.SplittableRandom;
|
||||
import com.kingsrook.qbits.renderer.scene2.Length;
|
||||
import com.kingsrook.qbits.renderer.scene2.Scene2;
|
||||
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.assertTrue;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The module-fit rule as arithmetic: the largest whole-dot module that holds
|
||||
** the symbol and its quiet zones in the box, never under the minimum, and for
|
||||
** a linear symbol never one that makes it too wide for the height of its box.
|
||||
*******************************************************************************/
|
||||
class ModuleFitTest
|
||||
{
|
||||
private static final int MICROMETRES_PER_INCH = 25_400;
|
||||
private static final int NO_MAXIMUM = Integer.MAX_VALUE;
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** A linear symbol with no rule for the height of its bars.
|
||||
***************************************************************************/
|
||||
private static ModuleFit.Request linear(int columns, int along, int across, int minimum, int maximum)
|
||||
{
|
||||
return (new ModuleFit.Request(columns, 0, along, across, minimum, maximum, new ModuleFit.Height(columns, 0, 0, 1)));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** The worked example: a GS1-128 of 319 modules, quiet zones included, in a
|
||||
** box 719 dots wide (90 mm at 203 dpi) is drawn at 2 dots, 638 dots wide;
|
||||
** 3 dots would need 957. In a box of 637 dots it does not fit: the largest
|
||||
** module is 1 dot, under the minimum, and the answer states the 638 dots
|
||||
** the minimum needs.
|
||||
***************************************************************************/
|
||||
@Test
|
||||
void aLinearSymbolTakesTheLargestModuleThatFitsItsWidth()
|
||||
{
|
||||
assertEquals(new ModuleFit.Fit(2, 638, 87, true), ModuleFit.fit(linear(319, 719, 87, 2, NO_MAXIMUM)));
|
||||
assertEquals(new ModuleFit.Fit(2, 638, 87, true), ModuleFit.fit(linear(319, 638, 87, 2, NO_MAXIMUM)));
|
||||
assertEquals(new ModuleFit.Fit(1, 638, 0, false), ModuleFit.fit(linear(319, 637, 87, 2, NO_MAXIMUM)));
|
||||
assertEquals(new ModuleFit.Fit(0, 638, 0, false), ModuleFit.fit(linear(319, 318, 87, 2, NO_MAXIMUM)));
|
||||
assertEquals(new ModuleFit.Fit(1, 319, 87, true), ModuleFit.fit(linear(319, 637, 87, 1, NO_MAXIMUM)));
|
||||
assertEquals(new ModuleFit.Fit(7, 2233, 1, true), ModuleFit.fit(linear(319, 2400, 1, 6, NO_MAXIMUM)));
|
||||
assertEquals(new ModuleFit.Fit(3, 957, 87, true), ModuleFit.fit(linear(319, 2400, 87, 2, 3)));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** A two-dimensional symbol is held on both axes, so the shorter side of
|
||||
** the box decides; a stated maximum caps either kind.
|
||||
***************************************************************************/
|
||||
@Test
|
||||
void aMatrixSymbolIsHeldOnBothAxesAndAMaximumCaps()
|
||||
{
|
||||
assertEquals(new ModuleFit.Fit(5, 145, 145, true), ModuleFit.fit(new ModuleFit.Request(29, 29, 319, 147, 3, NO_MAXIMUM, null)));
|
||||
assertEquals(new ModuleFit.Fit(11, 319, 319, true), ModuleFit.fit(new ModuleFit.Request(29, 29, 319, 400, 3, NO_MAXIMUM, null)));
|
||||
assertEquals(new ModuleFit.Fit(4, 116, 116, true), ModuleFit.fit(new ModuleFit.Request(29, 29, 319, 400, 3, 4, null)));
|
||||
assertEquals(new ModuleFit.Fit(2, 87, 87, false), ModuleFit.fit(new ModuleFit.Request(29, 29, 319, 86, 3, NO_MAXIMUM, null)));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** The bars of a linear symbol must be as tall as their own width asks: here
|
||||
** the rule of Code 128, 15% of the width of the bars and never under 5 mm,
|
||||
** which is 40 dots at 203 dpi. A 44-character GS1-128 has 299 modules of
|
||||
** bars: at 2 dots they are 598 dots wide and must be 90 dots tall (89.7,
|
||||
** taken up); at 3 dots, 135. So a module the width would allow is not
|
||||
** taken when the box is too short for it, and when even the minimum module
|
||||
** needs more height than the box has, the answer states the height it
|
||||
** needs. A box under the 5 mm holds no module at all.
|
||||
***************************************************************************/
|
||||
@Test
|
||||
void theBarsOfALinearSymbolAreAsTallAsTheirWidthAsks()
|
||||
{
|
||||
ModuleFit.Height asked = new ModuleFit.Height(299, 40, 15, 100);
|
||||
assertEquals(List.of(40, 45, 90, 135, 180), List.of(asked.at(0), asked.at(1), asked.at(2), asked.at(3), asked.at(4)));
|
||||
assertEquals(new ModuleFit.Fit(2, 638, 95, true), ModuleFit.fit(new ModuleFit.Request(319, 0, 719, 95, 2, NO_MAXIMUM, asked)));
|
||||
assertEquals(new ModuleFit.Fit(2, 638, 95, true), ModuleFit.fit(new ModuleFit.Request(319, 0, 1000, 95, 2, NO_MAXIMUM, asked)));
|
||||
assertEquals(new ModuleFit.Fit(3, 957, 135, true), ModuleFit.fit(new ModuleFit.Request(319, 0, 1000, 135, 2, NO_MAXIMUM, asked)));
|
||||
assertEquals(new ModuleFit.Fit(2, 638, 134, true), ModuleFit.fit(new ModuleFit.Request(319, 0, 1000, 134, 2, NO_MAXIMUM, asked)));
|
||||
assertEquals(new ModuleFit.Fit(2, 638, 90, true), ModuleFit.fit(new ModuleFit.Request(319, 0, 719, 90, 2, NO_MAXIMUM, asked)));
|
||||
assertEquals(new ModuleFit.Fit(1, 638, 90, false), ModuleFit.fit(new ModuleFit.Request(319, 0, 719, 89, 2, NO_MAXIMUM, asked)));
|
||||
assertEquals(new ModuleFit.Fit(1, 319, 89, true), ModuleFit.fit(new ModuleFit.Request(319, 0, 719, 89, 1, NO_MAXIMUM, asked)));
|
||||
assertEquals(new ModuleFit.Fit(0, 638, 90, false), ModuleFit.fit(new ModuleFit.Request(319, 0, 719, 39, 2, NO_MAXIMUM, asked)));
|
||||
assertEquals(new ModuleFit.Fit(0, 319, 45, false), ModuleFit.fit(new ModuleFit.Request(319, 0, 719, 39, 1, NO_MAXIMUM, asked)));
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// a height the scene states itself is a length and nothing more: no share of the width //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
ModuleFit.Height stated = new ModuleFit.Height(299, 60, 0, 1);
|
||||
assertEquals(60, stated.at(7));
|
||||
assertEquals(new ModuleFit.Fit(3, 957, 60, true), ModuleFit.fit(new ModuleFit.Request(319, 0, 1000, 60, 2, NO_MAXIMUM, stated)));
|
||||
assertEquals(new ModuleFit.Fit(0, 638, 60, false), ModuleFit.fit(new ModuleFit.Request(319, 0, 1000, 59, 2, NO_MAXIMUM, stated)));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Property, over seeded symbols, boxes and limits: the module is whole
|
||||
** dots; when the symbol fits it is inside its box at a module between the
|
||||
** minimum and the maximum, its bars are as tall as that module asks, and
|
||||
** one dot more would not fit or would ask for taller bars; when it does
|
||||
** not, the dots it needs at the minimum are more than the box has, on the
|
||||
** axis that refused it.
|
||||
***************************************************************************/
|
||||
@Test
|
||||
void theFittedSymbolIsInsideItsBoxAtTheLargestModuleOrTheNumbersSayWhyNot()
|
||||
{
|
||||
SplittableRandom random = new SplittableRandom(20261112L);
|
||||
int fitted = 0;
|
||||
int refused = 0;
|
||||
int low = 0;
|
||||
for(int iteration = 0; iteration < 20_000; iteration++)
|
||||
{
|
||||
boolean matrix = random.nextBoolean();
|
||||
int bars = random.nextInt(21, 600);
|
||||
int columns = bars + random.nextInt(8, 40);
|
||||
int rows = matrix ? random.nextInt(9, 200) : 0;
|
||||
int along = random.nextInt(1, 4000);
|
||||
int across = random.nextInt(1, 2000);
|
||||
int minimum = random.nextInt(1, 10);
|
||||
int maximum = random.nextInt(4) == 0 ? minimum + random.nextInt(0, 6) : NO_MAXIMUM;
|
||||
ModuleFit.Height height = matrix ? null : new ModuleFit.Height(bars, random.nextInt(0, 80), random.nextInt(3) == 0 ? 0 : random.nextInt(1, 80), 100);
|
||||
ModuleFit.Request request = new ModuleFit.Request(columns, rows, along, across, minimum, maximum, height);
|
||||
ModuleFit.Fit fit = ModuleFit.fit(request);
|
||||
String about = request + " gave " + fit;
|
||||
if(fit.fits())
|
||||
{
|
||||
fitted++;
|
||||
assertTrue(fit.module() >= minimum && fit.module() <= maximum, about);
|
||||
assertEquals(columns * fit.module(), fit.width(), about);
|
||||
assertEquals(matrix ? rows * fit.module() : across, fit.height(), about);
|
||||
assertTrue(fit.width() <= along && fit.height() <= across, about);
|
||||
assertTrue(matrix || height.at(fit.module()) <= across, about);
|
||||
assertTrue(fit.module() == maximum || columns * (fit.module() + 1) > along || (matrix ? rows * (fit.module() + 1) > across : height.at(fit.module() + 1) > across), about);
|
||||
}
|
||||
else
|
||||
{
|
||||
refused++;
|
||||
assertTrue(fit.module() < minimum && fit.module() >= 0, about);
|
||||
assertEquals(columns * minimum, fit.width(), about);
|
||||
assertEquals(matrix ? rows * minimum : height.at(minimum), fit.height(), about);
|
||||
assertTrue(fit.width() > along || fit.height() > across, about);
|
||||
low += fit.width() <= along ? 1 : 0;
|
||||
}
|
||||
}
|
||||
assertTrue(fitted > 2000 && refused > 2000 && low > 500, fitted + " fitted, " + refused + " refused, " + low + " for their height alone");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Property, over seeded symbols and physical boxes at the design's minimum
|
||||
** modules: a symbol with room to spare is refused at no resolution. The
|
||||
** room is what rounding can cost: the minimum goes up to a whole dot, which
|
||||
** at most makes 0.25 mm into 0.254 mm (and 0.375 mm into 0.424 mm, five
|
||||
** dots at 300 dpi), and the box goes down to a whole dot, which at most
|
||||
** loses one dot of 203 dpi. Where a symbol with less room fits at one
|
||||
** resolution and not at another, the refusal states the dots it needs and
|
||||
** the dots the box has.
|
||||
***************************************************************************/
|
||||
@Test
|
||||
void aSymbolWithRoomIsRefusedAtNoResolutionAndARoundingRefusalStatesItsNumbers()
|
||||
{
|
||||
SplittableRandom random = new SplittableRandom(20261113L);
|
||||
int roomy = 0;
|
||||
int divided = 0;
|
||||
for(int iteration = 0; iteration < 20_000; iteration++)
|
||||
{
|
||||
boolean matrix = random.nextBoolean();
|
||||
int columns = random.nextInt(21, 500);
|
||||
Length minimum = new Length(Length.Unit.MM, matrix ? 375 : 250);
|
||||
int exact = columns * minimum.micrometres();
|
||||
Length box = new Length(Length.Unit.MM, exact + random.nextInt(-exact / 5, exact / 4));
|
||||
int dotAt203 = (MICROMETRES_PER_INCH + 202) / 203;
|
||||
boolean room = columns * (matrix ? 424 : 254) + dotAt203 <= box.micrometres();
|
||||
List<ModuleFit.Fit> fits = new ArrayList<>();
|
||||
for(int dpi : Scene2.RESOLUTIONS)
|
||||
{
|
||||
int along = box.size(dpi, 0);
|
||||
ModuleFit.Fit fit = ModuleFit.fit(matrix ? new ModuleFit.Request(columns, columns, along, along, minimum.module(dpi), NO_MAXIMUM, null) : linear(columns, along, along, minimum.module(dpi), NO_MAXIMUM));
|
||||
fits.add(fit);
|
||||
String about = columns + " modules in " + box.micrometres() + " micrometres at " + dpi + " dpi gave " + fit;
|
||||
assertFalse(room && !fit.fits(), about);
|
||||
assertTrue(fit.fits() || (fit.width() == columns * minimum.module(dpi) && fit.width() > along), about);
|
||||
assertTrue(!fit.fits() || (long) fit.module() * MICROMETRES_PER_INCH >= (long) minimum.micrometres() * dpi, about + ": a module under the physical minimum");
|
||||
}
|
||||
roomy += room ? 1 : 0;
|
||||
divided += fits.stream().map(ModuleFit.Fit::fits).distinct().count() > 1 ? 1 : 0;
|
||||
}
|
||||
assertTrue(roomy > 2000 && divided > 100, roomy + " with room, " + divided + " refused at one resolution only");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,313 @@
|
||||
/*
|
||||
* 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.scene2.barcode;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.SplittableRandom;
|
||||
import com.kingsrook.qbits.renderer.BarcodeFit;
|
||||
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.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The rules that keep a symbol from reading as something other than its
|
||||
** value: no escape of the encoder in any value, no odd Interleaved 2 of 5, an
|
||||
** EAN or UPC number whole and with its own check digit, and the check digit
|
||||
** of every GS1 key that has one. Values are obviously fake, and a refusal is
|
||||
** numbers only.
|
||||
*******************************************************************************/
|
||||
class PayloadTest
|
||||
{
|
||||
private static final String BACKSLASH = String.valueOf((char) 92);
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** The GS1 check digit of some digits, worked the way the General
|
||||
** Specifications describe it and not the way `Payload` does: the digit
|
||||
** nearest the check digit is multiplied by 3, the next by 1, and so on,
|
||||
** and the check digit takes the sum up to a multiple of ten.
|
||||
***************************************************************************/
|
||||
private static char checkDigit(String body)
|
||||
{
|
||||
int sum = 0;
|
||||
int weight = 3;
|
||||
for(int index = body.length() - 1; index >= 0; index--)
|
||||
{
|
||||
sum += Character.digit(body.charAt(index), 10) * weight;
|
||||
weight = 4 - weight;
|
||||
}
|
||||
return ((char) ('0' + (10 - sum % 10) % 10));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Seeded digits.
|
||||
***************************************************************************/
|
||||
private static String digits(SplittableRandom random, int length)
|
||||
{
|
||||
StringBuilder digits = new StringBuilder();
|
||||
for(int index = 0; index < length; index++)
|
||||
{
|
||||
digits.append((char) ('0' + random.nextInt(10)));
|
||||
}
|
||||
return (digits.toString());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** A number of a length with any check digit but its own.
|
||||
***************************************************************************/
|
||||
private static String mistyped(String body)
|
||||
{
|
||||
return (body + (char) ('0' + (checkDigit(body) - '0' + 3) % 10));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** The check digit is the one rule of every GS1 key. Published numbers
|
||||
** first (the GTIN-13 and UPC-A of the usual textbook examples, an EAN-8,
|
||||
** the GTIN-14 these tests use), then every length a key has, against the
|
||||
** rule worked the other way round; one digit changed, or anything that is
|
||||
** not a digit, does not pass.
|
||||
***************************************************************************/
|
||||
@Test
|
||||
void aCheckDigitIsVerifiedByTheGs1Rule()
|
||||
{
|
||||
for(String published : List.of("4006381333931", "036000291452", "73513537", "00012345678905", "0012345678905", "012345678905", "12345670"))
|
||||
{
|
||||
assertTrue(Payload.checked(published), published);
|
||||
assertEquals(published.charAt(published.length() - 1), checkDigit(published.substring(0, published.length() - 1)), published);
|
||||
}
|
||||
SplittableRandom random = new SplittableRandom(20261118L);
|
||||
for(int iteration = 0; iteration < 2000; iteration++)
|
||||
{
|
||||
String body = digits(random, List.of(7, 11, 12, 13, 16, 17).get(random.nextInt(6)));
|
||||
String whole = body + checkDigit(body);
|
||||
assertTrue(Payload.checked(whole), whole);
|
||||
for(int other = 1; other < 10; other++)
|
||||
{
|
||||
assertFalse(Payload.checked(body + (char) ('0' + (whole.charAt(body.length()) - '0' + other) % 10)), whole + " with another check digit");
|
||||
}
|
||||
int changed = random.nextInt(body.length());
|
||||
assertFalse(Payload.checked(whole.substring(0, changed) + (char) ('0' + (whole.charAt(changed) - '0' + 1 + random.nextInt(9)) % 10) + whole.substring(changed + 1)), whole + " with digit " + changed + " changed");
|
||||
}
|
||||
assertFalse(Payload.checked("001234567890S"));
|
||||
assertFalse(Payload.checked("00123 5678905"));
|
||||
assertFalse(Payload.checked("0012345678905".replace('0', (char) 0xFF10)));
|
||||
assertFalse(Payload.checked("-012345678905"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** A UPC-E number stands for a UPC-A number, whose check digit it carries.
|
||||
** The last of its six digits says where the zeros were taken out.
|
||||
***************************************************************************/
|
||||
@Test
|
||||
void aUpcENumberIsCheckedAsTheNumberItStandsFor()
|
||||
{
|
||||
assertEquals("04210000526", Payload.expanded("0425261"));
|
||||
assertEquals("01200000345", Payload.expanded("0123450"));
|
||||
assertEquals("01220000345", Payload.expanded("0123452"));
|
||||
assertEquals("01230000045", Payload.expanded("0123453"));
|
||||
assertEquals("01234000005", Payload.expanded("0123454"));
|
||||
assertEquals("01234500005", Payload.expanded("0123455"));
|
||||
assertEquals("11234500009", Payload.expanded("1123459"));
|
||||
assertEquals('4', checkDigit("04210000526"));
|
||||
assertEquals(new Payload.Checked("0425261", null, null), Payload.of(Symbology.UPCE, "04252614"));
|
||||
assertEquals(new Payload.Checked(null, Map.of("characters", 8, "checkDigitMatches", 0), Map.of("characters", 8, "checkDigitMatches", 1)), Payload.of(Symbology.UPCE, "04252615"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** The encoder reads `\<FNC1>` to `\<FNC4>` in a value as function
|
||||
** characters: a record could so make a GS1-128 of a plain Code 128, set
|
||||
** the reader-programming flag or change a character, and where a symbology
|
||||
** has no such character the escape is dropped. No value holding the two
|
||||
** characters an escape begins with is drawn, in any symbology, wherever
|
||||
** they stand; the refusal is the count of characters. A backslash, or a
|
||||
** less-than sign, alone is a character like any other.
|
||||
***************************************************************************/
|
||||
@Test
|
||||
void noValueHoldsAnEscapeOfTheEncoder()
|
||||
{
|
||||
assertEquals(BACKSLASH + "<", Payload.ESCAPE);
|
||||
for(String id : BarcodeFit.TYPES)
|
||||
{
|
||||
Symbology symbology = Symbology.of(id);
|
||||
for(String function : List.of("FNC1>", "FNC2>", "FNC3>", "FNC4>", "", "fnc1>", "FNC5>", "<"))
|
||||
{
|
||||
for(String value : List.of(BACKSLASH + "<" + function + "0112345678901231", "TEST" + BACKSLASH + "<" + function + "A", "0012" + BACKSLASH + "<" + function, "[01]00012345678905[21]" + BACKSLASH + "<" + function))
|
||||
{
|
||||
assertTrue(Payload.escapes(value), id);
|
||||
assertEquals(new Payload.Checked(null, Map.of("characters", value.length()), null), Payload.of(symbology, value), id + " with " + function);
|
||||
}
|
||||
}
|
||||
}
|
||||
for(String plain : List.of(BACKSLASH, "<", BACKSLASH + " <", "<" + BACKSLASH, "TEST" + BACKSLASH + "FNC1>", "<FNC1>", BACKSLASH + BACKSLASH + "TEST"))
|
||||
{
|
||||
assertFalse(Payload.escapes(plain), plain);
|
||||
assertEquals(new Payload.Checked(plain, null, null), Payload.of(Symbology.CODE128, plain), plain);
|
||||
assertEquals(new Payload.Checked(plain, null, null), Payload.of(Symbology.QR, plain), plain);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Interleaved 2 of 5 draws digits in pairs, and the encoder puts a zero in
|
||||
** front of an odd number of them, which a scanner then reads. An odd
|
||||
** number of digits is refused by its count; an even number goes to the
|
||||
** encoder as it is.
|
||||
***************************************************************************/
|
||||
@Test
|
||||
void interleavedTwoOfFiveOfAnOddLengthIsRefused()
|
||||
{
|
||||
for(int length = 1; length <= 41; length += 2)
|
||||
{
|
||||
String odd = "0012345678".repeat(5).substring(0, length);
|
||||
assertEquals(new Payload.Checked(null, Map.of("characters", length), Map.of("charactersMultipleOf", 2)), Payload.of(Symbology.ITF, odd), odd);
|
||||
assertEquals(new Payload.Checked(odd + "0", null, null), Payload.of(Symbology.ITF, odd + "0"), odd);
|
||||
assertEquals(new Payload.Checked(odd, null, null), Payload.of(Symbology.CODE128, odd), odd);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** An EAN or UPC value is the whole number, check digit included: 13 digits
|
||||
** for `ean13`, 8 for `ean8`, 12 for `upca`, 8 for `upce`. The check digit
|
||||
** is verified and the encoder is given the digits before it. A number
|
||||
** without its check digit, or of any other length, is refused with the
|
||||
** length it must have, and never padded; a wrong check digit, and anything
|
||||
** that is not a digit, is refused as a check digit that does not match.
|
||||
***************************************************************************/
|
||||
@Test
|
||||
void aRetailNumberIsWholeAndCarriesItsOwnCheckDigit()
|
||||
{
|
||||
Map<Symbology, String> numbers = Map.of(Symbology.EAN13, "0012345678905", Symbology.EAN8, "12345670", Symbology.UPCA, "012345678905", Symbology.UPCE, "04252614");
|
||||
for(Map.Entry<Symbology, String> number : numbers.entrySet())
|
||||
{
|
||||
String whole = number.getValue();
|
||||
String body = whole.substring(0, whole.length() - 1);
|
||||
String about = number.getKey().id();
|
||||
assertEquals(new Payload.Checked(body, null, null), Payload.of(number.getKey(), whole), about);
|
||||
for(String other : List.of(body, body.substring(1), "0" + whole, whole + "0", "1", whole + "+12", "0".repeat(40)))
|
||||
{
|
||||
assertEquals(new Payload.Checked(null, Map.of("characters", other.length()), Map.of("characters", whole.length())), Payload.of(number.getKey(), other), about + " of " + other.length());
|
||||
}
|
||||
Payload.Checked mismatch = new Payload.Checked(null, Map.of("characters", whole.length(), "checkDigitMatches", 0), Map.of("characters", whole.length(), "checkDigitMatches", 1));
|
||||
for(char digit = '0'; digit <= '9'; digit++)
|
||||
{
|
||||
assertEquals(digit == whole.charAt(body.length()) ? new Payload.Checked(body, null, null) : mismatch, Payload.of(number.getKey(), body + digit), about + " ending " + digit);
|
||||
}
|
||||
assertEquals(mismatch, Payload.of(number.getKey(), "A" + whole.substring(1)), about);
|
||||
assertEquals(mismatch, Payload.of(number.getKey(), body + "+"), about);
|
||||
assertEquals(mismatch, Payload.of(number.getKey(), " ".repeat(whole.length())), about);
|
||||
}
|
||||
SplittableRandom random = new SplittableRandom(20261119L);
|
||||
for(int iteration = 0; iteration < 500; iteration++)
|
||||
{
|
||||
String thirteen = digits(random, 12);
|
||||
assertEquals(thirteen, Payload.of(Symbology.EAN13, thirteen + checkDigit(thirteen)).encoded());
|
||||
assertNull(Payload.of(Symbology.EAN13, mistyped(thirteen)).encoded());
|
||||
String twelve = digits(random, 11);
|
||||
assertEquals(twelve, Payload.of(Symbology.UPCA, twelve + checkDigit(twelve)).encoded());
|
||||
assertNull(Payload.of(Symbology.UPCA, mistyped(twelve)).encoded());
|
||||
String eight = digits(random, 7);
|
||||
assertEquals(eight, Payload.of(Symbology.EAN8, eight + checkDigit(eight)).encoded());
|
||||
assertNull(Payload.of(Symbology.EAN8, mistyped(eight)).encoded());
|
||||
assertEquals(eight, Payload.of(Symbology.UPCE, eight + checkDigit(Payload.expanded(eight))).encoded());
|
||||
assertNull(Payload.of(Symbology.UPCE, mistyped(Payload.expanded(eight)).substring(11).transform(check -> eight + check)).encoded());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** The encoder checks the lengths of a GS1 element string, not its check
|
||||
** digits, so a GTIN with a wrong check digit would print as a label that
|
||||
** scans as another item. Every application identifier whose data begins
|
||||
** with a key that ends in a check digit is verified: SSCC, GTIN, GDTI,
|
||||
** GCN, GSIN, the GLNs, GRAI after its leading zero, ITIP and GSRN. The
|
||||
** refusal names the identifier, from that fixed list, and nothing of the
|
||||
** value. Data of an identifier outside the list is left to the encoder.
|
||||
***************************************************************************/
|
||||
@Test
|
||||
void theCheckDigitOfEveryGs1KeyIsVerified()
|
||||
{
|
||||
String typical = "[01]00012345678905[11]260101[21]TEST00010002";
|
||||
assertEquals(new Payload.Checked(typical, null, null), Payload.of(Symbology.GS1_128, typical));
|
||||
assertEquals(new Payload.Checked(null, Map.of("characters", 44, "applicationIdentifier", 1, "checkDigitMatches", 0), Map.of("checkDigitMatches", 1)),
|
||||
Payload.of(Symbology.GS1_128, typical.replace("678905", "678906")));
|
||||
assertEquals(new Payload.Checked(null, Map.of("characters", 44, "applicationIdentifier", 1, "checkDigitMatches", 0), Map.of("checkDigitMatches", 1)),
|
||||
Payload.of(Symbology.GS1_128, "[11]260101[21]TEST00010002[01]00012345678995"));
|
||||
|
||||
Map<String, List<Integer>> keys = Map.ofEntries(Map.entry("00", List.of(0, 18)), Map.entry("01", List.of(0, 14)), Map.entry("02", List.of(0, 14)), Map.entry("03", List.of(0, 14)),
|
||||
Map.entry("253", List.of(0, 13)), Map.entry("255", List.of(0, 13)), Map.entry("402", List.of(0, 17)), Map.entry("410", List.of(0, 13)), Map.entry("411", List.of(0, 13)),
|
||||
Map.entry("412", List.of(0, 13)), Map.entry("413", List.of(0, 13)), Map.entry("414", List.of(0, 13)), Map.entry("415", List.of(0, 13)), Map.entry("416", List.of(0, 13)),
|
||||
Map.entry("417", List.of(0, 13)), Map.entry("8003", List.of(1, 13)), Map.entry("8006", List.of(0, 14)), Map.entry("8026", List.of(0, 14)), Map.entry("8017", List.of(0, 18)),
|
||||
Map.entry("8018", List.of(0, 18)));
|
||||
SplittableRandom random = new SplittableRandom(20261120L);
|
||||
for(Map.Entry<String, List<Integer>> key : keys.entrySet())
|
||||
{
|
||||
for(int iteration = 0; iteration < 50; iteration++)
|
||||
{
|
||||
String body = digits(random, key.getValue().get(1) - 1);
|
||||
String before = "0".repeat(key.getValue().get(0));
|
||||
String after = List.of("", "0101", "TEST01").get(random.nextInt(3));
|
||||
String good = "[" + key.getKey() + "]" + before + body + checkDigit(body) + after;
|
||||
String bad = "[" + key.getKey() + "]" + before + mistyped(body) + after;
|
||||
String about = "(" + key.getKey() + ")";
|
||||
assertEquals(good, Payload.of(Symbology.GS1_128, good).encoded(), about);
|
||||
assertEquals(good + "[10]TEST", Payload.of(Symbology.GS1_128, good + "[10]TEST").encoded(), about);
|
||||
Payload.Checked refused = new Payload.Checked(null, Map.of("characters", bad.length(), "applicationIdentifier", Integer.parseInt(key.getKey()), "checkDigitMatches", 0), Map.of("checkDigitMatches", 1));
|
||||
assertEquals(refused, Payload.of(Symbology.GS1_128, bad), about);
|
||||
assertNull(Payload.of(Symbology.GS1_128, "[10]TEST" + bad).encoded(), about);
|
||||
assertNull(Payload.of(Symbology.GS1_128, good + bad).encoded(), about);
|
||||
}
|
||||
assertNull(Payload.of(Symbology.GS1_128, "[" + key.getKey() + "]123").encoded(), key.getKey());
|
||||
assertNull(Payload.of(Symbology.GS1_128, "[" + key.getKey() + "]").encoded(), key.getKey());
|
||||
assertNull(Payload.of(Symbology.GS1_128, "[" + key.getKey() + "]" + "A".repeat(18)).encoded(), key.getKey());
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// identifiers with no such key, and strings the encoder will refuse for their shape, //
|
||||
// pass here as they are; and the rule is for element strings only //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
for(String other : List.of("[21]00012345678906", "[10]1", "[8013]TEST", "[401]00012345678906", "[01", "[", "(01)00012345678906", "[1]00012345678906", "[001]00012345678906"))
|
||||
{
|
||||
assertEquals(new Payload.Checked(other, null, null), Payload.of(Symbology.GS1_128, other), other);
|
||||
}
|
||||
assertEquals("[01]00012345678906", Payload.of(Symbology.CODE128, "[01]00012345678906").encoded());
|
||||
assertEquals("[01]00012345678906", Payload.of(Symbology.QR, "[01]00012345678906").encoded());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,291 @@
|
||||
/*
|
||||
* 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.scene2.barcode;
|
||||
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.SplittableRandom;
|
||||
import com.kingsrook.qbits.renderer.BarcodeFit;
|
||||
import com.kingsrook.qbits.renderer.Barcodes;
|
||||
import com.kingsrook.qbits.renderer.Wire;
|
||||
import com.kingsrook.qbits.renderer.scene2.Scene2;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The table of the symbologies: its numbers written out a second time here,
|
||||
** so that a change to one is a change to both, and what it says of the
|
||||
** encoder held to the encoder.
|
||||
*******************************************************************************/
|
||||
class SymbologyTest
|
||||
{
|
||||
private static final String ACCENTED = Character.toString(0xE9);
|
||||
private static final List<String> LINEAR = List.of("code128", "code39", "code93", "itf", "gs1-128", "ean13", "ean8", "upca", "upce");
|
||||
private static final List<String> RETAIL = List.of("ean13", "ean8", "upca", "upce");
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** The modules of a symbol as the encoder of version 1 draws it, without
|
||||
** its quiet zones: across, and down.
|
||||
***************************************************************************/
|
||||
private static List<Integer> modules(String symbology, String value)
|
||||
{
|
||||
int margin = LINEAR.contains(symbology) ? 10 : 4;
|
||||
BufferedImage image = Barcodes.renderForFormat("ZPL", symbology, value, 1, margin, Wire.JSON.createObjectNode());
|
||||
return (List.of(image.getWidth() - 2 * margin, image.getHeight() - 2 * margin));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** The narrowest bar of a symbology under a profile, in dots at 203, 300 and
|
||||
** 600 dpi.
|
||||
***************************************************************************/
|
||||
private static List<Integer> narrowest(String symbology, Symbology.Profile profile)
|
||||
{
|
||||
return (Scene2.RESOLUTIONS.stream().map(dpi -> Symbology.of(symbology).narrowest(profile, dpi)).toList());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Seeded text of a length from an alphabet.
|
||||
***************************************************************************/
|
||||
private static String text(SplittableRandom random, String alphabet, int length)
|
||||
{
|
||||
StringBuilder text = new StringBuilder();
|
||||
for(int index = 0; index < length; index++)
|
||||
{
|
||||
text.append(alphabet.charAt(random.nextInt(alphabet.length())));
|
||||
}
|
||||
return (text.toString());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Every character from space to tilde.
|
||||
***************************************************************************/
|
||||
private static String printable()
|
||||
{
|
||||
StringBuilder printable = new StringBuilder();
|
||||
for(char character = ' '; character <= '~'; character++)
|
||||
{
|
||||
printable.append(character);
|
||||
}
|
||||
return (printable.toString());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** The table names exactly the fit-qualified symbologies of version 1. Quiet
|
||||
** zones are each symbology's own: 10 modules on each side of Code 128, Code
|
||||
** 39, Code 93 and Interleaved 2 of 5; 11 and 7 for EAN-13, 7 and 7 for
|
||||
** EAN-8, 9 and 9 for UPC-A, 9 and 7 for UPC-E; 4 on every side of a
|
||||
** two-dimensional symbol.
|
||||
***************************************************************************/
|
||||
@Test
|
||||
void theQuietZonesAreThoseOfEachSymbology()
|
||||
{
|
||||
Map<String, List<Integer>> quiet = Map.ofEntries(Map.entry("code128", List.of(10, 10)), Map.entry("code39", List.of(10, 10)), Map.entry("code93", List.of(10, 10)), Map.entry("itf", List.of(10, 10)),
|
||||
Map.entry("gs1-128", List.of(10, 10)), Map.entry("ean13", List.of(11, 7)), Map.entry("ean8", List.of(7, 7)), Map.entry("upca", List.of(9, 9)), Map.entry("upce", List.of(9, 7)),
|
||||
Map.entry("qr", List.of(4, 4)), Map.entry("datamatrix", List.of(4, 4)), Map.entry("pdf417", List.of(4, 4)), Map.entry("aztec", List.of(4, 4)));
|
||||
assertEquals(BarcodeFit.TYPES.size(), Symbology.values().length);
|
||||
for(String id : BarcodeFit.TYPES)
|
||||
{
|
||||
Symbology symbology = Symbology.of(id);
|
||||
assertEquals(id, symbology.id());
|
||||
assertEquals(quiet.get(id), List.of(symbology.quietLeft(), symbology.quietRight()), id);
|
||||
assertEquals(!LINEAR.contains(id), symbology.matrix(), id);
|
||||
assertEquals(RETAIL.contains(id), symbology.fixed(), id);
|
||||
assertEquals(LINEAR.contains(id), symbology.provable(), id);
|
||||
}
|
||||
assertNull(Symbology.of("codabar"));
|
||||
assertNull(Symbology.of(null));
|
||||
assertEquals(64, Symbology.MAXIMUM_QUIET_MODULES);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** The narrowest bar by profile, taken up to whole dots at 203, 300 and 600
|
||||
** dpi. The design's own 0.25 mm is 2, 3 and 6 dots and its 0.375 mm for a
|
||||
** two-dimensional symbol 3, 5 and 9. The GS1 minimum for EAN/UPC at general
|
||||
** retail, 0.264 mm, is 3, 4 and 7 dots; the GS1 minimum in general
|
||||
** distribution, 0.495 mm, is 4, 6 and 12 dots. With no profile named, each
|
||||
** symbology takes the strictest rule written for its own use: general
|
||||
** distribution for GS1-128 and Interleaved 2 of 5, general retail for
|
||||
** EAN/UPC, and the design's for the rest.
|
||||
***************************************************************************/
|
||||
@Test
|
||||
void theNarrowestBarIsThatOfTheProfile()
|
||||
{
|
||||
List<Integer> design = List.of(2, 3, 6);
|
||||
List<Integer> retail = List.of(3, 4, 7);
|
||||
List<Integer> distribution = List.of(4, 6, 12);
|
||||
for(String id : BarcodeFit.TYPES)
|
||||
{
|
||||
boolean matrix = !LINEAR.contains(id);
|
||||
boolean logistic = List.of("itf", "gs1-128").contains(id);
|
||||
assertEquals(matrix ? List.of(3, 5, 9) : design, narrowest(id, Symbology.Profile.NONE), id);
|
||||
assertEquals(matrix ? List.of(3, 5, 9) : RETAIL.contains(id) ? retail : design, narrowest(id, Symbology.Profile.GS1_GENERAL), id);
|
||||
assertEquals(matrix ? List.of(3, 5, 9) : RETAIL.contains(id) || logistic ? distribution : design, narrowest(id, Symbology.Profile.GS1_LOGISTICS), id);
|
||||
assertEquals(matrix ? List.of(3, 5, 9) : RETAIL.contains(id) ? retail : logistic ? distribution : design, narrowest(id, null), id);
|
||||
}
|
||||
assertEquals(Symbology.Profile.NONE, Symbology.Profile.of("none"));
|
||||
assertEquals(Symbology.Profile.GS1_GENERAL, Symbology.Profile.of("gs1-general"));
|
||||
assertEquals(Symbology.Profile.GS1_LOGISTICS, Symbology.Profile.of("gs1-logistics"));
|
||||
assertNull(Symbology.Profile.of("retail"));
|
||||
assertNull(Symbology.Profile.of(null));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** How tall bars must be. Code 128 and the symbologies held to its rule: 15%
|
||||
** of the width of the bars and never under 5 mm, which is 40, 60 and 119
|
||||
** dots. EAN-13, UPC-A and UPC-E: 69.24 modules, so 208 dots at a module of
|
||||
** 3 dots (207.72, taken up); EAN-8: 55.24 modules, 166 dots. A
|
||||
** two-dimensional symbol has no such rule.
|
||||
***************************************************************************/
|
||||
@Test
|
||||
void theHeightOfTheBarsIsThatOfEachSymbology()
|
||||
{
|
||||
for(String id : List.of("code128", "code39", "code93", "itf", "gs1-128"))
|
||||
{
|
||||
assertEquals(new ModuleFit.Height(299, 40, 15, 100), Symbology.of(id).height(299, 203), id);
|
||||
assertEquals(List.of(60, 119), List.of(Symbology.of(id).height(299, 300).least(), Symbology.of(id).height(299, 600).least()), id);
|
||||
}
|
||||
assertEquals(new ModuleFit.Height(95, 0, 6924, 9500), Symbology.EAN13.height(95, 203));
|
||||
assertEquals(List.of(208, 208, 208, 166), List.of(Symbology.EAN13.height(95, 203).at(3), Symbology.UPCA.height(95, 203).at(3), Symbology.UPCE.height(51, 203).at(3), Symbology.EAN8.height(67, 203).at(3)));
|
||||
assertEquals(0, Symbology.QR.height(21, 600).at(9));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** The widest symbol of a length is what the encoder draws for the widest
|
||||
** value of that length, and no value from space to tilde is wider: Code 128
|
||||
** in lower-case letters, Code 39, Code 93 in lower-case letters (two symbol
|
||||
** characters each), Interleaved 2 of 5 in pairs of digits, where the widest
|
||||
** of an odd length is that of the even length below it. The EAN/UPC symbols
|
||||
** are one width.
|
||||
***************************************************************************/
|
||||
@Test
|
||||
void theWidestSymbolOfALengthIsTheWidestTheEncoderDraws()
|
||||
{
|
||||
for(int length = 1; length <= 48; length++)
|
||||
{
|
||||
assertEquals(Symbology.CODE128.widest(length), modules("code128", "a".repeat(length)).getFirst(), "code128 of " + length);
|
||||
assertEquals(Symbology.CODE39.widest(length), modules("code39", "A".repeat(length)).getFirst(), "code39 of " + length);
|
||||
assertEquals(Symbology.CODE93.widest(length), modules("code93", "a".repeat(length)).getFirst(), "code93 of " + length);
|
||||
assertEquals(Symbology.ITF.widest(2 * length), modules("itf", "12".repeat(length)).getFirst(), "itf of " + 2 * length);
|
||||
assertEquals(Symbology.ITF.widest(2 * length), Symbology.ITF.widest(2 * length + 1), "itf of " + (2 * length + 1));
|
||||
}
|
||||
assertEquals(9, Symbology.ITF.widest(1));
|
||||
assertEquals(List.of(95, 67, 95, 51), List.of(modules("ean13", "001234567890").getFirst(), modules("ean8", "1234567").getFirst(), modules("upca", "01234567890").getFirst(), modules("upce", "0425261").getFirst()));
|
||||
assertEquals(List.of(95, 67, 95, 51), List.of(Symbology.EAN13.widest(0), Symbology.EAN8.widest(8), Symbology.UPCA.widest(12), Symbology.UPCE.widest(4000)));
|
||||
|
||||
SplittableRandom random = new SplittableRandom(20261115L);
|
||||
String printable = printable();
|
||||
for(int iteration = 0; iteration < 3000; iteration++)
|
||||
{
|
||||
int length = random.nextInt(1, 49);
|
||||
String mixed = text(random, random.nextBoolean() ? printable : "0123456789Aa", length);
|
||||
assertTrue(modules("code128", mixed).getFirst() <= Symbology.CODE128.widest(length), "code128 of " + length + " characters");
|
||||
assertTrue(modules("code93", mixed).getFirst() <= Symbology.CODE93.widest(length), "code93 of " + length + " characters");
|
||||
assertEquals(Symbology.CODE39.widest(length), modules("code39", text(random, "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ-. $/+%", length)).getFirst(), "code39 of " + length + " characters");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** A GS1-128 element string of a length, brackets included, is never wider
|
||||
** than the table says: each application identifier costs two brackets that
|
||||
** are not drawn and at most one separator that is. The widest are strings
|
||||
** of one identifier and letters; a real label, mostly digits, is far
|
||||
** narrower, which is why a declared length proves little for GS1-128.
|
||||
***************************************************************************/
|
||||
@Test
|
||||
void aGs1ElementStringIsNoWiderThanItsLength()
|
||||
{
|
||||
assertEquals(11 * 24 + 24, Symbology.GS1_128.widest(24));
|
||||
assertEquals(Symbology.GS1_128.widest(24), modules("gs1-128", "[21]" + "a".repeat(20)).getFirst());
|
||||
assertEquals(299, modules("gs1-128", "[01]00012345678905[11]260101[21]TEST00010002").getFirst());
|
||||
assertEquals(11 * 44 + 24, Symbology.GS1_128.widest(44));
|
||||
SplittableRandom random = new SplittableRandom(20261116L);
|
||||
for(int iteration = 0; iteration < 3000; iteration++)
|
||||
{
|
||||
StringBuilder elements = new StringBuilder();
|
||||
for(int element = random.nextInt(1, 5); element > 0; element--)
|
||||
{
|
||||
switch(random.nextInt(5))
|
||||
{
|
||||
case 0 -> elements.append("[01]0001234567890").append(5);
|
||||
case 1 -> elements.append("[11]").append(text(random, "0123456789", 6));
|
||||
case 2 -> elements.append("[10]").append(text(random, "0123456789ABCabc-", random.nextInt(1, 21)));
|
||||
case 3 -> elements.append("[21]").append(text(random, "a1", random.nextInt(1, 21)));
|
||||
default -> elements.append("[21]").append(text(random, "abcXYZ0123456789", random.nextInt(1, 21)));
|
||||
}
|
||||
}
|
||||
assertTrue(modules("gs1-128", elements.toString()).getFirst() <= Symbology.GS1_128.widest(elements.length()), elements.length() + " characters of element string");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** No two-dimensional symbology has one value of a length that is its
|
||||
** largest symbol, which is why a declared length proves nothing for them.
|
||||
** Data Matrix and PDF417 choose among shapes, so a shorter value can be the
|
||||
** wider: seven bytes of Data Matrix are 32 by 8 modules where six are 14 by
|
||||
** 14, and eleven of PDF417 are 120 wide and 21 high where ten are 103 and
|
||||
** 30. QR and Aztec change mode within a value, and that costs: here values
|
||||
** of ordinary characters that need a larger symbol than as many plain
|
||||
** bytes.
|
||||
***************************************************************************/
|
||||
@Test
|
||||
void noTwoDimensionalSymbologyHasALargestSymbolOfALength()
|
||||
{
|
||||
SplittableRandom random = new SplittableRandom(20261117L);
|
||||
String printable = printable();
|
||||
int square = 0;
|
||||
int target = 0;
|
||||
for(int iteration = 0; iteration < 1500; iteration++)
|
||||
{
|
||||
int length = random.nextInt(150, 401);
|
||||
String value = text(random, List.of(printable, "0123456789abcdefABCDEF $%*+-./:").get(random.nextInt(2)), length);
|
||||
square += modules("qr", value).getFirst() > modules("qr", ACCENTED.repeat(length)).getFirst() ? 1 : 0;
|
||||
target += modules("aztec", value).getFirst() > modules("aztec", ACCENTED.repeat(length)).getFirst() ? 1 : 0;
|
||||
}
|
||||
assertTrue(square > 0 && target > 0, square + " qr and " + target + " aztec symbols larger than as many bytes");
|
||||
assertEquals(List.of(List.of(14, 14), List.of(32, 8)), List.of(modules("datamatrix", ACCENTED.repeat(6)), modules("datamatrix", ACCENTED.repeat(7))));
|
||||
assertEquals(List.of(List.of(103, 30), List.of(120, 21)), List.of(modules("pdf417", ACCENTED.repeat(10)), modules("pdf417", ACCENTED.repeat(11))));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"stock": {"widthMicrons": 101600, "heightMicrons": 25400, "topMicrons": 3000, "rightMicrons": 3000, "bottomMicrons": 3000, "leftMicrons": 3000, "dpi": 203},
|
||||
"scene": {
|
||||
"schema": "qqq-zpl-scene/2",
|
||||
"design": {"width": "4in", "height": "1in"},
|
||||
"elements": [
|
||||
{"id": "code", "type": "barcode", "symbology": "gs1-128", "value": "[01]00012345678905[11]260101[21]TEST0001", "anchor": "top-center", "y": "0.5mm", "width": "94mm", "height": "13mm", "barcodeProfile": "gs1-general"},
|
||||
{"id": "rule", "type": "box", "anchor": "bottom-center", "y": "1mm", "width": "94mm", "height": "0.5pt", "thickness": "0.5pt"}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"stock": {"widthMicrons": 50800, "heightMicrons": 25400, "topMicrons": 2000, "rightMicrons": 2000, "bottomMicrons": 2000, "leftMicrons": 2000, "dpi": 203},
|
||||
"scene": {
|
||||
"schema": "qqq-zpl-scene/2",
|
||||
"design": {"width": "2in", "height": "1in"},
|
||||
"elements": [
|
||||
{"id": "ground", "type": "box", "y": "11mm", "width": "38mm", "height": "10mm", "fill": true},
|
||||
{"id": "code", "type": "barcode", "symbology": "code128", "value": "TEST-0001", "width": "38mm", "height": "9mm"},
|
||||
{"id": "capped", "type": "barcode", "symbology": "code39", "value": "T1", "x": "1mm", "y": "12mm", "width": "36mm", "height": "8mm", "align": "right", "maxModule": "0.4mm", "quiet": 12},
|
||||
{"id": "side", "type": "barcode", "symbology": "itf", "value": "0012", "anchor": "top-right", "width": "7mm", "height": "21mm", "rotation": 90, "barcodeProfile": "none"}
|
||||
],
|
||||
"overrides": [
|
||||
{"when": {"dpi": [600]}, "set": {"capped": {"maxModule": "0.3mm"}}}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"stock": {"widthMicrons": 50800, "heightMicrons": 25400, "topMicrons": 2000, "rightMicrons": 2000, "bottomMicrons": 2000, "leftMicrons": 2000, "dpi": 203},
|
||||
"scene": {
|
||||
"schema": "qqq-zpl-scene/2",
|
||||
"design": {"width": "2in", "height": "1in"},
|
||||
"elements": [
|
||||
{"id": "square", "type": "barcode", "symbology": "qr", "value": "TEST-0001", "y": "2mm", "width": "14mm", "height": "14mm"},
|
||||
{"id": "turned", "type": "barcode", "symbology": "datamatrix", "value": "TEST-0001-0002", "anchor": "top-center", "y": "2mm", "width": "9mm", "height": "18mm", "align": "left", "valign": "bottom", "rotation": 90},
|
||||
{"id": "target", "type": "barcode", "symbology": "aztec", "value": "TEST-0001", "anchor": "top-right", "y": "2mm", "width": "14mm", "height": "14mm", "align": "right", "valign": "top", "quiet": 5}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"stock": {"widthMicrons": 101600, "heightMicrons": 25400, "topMicrons": 3000, "rightMicrons": 3000, "bottomMicrons": 3000, "leftMicrons": 3000, "dpi": 203},
|
||||
"scene": {
|
||||
"schema": "qqq-zpl-scene/2",
|
||||
"design": {"width": "4in", "height": "1in"},
|
||||
"elements": [
|
||||
{"id": "retail", "type": "barcode", "symbology": "ean13", "value": "0012345678905", "width": "45mm", "minHeight": "15mm"},
|
||||
{"id": "small", "type": "barcode", "symbology": "upce", "value": "04252614", "anchor": "top-right", "width": "26mm", "height": "16mm", "minHeight": "15mm", "align": "left", "valign": "bottom"},
|
||||
{"id": "rule", "type": "box", "anchor": "bottom-right", "width": "26mm", "height": "0.5pt", "thickness": "0.5pt"}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 271 B |
@@ -0,0 +1,4 @@
|
||||
^XA^PW812^LL203^LH0,0^CI28
|
||||
^FO128,28^GFA,7210,7210,70,F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3Line truncated
|
||||
^FO30,170^GFA,94,94,94,FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE^FS
|
||||
^XZ
|
||||
|
After Width: | Height: | Size: 487 B |
@@ -0,0 +1,4 @@
|
||||
^XA^PW1200^LL300^LH0,0^CI28
|
||||
^FO184,42^GFA,15912,15912,104,FC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03Line truncated
|
||||
^FO45,251^GFA,278,278,139,FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC^FS
|
||||
^XZ
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,4 @@
|
||||
^XA^PW2400^LL600^LH0,0^CI28
|
||||
^FO230,83^GFA,74601,74601,243,FFFC07F0003FFFFE0007FFFFFF80FE03FFFFE03FFF0003FFF01FFF8001FFF80FFFC000FFFC000FFFC000FFFC07FFE0007FFFFC07FFE03FFFFE03F80FFFFF80FFFC00001FC0000003F80FFFC000FFFC07FFE03FFFFFFC07F000007F0003FFF0003FFF000007F0003F8001FFFFF0003F8001FFF80FFFC000FFFC07FFE0007FFE0007FFE03FFF0003FFF01FFFFF0003F8001FC07FFFFFF80FFFFF80FFFC07FFFFC00001FC07F000007FFE03F800003FFF01FFFFF01FC00001FFF80FFFFF800003F80FE03FFFFE03FFFFFFC07FFE03FFF0003FFF0003FFF0003FFF01FFF8001FC00001FC00001FFF80FFFC00001FFFFF01FC07FFE0FFFC07F0003FFFFE0007FFFFFF80FE03FFFFE03FFF0003FFF01FFF8001FFF80FFFC000FFFC000FFFC000FFFC07FFE0007FFFFC07FFE03FFFFE03F80FFFFF80FFFC00001FC0000003F80FFFC000FFFC07FFE03FFFFFFC07F000007F0003FFF0003FFF000007F0003F8001FFFFF0003F8001FFF80FFFC000FFFC07FFE0007FFE0007FFE03FFF0003FFF01FFFFF0003F8001FC07FFFFFF80FFFFF80FFFC07FFFFC00001FC07F000007FFE03F800003FFF01FFFFF01FC00001FFF80FFFFF800003F80FE03FFFFE03FFFFFFC07FFE03FFF0003FFF0003FFF0003FFF01FFF8001FC00001FC00001FFF80FFFC00001FFFFF01FC07FFE0FFFC07F0003FFFFE0007FFFFFF80FE03FFFFE03FFF0003FFF01FFF8001FFF80FFFC000FFFC000FFFC000FFFC07FFE0007FFFFC07FFE03FFFFE03F80FFFFF80FFFC00001FC0000003F80FFFC000FFFC07FFE03FFFFFFC07F000007F0003FFF0003FFF000007F0003F8001FFFFF0003F8001FFF80FFFC000FFFC07FFE0007FFE0007FFE03FFF0003FFF01FFFFF0003F8001FC07FFFFFF80FFFFF80FFFC07FFFFC00001FC07F000007FFE03F800003FFF01FFFFF01FC00001FFF80FFFFF800003F80FE03FFFFE03FFFFFFC07FFE03FFF0003FFF0003FFF0003FFF01FFF8001FC00001FC00001FFF80FFFC00001FFFFF01FC07FFE0FFFC07F0003FFFFE0007FFFFFF80FE03FFFFE03FFF0003FFF01FFF8001FFF80FFFC000FFFC000FFFC000FFFC07FFE0007FFFFC07FFE03FFFFE03F80FFFFF80FFFC00001FC0000003F80FFFC000FFFC07FFE03FFFFFFC07F000007F0003FFF0003FFF000007F0003F8001FFFFF0003F8001FFF80FFFC000FFFC07FFE0007FFE0007FFE03FFF0003FFF01FFFFF0003F8001FC07FFFFFF80FFFFF80FFFC07FFFFC00001FC07F000007FFE03F800003FFF01FFFFF01FC00001FFF80FFFFF800003F80FE03FFFFE03FFFFFFC07FFE03FFF0003FFF0003FFF0003FFF01FFF8001FC00001FC00001FFF80FFFC00001FFFFF01FC07FFE0FFFC07F0003FFFFE0007FFFFFF80FE03FFFFE03FFF0003FFF01FFF8001FFF80FFFC000FFFC000FFFC000FFFC07FFE0007FFFFC07FFE03FFFFE03F80FFFFF80FFFC00001FC0000003F80FFFC000FFFC07FFE03FFFFFFC07F000007F0003FFF0003FFF000007F0003F8001FFFFF0003F8001FFF80FFFC000FFFC07FFE0007FFE0007FFE03FFF0003FFF01FFFFF0003F8001FC07FFFFFF80FFFFF80FFFC07FFFFC00001FC07F000007FFE03F800003FFF01FFFFF01FC00001FFF80FFFFF800003F80FE03FFFFE03FFFFFFC07FFE03FFF0003FFF0003FFF0003FFF01FFF8001FC00001FC00001FFF80FFFC00001FFFFF01FC07FFE0FFFC07F0003FFFFE0007FFFFFF80FE03FFFFE03FFF0003FFF01FFF8001FFF80FFFC000FFFC000FFFC000FFFC07FFE0007FFFFC07FFE03FFFFE03F80FFFFF80FFFC00001FC0000003F80FFFC000FFFC07FFE03FFFFFFC07F000007F0003FFF0003FFF000007F0003F8001FFFFF0003F8001FFF80FFFC000FFFC07FFE0007FFE0007FFE03FFF0003FFF01FFFFF0003F8001FC07FFFFFF80FFFFF80FFFC07FFFFC00001FC07F000007FFE03F800003FFF01FFFFF01FC00001FFF80FFFFF800003F80FE03FFFFE03FFFFFFC07FFE03FFF0003FFF0003FFF0003FFF01FFF8001FC00001FC00001FFF80FFFC00001FFFFF01FC07FFE0FFFC07F0003FFFFE0007FFFFFF80FE03FFFFE03FFF0003FFF01FFF8001FFF80FFFC000FFFC000FFFC000FFFC07FFE0007FFFFC07FFE03FFFFE03F80FFFFF80FFFC00001FC0000003F80FFFC000FFFC07FFE03FFFFFFC07F000007F0003FFF0003FFF000007F0003F8001FFFFF0003F8001FFF80FFFC000FFFC07FFE0007FFE0007FFE03FFF0003FFF01FFFFF0003F8001FC07FFFFFF80FFFFF80FFFC07FFFFC00001FC07F000007FFE03F800003FFF01FFFFF01FC00001FFF80FFFFF800003F80FE03FFFFE03FFFFFFC07FFE03FFF0003FFF0003FFF0003FFF01FFF8001FC00001FC00001FFF80FFFC00001FFFFF01FC07FFE0FFFC07F0003FFFFE0007FFFFFF80FE03FFFFE03FFF0003FFF01FFF8001FFF80FFFC000FFFC000FFFC000FFFC07FFE0007FFFFC07FFE03FFFFE03F80FFFFF80FFFC00001FC0000003F80FFFC000FFFC07FFE03FFFFFFC07F000007F0003FFF0003FFF000007F0003F8001FFFFF0003F8001FFF80FFFC000FFFC07FFE0007FFE0007FFE03FFF0003FFF01FFFFF0003F8001FC07FFFFFF80FFFFF80FFFC07FFFFC00001FC07F000007FFE03F800003FFF01FFFFF01FC00001FFF80FFFFF800003F80FE03FFFFE03FFFFFFC07FFE03FFF0003FFF0003FFF0003FFF01FFF8001FC00001FC00001FFF80FFFC00001FFFFF01FC07FFE0FFFC07F0003FFFFE0007FFFFFF80FE03FFFFE03FFF0003FFF01FFF8001FFF80FFFC000FFFC000FFFC000FFFC07FFE0007FFFFC07FFE03FFFFE03F80FFFFF80FFFC00001FC0000003F80FFFC000FFFC07FFE03FFFFFFC07F000007F0003FFF0003FFF000007F0003F8001FFFFF0003F8001FFF80FFFC000FFFC07FFE0007FFE0007FFE03FFF0003FFF01FFFFF0003F8001FC07FFFFFF80FFFFF80FFFC07FFFFC00001FC07F000007FFE03F800003FFF01FFFFF01FC00001FFF80FFFFF800003F80FE03FFFFE03FFFFFFC07FFE03FFF0003FFF0003FFF0003FFF01FFF8001FC00001FC00001FFF80FFFC00001FFFFF01FC07FFE0FFFC07F0003FFFFE0007FFFFFF80FE03FFFFE03FFF0003FFF01FFF8001FFF80FFFC000FFFC000FFFC000FFFC07FFE0007FFFFC07FFE03FFFFE03F80FFFFF80FFFC00001FC0000003F80FFFC000FFFC07FFE03FFFFFFC07F000007F0003FFF0003FFF000007F0003F8001FFFFF0003F8001FFF80FFFC000FFFC07FFE0007FFE0007FFE03FFF0003FFF01FFFFF0003F8001FC07FFFFFF80FFFFF80FFFC07FFFFC00001FC07F000007FFE03F800003FFF01FFFFF01FC00001FFF80FFFFF800003F80FE03FFFFE03FFFFFFC07FFE03FFF0003FFF0003FFF0003FFF01FFF8001FC00001FC00001FFF80FFFC00001FFFFF01FC07FFE0FFFC07F0003FFFFE0007FFFFFF80FE03FFFFE03FFF0003FFF01FFF8001FFF80FFFC000FFFC000FFFC000FFFC07FFE0007FFFFC07FFE03Line truncated
|
||||
^FO90,501^GFA,1112,1112,278,FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0^FS
|
||||
^XZ
|
||||
|
After Width: | Height: | Size: 295 B |
@@ -0,0 +1,3 @@
|
||||
^XA^PW406^LL203^LH0,0^CI28
|
||||
^FO16,16^GFA,7849,7849,47,0000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000001FFFFFFFFFFFFFC0000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000001FFFFFFFFFFFFFC0000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000001FFFFFFFFFFFFFC0000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000001FFFFFFFFFFFFFC0000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000001FFFFFFFFFFFFFC0000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000001FFFFFFFFFFFFFC0000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000001FFFFFFFFFFFFFC0000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000001FFFFFFFFFFFFFC0000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C0000000000000000Line truncated
|
||||
^XZ
|
||||
|
After Width: | Height: | Size: 417 B |
@@ -0,0 +1,4 @@
|
||||
^XA^PW600^LL300^LH0,0^CI28
|
||||
^FO63,24^GFA,6955,6955,65,FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00Line truncated
|
||||
^FO24,140^GFA,9108,9108,69,0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003FFFFFFFFFFFFFFFFFFFF0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003FFFFFFFFFFFFFFFFFFFF0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003FFFFFFFFFFFFFFFFFFFF0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003FFFFFFFFFFFFFFFFFFFF0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003FFFFFFFFFFFFFFFFFFFF0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003FFFFFFFFFFFFFFFFFFFF0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003FFFFFFFFFFFFFFFFFFFF0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003FFFFFFFFFFFFFFFFFFFF0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003FFFFFFFFFFFFFFFFFFFF0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003FFFFFFFFFFFFFFFFFFFF0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF000003FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF000003FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF000003FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF000003FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000000000000000000000FFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF00000000000000000000000000FFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF00000000000000000000000000FFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF00000000000000000000000000FFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF00000000000000000000000000FFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF000003FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF000003FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF000003FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF00000000000000000000000000FFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF00000000000000000000000000FFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF00000000000000000000000000FFFFLine truncated
|
||||
^XZ
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,4 @@
|
||||
^XA^PW1200^LL600^LH0,0^CI28
|
||||
^FO127,48^GFA,27348,27348,129,FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FLine truncated
|
||||
^FO48,278^GFA,36974,36974,139,0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FFFFFFFFLine truncated
|
||||
^XZ
|
||||
|
After Width: | Height: | Size: 622 B |
@@ -0,0 +1,3 @@
|
||||
^XA^PW406^LL203^LH0,0^CI28
|
||||
^FO40,52^GFA,4662,4662,42,0000000000000000000000000000000000000000000000000000000000000000000003C03FFC03C000000000000000000000000000000000000000000000000000000000000000000000000003C03FFC03C000000000000000000000000000000000000000000000000000000000000000000000000003C03FFC03C000000000000000000000000000000000000000000000000000000000000000000000000003C03FFC03C00000FFFFF8E3803FFFFE0000000000000000000000000000000000000000000000000003C3C03FFFFFC3FC00FFFFF8E3803FFFFE0000000000000000000000000000000000000000000000000003C3C03FFFFFC3FC00FFFFF8E3803FFFFE0000000000000000000000000000000000000000000000000003C3C03FFFFFC3FC00E000381C7E38000E0000000000000000000000000000000000000000000000000003C3C03FFFFFC3FC00E000381C7E38000E0000000000000000000000000000000000000000000000000003C3FC0003C003FC00E000381C7E38000E0000000000000000000000000000000000000000000000000003C3FC0003C003FC00E3FE381C0E38FF8E0000000000000000000000000000000000000000000000000003C3FC0003C003FC00E3FE381C0E38FF8E0000000000000000000000000000000000000000000000000003C3FC0003C003FC00E3FE381C0E38FF8E0000000000000000000000000000000000000000000000000003C3FFFFFFFFFFFFC0E3FE38E00E38FF8E0000000000000000000000000000000000000000000000000003C3FFFFFFFFFFFFC0E3FE38E00E38FF8E0000000000000000000000000000000000000000000000000003C3FFFFFFFFFFFFC0E3FE38E00E38FF8E0000000000000000001FFFFFE000000000000000000000000003C3FFFFFFFFFFFFC0E3FE381C7038FF8E0000000000000000001FFFFFE0000000000000000000000000003C3C0000003C03C0E3FE381C7038FF8E0000000000000000001FFFFFE0000000000000000000000000003C3C0000003C03C0E3FE381C7038FF8E0000000000000000001C71C000000000000000000000000000003C3C0000003C03C0E0003803F038000E0000000000000000001C71C000000000000000000000000000003C3C0000003C03C0E0003803F038000E0000000000000000001C71C00000000000000000000000000003C03C3FFFFC3C3C00E0003803F038000E0000000000000000001C0FC7E000000000000000000000000003C03C3FFFFC3C3C00FFFFF8E38E3FFFFE0000000000000000001C0FC7E000000000000000000000000003C03C3FFFFC3C3C00FFFFF8E38E3FFFFE0000000000000000001C0FC7E000000000000000000000000003C03C3FFFFC3C3C00FFFFF8E38E3FFFFE0000000000000000001F80070000000000000000000000000003C03C3C003C3FC3C00000001F8E0000000000000000000000001F80070000000000000000000000000003C03C3C003C3FC3C00000001F8E0000000000000000000000001F80070000000000000000000000000003C03C3C003C3FC3C00000001F8E0000000000000000000000001C0FC0E000000000000000000000000003C03C3C003C3FC3C0038FF8E38FC01C0E0000000000000000001C0FC0E000000000000000000000000003C3FC3C3C3C3FFFC0038FF8E38FC01C0E0000000000000000001C0FC0E000000000000000000000000003C3FC3C3C3C3FFFC0038FF8E38FC01C0E0000000000000000001C0E000000000000000000000000000003C3FC3C3C3C3FFFC01C7E00E071FF1F8E0000000000000000001C0E000000000000000000000000000003C3FC3C3C3C3FFFC01C7E00E071FF1F8E0000000000000000001C0E0000000000000000000000000000003FFC3C003C3FC0001C7E00E071FF1F8E0000000000000000001FF1F8E0000000000000000000000000003FFC3C003C3FC0001C0FF8E07000E07E0000000000000000001FF1F8E0000000000000000000000000003FFC3C003C3FC0001C0FF8E07000E07E0000000000000000001FF1F8E0000000000000000000000000003FFC3C003C3FC0001C0FF8E07000E07E0000000000000000001C01C700000000000000000000000000003FFC3FFFFC3C03C0E07000FFFFFF1C7E0000000000000000001C01C700000000000000000000000000003FFC3FFFFC3C03C0E07000FFFFFF1C7E0000000000000000001C01C700000000000000000000000000003FFC3FFFFC3C03C0E07000FFFFFF1C7E0000000000000000001C7038E0000000000000000000000000003FFC3FFFFC3C03C0FFFE3FFC7038FF8E0000000000000000001C7038E0000000000000000000000000003C3C0000003FFFC0FFFE3FFC7038FF8E0000000000000000001C7038E0000000000000000000000000003C3C0000003FFFC0FFFE3FFC7038FF8E0000000000000000001FF1F800000000000000000000000000003C3C0000003FFFC0000000FC01C7FC000000000000000000001FF1F800000000000000000000000000003C3C0000003FFFC0000000FC01C7FC000000000000000000001FF1F80000000000000000000000000000003FFFFFFFFFC000000000FC01C7FC000000000000000000001F81C0E000000000000000000000000000003FFFFFFFFFC000FFFFF81C0FF8FC700000000000000000001F81C0E000000000000000000000000000003FFFFFFFFFC000FFFFF81C0FF8FC700000000000000000001F81C0E000000000000000000000000000003FFFFFFFFFC000FFFFF81C0FF8FC700000000000000000001C0E38000000000000000000000000000000003C3C03C00000E00038FC700703F00000000000000000001C0E38000000000000000000000000000000003C3C03C00000E00038FC700703F00000000000000000001C0E38000000000000000000000000000000003C3C03C00000E00038FC700703F00000000000000000001F8FC7E00000000000000000000000000000003C3C03C00000E3FE38E38FF8E3FE0000000000000000001F8FC7E000000000000000000000000003C03FC3C3C3C3FFC0E3FE38E38FF8E3FE0000000000000000001F8FC7E000000000000000000000000003C03FC3C3C3C3FFC0E3FE38E38FF8E3FE0000000000000000001C71F80000000000000000000000000003C03FC3C3C3C3FFC0E3FE381F803F03F00000000000000000001C71F80000000000000000000000000003C03FC3C3C3C3FFC0E3FE381F803F03F00000000000000000001C71F8000000000000000000000000000003FFC00000003FC0E3FE381F803F03F00000000000000000001C01F8E00000000000000000000000000003FFC00000003FC0E3FE38E38FC0FF8E0000000000000000001C01F8E00000000000000000000000000003FFC00000003FC0E3FE38E38FC0FF8E0Line truncated
|
||||
^XZ
|
||||
|
After Width: | Height: | Size: 772 B |
@@ -0,0 +1,3 @@
|
||||
^XA^PW600^LL300^LH0,0^CI28
|
||||
^FO54,78^GFA,10044,10044,62,FFFFFFFFE0F83E0003FFFFFFFF800000000000000000000000000000000000000000000000000000000000000000000000000003F000FFFFC003F0000000FFFFFFFFE0F83E0003FFFFFFFF800000000000000000000000000000000000000000000000000000000000000000000000000003F000FFFFC003F0000000FFFFFFFFE0F83E0003FFFFFFFF8000000000000000000000000007FFFFFFFFF80000000000000000000000000000000000000003F000FFFFC003F0000000FFFFFFFFE0F83E0003FFFFFFFF8000000000000000000000000007FFFFFFFFF80000000000000000000000000000000000000003F000FFFFC003F0000000FFFFFFFFE0F83E0003FFFFFFFF8000000000000000000000000007FFFFFFFFF80000000000000000000000000000000000000003F000FFFFC003F0000000F8000003E007C1FF83E000000F8000000000000000000000000007FFFFFFFFF80000000000000000000000000000000000000003F000FFFFC003F0000000F8000003E007C1FF83E000000F8000000000000000000000000007FFFFFFFFF80000000000000000000000000000000000003F03F000FFFFFFFFF03FFC00F8000003E007C1FF83E000000F8000000000000000000000000007C1F07C00000000000000000000000000000000000000003F03F000FFFFFFFFF03FFC00F8000003E007C1FF83E000000F8000000000000000000000000007C1F07C00000000000000000000000000000000000000003F03F000FFFFFFFFF03FFC00F8000003E007C1FF83E000000F8000000000000000000000000007C1F07C00000000000000000000000000000000000000003F03F000FFFFFFFFF03FFC00F83FFF83E007C00F83E0FFFE0F8000000000000000000000000007C1F07C00000000000000000000000000000000000000003F03F000FFFFFFFFF03FFC00F83FFF83E007C00F83E0FFFE0F8000000000000000000000000007C1F07C00000000000000000000000000000000000000003F03F000FFFFFFFFF03FFC00F83FFF83E007C00F83E0FFFE0F8000000000000000000000000007C00FFC1FF80000000000000000000000000000000000003F03FFC000003F00003FFC00F83FFF83E007C00F83E0FFFE0F8000000000000000000000000007C00FFC1FF80000000000000000000000000000000000003F03FFC000003F00003FFC00F83FFF83E007C00F83E0FFFE0F8000000000000000000000000007C00FFC1FF80000000000000000000000000000000000003F03FFC000003F00003FFC00F83FFF83E0F8000F83E0FFFE0F8000000000000000000000000007C00FFC1FF80000000000000000000000000000000000003F03FFC000003F00003FFC00F83FFF83E0F8000F83E0FFFE0F8000000000000000000000000007C00FFC1FF80000000000000000000000000000000000003F03FFC000003F00003FFC00F83FFF83E0F8000F83E0FFFE0F8000000000000000000000000007FE00001F000000000000000000000000000000000000003F03FFC000003F00003FFC00F83FFF83E0F8000F83E0FFFE0F8000000000000000000000000007FE00001F000000000000000000000000000000000000003F03FFFFFFFFFFFFFFFFFFF0F83FFF83E0F8000F83E0FFFE0F8000000000000000000000000007FE00001F000000000000000000000000000000000000003F03FFFFFFFFFFFFFFFFFFF0F83FFF83E007C1F003E0FFFE0F8000000000000000000000000007FE00001F000000000000000000000000000000000000003F03FFFFFFFFFFFFFFFFFFF0F83FFF83E007C1F003E0FFFE0F8000000000000000000000000007FE00001F000000000000000000000000000000000000003F03FFFFFFFFFFFFFFFFFFF0F83FFF83E007C1F003E0FFFE0F8000000000000000000000000007C00FFC00F80000000000000000000000000000000000003F03FFFFFFFFFFFFFFFFFFF0F83FFF83E007C1F003E0FFFE0F8000000000000000000000000007C00FFC00F80000000000000000000000000000000000003F03FFFFFFFFFFFFFFFFFFF0F83FFF83E007C1F003E0FFFE0F8000000000000000000000000007C00FFC00F800000000000000000000000000000000000000FC0FC0000000000FC003F0F8000003E0003FF003E000000F8000000000000000000000000007C00FFC00F800000000000000000000000000000000000000FC0FC0000000000FC003F0F8000003E0003FF003E000000F8000000000000000000000000007C00FFC00F800000000000000000000000000000000000000FC0FC0000000000FC003F0F8000003E0003FF003E000000F8000000000000000000000000007C00F80000000000000000000000000000000000000000000FC0FC0000000000FC003F0F8000003E0003FF003E000000F8000000000000000000000000007C00F80000000000000000000000000000000000000000000FC0FC0000000000FC003F0F8000003E0003FF003E000000F8000000000000000000000000007C00F80000000000000000000000000000000000000000000FC0FC0000000000FC003F0FFFFFFFFE0F83E0F83FFFFFFFF8000000000000000000000000007C00F8000000000000000000000000000000000000000003F000FC0FFFFFFFC0FC0FC00FFFFFFFFE0F83E0F83FFFFFFFF8000000000000000000000000007C00F8000000000000000000000000000000000000000003F000FC0FFFFFFFC0FC0FC00FFFFFFFFE0F83E0F83FFFFFFFF8000000000000000000000000007FFF07FE0F80000000000000000000000000000000000003F000FC0FFFFFFFC0FC0FC00FFFFFFFFE0F83E0F83FFFFFFFF8000000000000000000000000007FFF07FE0F80000000000000000000000000000000000003F000FC0FFFFFFFC0FC0FC00FFFFFFFFE0F83E0F83FFFFFFFF8000000000000000000000000007FFF07FE0F80000000000000000000000000000000000003F000FC0FFFFFFFC0FC0FC00000000000007FE0F80000000000000000000000000000000000007FFF07FE0F80000000000000000000000000000000000003F000FC0FFFFFFFC0FC0FC00000000000007FE0F80000000000000000000000000000000000007FFF07FE0F80000000000000000000000000000000000003F000FC0FC0000FC0FFF03F0000000000007FE0F80000000000000000000000000000000000007C0007C1F000000000000000000000000000000000000003F000FC0FC0000FC0FFF03F0000000000007FE0F80000000000000000000000000000000000007C0007C1F000000000000000000000000000000000000003F000FC0FC0000FC0FFF03F0000000000007FE0F80000000000000000000000000000000000007C0007C1F000000000000000000000000000000000000003F000FC0FC0000FC0FFF03F0003E0FFFE0FLine truncated
|
||||
^XZ
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,3 @@
|
||||
^XA^PW1200^LL600^LH0,0^CI28
|
||||
^FO97,144^GFA,41664,41664,124,FFFFFFFFFFFFFFFFFFF800FFE003FF800000003FFFFFFFFFFFFFFFFFFE0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFF800FFE003FF800000003FFFFFFFFFFFFFFFFFFE0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFF800FFE003FF800000003FFFFFFFFFFFFFFFFFFE0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFF800FFE003FF800000003FFFFFFFFFFFFFFFFFFE0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFF800FFE003FF800000003FFFFFFFFFFFFFFFFFFE0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFF800FFE003FF800000003FFFFFFFFFFFFFFFFFFE0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFF800FFE003FF800000003FFFFFFFFFFFFFFFFFFE0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFF800FFE003FF800000003FFFFFFFFFFFFFFFFFFE0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFF800FFE003FF800000003FFFFFFFFFFFFFFFFFFE0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFF800FFE003FF800000003FFFFFFFFFFFFFFFFFFE0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFF800FFE003FF800000003FFFFFFFFFFFFFFFFFFE0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FFE00000000000003FF800001FFC007FFFFE003FF80000000000000FFE0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FFE00000000000003FF800001FFC007FFFFE003FF80000000000000FFE0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FFE00000000000003FF800001FFC007FFFFE003FF80000000000000FFE0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FFE00000000000003FF800001FFC007FFFFE003FF80000000000000FFE0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FFE00000000000003FF800001FFC007FFFFE003FF80000000000000FFE0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FFE00000000000003FF800001FFC007FFFFE003FF80000000000000FFE0000000000000000000000000000000000000000000000000007FFFFFFFFFFFFFFFFFFF80000000000000000000000000000000000000000000000000000000000000000000000000003FFE0000007FFFFFFFFF0000003FFE0000000000000FFE00000000000003FF800001FFC007FFFFE003FF80000000000000FFE0000000000000000000000000000000000000000000000000007FFFFFFFFFFFFFFFFFFF80000000000000000000000000000000000000000000000000000000000000000000000000003FFE0000007FFFFFFFFF0000003FFE0000000000000FFE00000000000003FF800001FFC007FFFFE003FF80000000000000FFE0000000000000000000000000000000000000000000000000007FFFFFFFFFFFFFFFFFFF80000000000000000000000000000000000000000000000000000000000000000000000000003FFE0000007FFFFFFFFF0000003FFE0000000000000FFE00000000000003FF800001FFC007FFFFE003FF80000000000000FFE0000000000000000000000000000000000000000000000000007FFFFFFFFFFFFFFFFFFF80000000000000000000000000000000000000000000000000000000000000000000000000003FFE0000007FFFFFFFFF0000003FFE0000000000000FFE000000Line truncated
|
||||
^XZ
|
||||
|
After Width: | Height: | Size: 272 B |
@@ -0,0 +1,3 @@
|
||||
^XA^PW812^LL203^LH0,0^CI28
|
||||
^FO67,24^GFA,14105,14105,91,E3803F1C0FC0E0703F1FFE38E00FC7E00E38E38E001C01C0E07007FC703FE071C0FF8E3800000000000000000000000000000000000000000000000000000000000000071C0FF8E0703F1FF0381F8FC71FFE07E071C71C00000000E3803F1C0FC0E0703F1FFE38E00FC7E00E38E38E001C01C0E07007FC703FE071C0FF8E3800000000000000000000000000000000000000000000000000000000000000071C0FF8E0703F1FF0381F8FC71FFE07E071C71C00000000E3803F1C0FC0E0703F1FFE38E00FC7E00E38E38E001C01C0E07007FC703FE071C0FF8E3800000000000000000000000000000000000000000000000000000000000000071C0FF8E0703F1FF0381F8FC71FFE07E071C71C00000000E3803F1C0FC0E0703F1FFE38E00FC7E00E38E38E001C01C0E07007FC703FE071C0FF8E3800000000000000000000000000000000000000000000000000000000000000071C0FF8E0703F1FF0381F8FC71FFE07E071C71C00000000E3803F1C0FC0E0703F1FFE38E00FC7E00E38E38E001C01C0E07007FC703FE071C0FF8E3800000000000000000000000000000000000000000000000000000000000000071C0FF8E0703F1FF0381F8FC71FFE07E071C71C00000000E3803F1C0FC0E0703F1FFE38E00FC7E00E38E38E001C01C0E07007FC703FE071C0FF8E3800000000000000000000000000000000000000000000000000000000000000071C0FF8E0703F1FF0381F8FC71FFE07E071C71C00000000E3803F1C0FC0E0703F1FFE38E00FC7E00E38E38E001C01C0E07007FC703FE071C0FF8E3800000000000000000000000000000000000000000000000000000000000000071C0FF8E0703F1FF0381F8FC71FFE07E071C71C00000000E3803F1C0FC0E0703F1FFE38E00FC7E00E38E38E001C01C0E07007FC703FE071C0FF8E3800000000000000000000000000000000000000000000000000000000000000071C0FF8E0703F1FF0381F8FC71FFE07E071C71C00000000E3803F1C0FC0E0703F1FFE38E00FC7E00E38E38E001C01C0E07007FC703FE071C0FF8E3800000000000000000000000000000000000000000000000000000000000000071C0FF8E0703F1FF0381F8FC71FFE07E071C71C00000000E3803F1C0FC0E0703F1FFE38E00FC7E00E38E38E001C01C0E07007FC703FE071C0FF8E3800000000000000000000000000000000000000000000000000000000000000071C0FF8E0703F1FF0381F8FC71FFE07E071C71C00000000E3803F1C0FC0E0703F1FFE38E00FC7E00E38E38E001C01C0E07007FC703FE071C0FF8E3800000000000000000000000000000000000000000000000000000000000000071C0FF8E0703F1FF0381F8FC71FFE07E071C71C00000000E3803F1C0FC0E0703F1FFE38E00FC7E00E38E38E001C01C0E07007FC703FE071C0FF8E3800000000000000000000000000000000000000000000000000000000000000071C0FF8E0703F1FF0381F8FC71FFE07E071C71C00000000E3803F1C0FC0E0703F1FFE38E00FC7E00E38E38E001C01C0E07007FC703FE071C0FF8E3800000000000000000000000000000000000000000000000000000000000000071C0FF8E0703F1FF0381F8FC71FFE07E071C71C00000000E3803F1C0FC0E0703F1FFE38E00FC7E00E38E38E001C01C0E07007FC703FE071C0FF8E3800000000000000000000000000000000000000000000000000000000000000071C0FF8E0703F1FF0381F8FC71FFE07E071C71C00000000E3803F1C0FC0E0703F1FFE38E00FC7E00E38E38E001C01C0E07007FC703FE071C0FF8E3800000000000000000000000000000000000000000000000000000000000000071C0FF8E0703F1FF0381F8FC71FFE07E071C71C00000000E3803F1C0FC0E0703F1FFE38E00FC7E00E38E38E001C01C0E07007FC703FE071C0FF8E3800000000000000000000000000000000000000000000000000000000000000071C0FF8E0703F1FF0381F8FC71FFE07E071C71C00000000E3803F1C0FC0E0703F1FFE38E00FC7E00E38E38E001C01C0E07007FC703FE071C0FF8E3800000000000000000000000000000000000000000000000000000000000000071C0FF8E0703F1FF0381F8FC71FFE07E071C71C00000000E3803F1C0FC0E0703F1FFE38E00FC7E00E38E38E001C01C0E07007FC703FE071C0FF8E3800000000000000000000000000000000000000000000000000000000000000071C0FF8E0703F1FF0381F8FC71FFE07E071C71C00000000E3803F1C0FC0E0703F1FFE38E00FC7E00E38E38E001C01C0E07007FC703FE071C0FF8E3800000000000000000000000000000000000000000000000000000000000000071C0FF8E0703F1FF0381F8FC71FFE07E071C71C00000000E3803F1C0FC0E0703F1FFE38E00FC7E00E38E38E001C01C0E07007FC703FE071C0FF8E3800000000000000000000000000000000000000000000000000000000000000071C0FF8E0703F1FF0381F8FC71FFE07E071C71C00000000E3803F1C0FC0E0703F1FFE38E00FC7E00E38E38E001C01C0E07007FC703FE071C0FF8E3800000000000000000000000000000000000000000000000000000000000000071C0FF8E0703F1FF0381F8FC71FFE07E071C71C00000000E3803F1C0FC0E0703F1FFE38E00FC7E00E38E38E001C01C0E07007FC703FE071C0FF8E3800000000000000000000000000000000000000000000000000000000000000071C0FF8E0703F1FF0381F8FC71FFE07E071C71C00000000E3803F1C0FC0E0703F1FFE38E00FC7E00E38E38E001C01C0E07007FC703FE071C0FF8E3800000000000000000000000000000000000000000000000000000000000000071C0FF8E0703F1FF0381F8FC71FFE07E071C71C00000000E3803F1C0FC0E0703F1FFE38E00FC7E00E38E38E001C01C0E07007FC703FE071C0FF8E3800000000000000000000000000000000000000000000000000000000000000071C0FF8E0703F1FF0381F8FC71FFE07E071C71C00000000E3803F1C0FC0E0703F1FFE38E00FC7E00E38E38E001C01C0E07007FC703FE071C0FF8E3800000000000000000000000000000000000000000000000000000000000000071C0FF8E0703F1FF0381F8FC71FFE07E071C71C00000000E3803F1C0FC0E0703F1FFE38E00FC7E00E38E38E001C01C0E07007FC703FE071C0FF8E3800000000000000000000000000000000000000000000000000000000000000071C0FF8E0703F1FF0381F8FC71FFE07E071C71C00000000E3803F1C0FC0E0703F1FFE38E00FC7E00E38E38E001C01C0E07007FC703FE071C0FF8E3800000000000000000000000000000000000000000000000000000000000000071C0FF8E0703F1FF0381F8FC71FFE07E071C71C00000000E3803F1C0FC0E0703F1FFE38E00FC7E00E38E38E001C01C0E07007FC7Line truncated
|
||||
^XZ
|
||||
|
After Width: | Height: | Size: 456 B |
@@ -0,0 +1,3 @@
|
||||
^XA^PW1200^LL300^LH0,0^CI28
|
||||
^FO119,36^GFA,29999,29999,131,F0F000FF0F00FF00F00F00FF0FFFF0F0F000FF0FF000F0F0F0F0F0000F000F00F00F000FFF0F00FFF00F0F00FFF0F0F000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001E1E01FFE1E01E01FE1FFE01E01FE1FE1E1FFFE01FE01E1E1E1E00000000000000000F0F000FF0F00FF00F00F00FF0FFFF0F0F000FF0FF000F0F0F0F0F0000F000F00F00F000FFF0F00FFF00F0F00FFF0F0F000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001E1E01FFE1E01E01FE1FFE01E01FE1FE1E1FFFE01FE01E1E1E1E00000000000000000F0F000FF0F00FF00F00F00FF0FFFF0F0F000FF0FF000F0F0F0F0F0000F000F00F00F000FFF0F00FFF00F0F00FFF0F0F000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001E1E01FFE1E01E01FE1FFE01E01FE1FE1E1FFFE01FE01E1E1E1E00000000000000000F0F000FF0F00FF00F00F00FF0FFFF0F0F000FF0FF000F0F0F0F0F0000F000F00F00F000FFF0F00FFF00F0F00FFF0F0F000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001E1E01FFE1E01E01FE1FFE01E01FE1FE1E1FFFE01FE01E1E1E1E00000000000000000F0F000FF0F00FF00F00F00FF0FFFF0F0F000FF0FF000F0F0F0F0F0000F000F00F00F000FFF0F00FFF00F0F00FFF0F0F000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001E1E01FFE1E01E01FE1FFE01E01FE1FE1E1FFFE01FE01E1E1E1E00000000000000000F0F000FF0F00FF00F00F00FF0FFFF0F0F000FF0FF000F0F0F0F0F0000F000F00F00F000FFF0F00FFF00F0F00FFF0F0F000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001E1E01FFE1E01E01FE1FFE01E01FE1FE1E1FFFE01FE01E1E1E1E00000000000000000F0F000FF0F00FF00F00F00FF0FFFF0F0F000FF0FF000F0F0F0F0F0000F000F00F00F000FFF0F00FFF00F0F00FFF0F0F000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001E1E01FFE1E01E01FE1FFE01E01FE1FE1E1FFFE01FE01E1E1E1E00000000000000000F0F000FF0F00FF00F00F00FF0FFFF0F0F000FF0FF000F0F0F0F0F0000F000F00F00F000FFF0F00FFF00F0F00FFF0F0F000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001E1E01FFE1E01E01FE1FFE01E01FE1FE1E1FFFE01FE01E1E1E1E00000000000000000F0F000FF0F00FF00F00F00FF0FFFF0F0F000FF0FF000F0F0F0F0F0000F000F00F00F000FFF0F00FFF00F0F00FFF0F0F000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001E1E01FFE1E01E01FE1FFE01E01FE1FE1E1FFFE01FE01E1E1E1E00000000000000000F0F000FF0F00FF00F00F00FF0FFFF0F0F000FF0FF000F0F0F0F0F0000F000F00F00F000FFF0F00FFF00F0F00FFF0F0F000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001E1E01FFE1E01E01FE1FFE01E01FE1FE1E1FFFE01FE01E1E1E1E00000000000000000F0F000FF0F00FF00F00F00FF0FFFF0F0F000FF0FF000F0F0F0F0F0000F000F00F00F000FFF0F00FFF00F0F00FFF0F0F000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001E1E01FFE1E01E01FE1FFE01E01FE1FE1E1FFFE01FE01E1E1E1E00000000000000000F0F000FF0F00FF00F00F00FF0FFFF0F0F000FF0FF000F0F0F0F0F0000F000F00F00F000FFF0F00FFF00F0F00FFF0F0F000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001E1E01FFE1E01E01FE1FFE01E01FE1FE1E1FFFE01FE01E1E1E1E00000000000000000F0F000FF0F00FF00F00F00FF0FFFF0F0F000FF0FF000F0F0F0F0F0000F000F00F00F000FFF0F00FFF00F0F00FFF0F0F000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001E1E01FFE1E01E01FE1FFE01E01FE1FE1E1FFFE01FE01E1E1E1E00000000000000000F0F000FF0F00FF00F00F00FF0FFFF0F0F000FF0FF000F0F0F0F0F0000F000F00F00F000FFF0F00FFF00F0F00FFF0F0F000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001E1E01FFE1E01E01FE1FFE01E01FE1FE1E1FFFE01FE01E1E1E1E00000000000000000F0F000FF0F00FF00F00F00FF0FFFF0F0F000FF0FF000F0F0F0F0F0000F000F00F00F000FFF0F00FFF00F0F00FFF0F0F000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001E1E01FFE1E01E01FE1FFE01E01FE1FE1E1FFFE01FE01E1E1E1E00000000000000000F0F000FF0F00FF00F00F00FF0FFFF0F0F000FF0FF000F0F0F0F0F0000F000F00F00F000FFF0F00FFF00F0F00FFF0F0F000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001E1E01FFE1E01E01FE1FFE01E01FE1FE1E1FFFE01FE01E1E1E1E00000000000000000F0F000FF0F00FF00F00F00FF0FFFF0F0F000FF0FF000F0F0F0F0F0000F000F00F00F000FFF0F00FFF00F0F00FFF0F0F000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001E1E01FFE1E01E01FE1FFE01E01FE1FE1E1FFFE01FE01E1E1E1E00000000000000000F0F000FF0F00FF00F00F00FF0FFFF0F0F000FF0FF000F0F0F0F0F0000F000F00F00F000FFF0F00FFF00F0F00FFF0F0F000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001E1E01FFE1E01E01FE1FFE01E01FE1FE1E1FFFE01FE01E1E1E1E00000000000000000F0F000FF0F00FF00F00F00FF0FFFF0F0F000FF0FF000F0F0F0F0F0000F000F00F00F000FFF0F00FFF00F0F00FFF0F0F000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001E1E01FFE1E01E01FE1FFE01E01FE1FE1E1FFFE01FE01E1E1E1E00000000Line truncated
|
||||
^XZ
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,4 @@
|
||||
^XA^PW2400^LL600^LH0,0^CI28
|
||||
^FO192,71^GFA,99964,99964,268,FF803FE0000003FFFF007FC0000FFFFC0000FF80001FF00003FFFF007FFFFFFFF803FE00FF8000000FFFFC01FFFF8000000FF803FE00FF803FE00FF8000000007FC0000007FC0000FF80001FF0000001FFFFFFC01FF00003FFFFFF80001FF007FC0000FFFFFFE00FF803FE0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FF803FE00007FFFFFF007FC0000FF80001FFFF803FFFFFF80001FF00003FFFF007FFFE00FF803FFFFFFFFC0000FFFFC0000FF803FE00FF803FE00000000000000000000FF803FE0000003FFFF007FC0000FFFFC0000FF80001FF00003FFFF007FFFFFFFF803FE00FF8000000FFFFC01FFFF8000000FF803FE00FF803FE00FF8000000007FC0000007FC0000FF80001FF0000001FFFFFFC01FF00003FFFFFF80001FF007FC0000FFFFFFE00FF803FE0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FF803FE00007FFFFFF007FC0000FF80001FFFF803FFFFFF80001FF00003FFFF007FFFE00FF803FFFFFFFFC0000FFFFC0000FF803FE00FF803FE00000000000000000000FF803FE0000003FFFF007FC0000FFFFC0000FF80001FF00003FFFF007FFFFFFFF803FE00FF8000000FFFFC01FFFF8000000FF803FE00FF803FE00FF8000000007FC0000007FC0000FF80001FF0000001FFFFFFC01FF00003FFFFFF80001FF007FC0000FFFFFFE00FF803FE0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FF803FE00007FFFFFF007FC0000FF80001FFFF803FFFFFF80001FF00003FFFF007FFFE00FF803FFFFFFFFC0000FFFFC0000FF803FE00FF803FE00000000000000000000FF803FE0000003FFFF007FC0000FFFFC0000FF80001FF00003FFFF007FFFFFFFF803FE00FF8000000FFFFC01FFFF8000000FF803FE00FF803FE00FF8000000007FC0000007FC0000FF80001FF0000001FFFFFFC01FF00003FFFFFF80001FF007FC0000FFFFFFE00FF803FE0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FF803FE00007FFFFFF007FC0000FF80001FFFF803FFFFFF80001FF00003FFFF007FFFE00FF803FFFFFFFFC0000FFFFC0000FF803FE00FF803FE00000000000000000000FF803FE0000003FFFF007FC0000FFFFC0000FF80001FF00003FFFF007FFFFFFFF803FE00FF8000000FFFFC01FFFF8000000FF803FE00FF803FE00FF8000000007FC0000007FC0000FF80001FF0000001FFFFFFC01FF00003FFFFFF80001FF007FC0000FFFFFFE00FF803FE0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FF803FE00007FFFFFF007FC0000FF80001FFFF803FFFFFF80001FF00003FFFF007FFFE00FF803FFFFFFFFC0000FFFFC0000FF803FE00FF803FE00000000000000000000FF803FE0000003FFFF007FC0000FFFFC0000FF80001FF00003FFFF007FFFFFFFF803FE00FF8000000FFFFC01FFFF8000000FF803FE00FF803FE00FF8000000007FC0000007FC0000FF80001FF0000001FFFFFFC01FF00003FFFFFF80001FF007FC0000FFFFFFE00FF803FE0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FF803FE00007FFFFFF007FC0000FF80001FFFF803FFFFFF80001FF00003FFFF007FFFE00FF803FFFFFFFFC0000FFFFC0000FF803FE00FF803FE00000000000000000000FF803FE0000003FFFF007FC0000FFFFC0000FF80001FF00003FFFF007FFFFFFFF803FE00FF8000000FFFFC01FFFF8000000FF803FE00FF803FE00FF8000000007FC0000007FC0000FF80001FF0000001FFFFFFC01FF00003FFFFFF80001FF007FC0000FFFFFFE00FF803FE0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FF803FE00007FFFFFF007FC0000FF80001FFFF803FFFFFF80001FF00003FFFF007FFFE00FF803FFFFFFFFC0000FFFFC0000FF803FE00FF803FE00000000000000000000FF803FE0000003FFFF007FC0000FFFFC0000FF80001FF00003FFFF007FFFFFFFF803FE00FF8000000FFFFC01FFFF8000000FF803FE00FF803FE00FF8000000007FC0000007FC0000FF80001FF0000001FFFFFFC01FF00003FFFFFF80001FF007FC0000FFFFFFE00FF803FE0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FF803FE00007FFFFFF007FC0000FF80001FFFF803FFFFFF80001FF00003FFFF007FFFE00FF803FFFFFFFFC0000FFFFC0000FF803FE00FF803FE00000000000000000000FF803FE0000003FFFF007FC0000FFFFC0000FF80001FF00003FFFF007FFFFFFFF803FE00FF8000000FFFFC01FFFF8000000FF803FE00FF803FE00FF8000000007FC0000007FC0000FF80001FF0000001FFFFFFC01FF00003FFFFFF80001FF007FC0000FFFFFFE00FF803FE0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FF803FE00007FFFFFF007FC0000FF80001FFFF803FFFFFF80001FF00003FFFF007FFFE00FF803FFFFFFFFC0000FFFFC0000FF803FE00FF803FE00000000000000000000FF803FE0000003FFFF007FC0000FFFFC0000FF80001FF00003FFFF007FFFFFFFF803FE00FF8000000FFFFC01FFFF8000000FF803FE00FF803FE00FF8000000007FC0000007FC0000FLine truncated
|
||||
^FO192,444^GFA,22780,22780,268,FF803FE0000003FFFF007FC0000FFFFC0000FF80001FF00003FFFF007FFFFFFFF803FE00FF8000000FFFFC01FFFF8000000FF803FE00FF803FE00FF8000000007FC0000007FC0000FF80001FF0000001FFFFFFC01FF00003FFFFFF80001FF007FC0000FFFFFFE00FF803FE0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FF803FE00007FFFFFF007FC0000FF80001FFFF803FFFFFF80001FF00003FFFF007FFFE00FF803FFFFFFFFC0000FFFFC0000FF803FE00FF803FE00000000000000000000FF803FE0000003FFFF007FC0000FFFFC0000FF80001FF00003FFFF007FFFFFFFF803FE00FF8000000FFFFC01FFFF8000000FF803FE00FF803FE00FF8000000007FC0000007FC0000FF80001FF0000001FFFFFFC01FF00003FFFFFF80001FF007FC0000FFFFFFE00FF803FE0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FF803FE00007FFFFFF007FC0000FF80001FFFF803FFFFFF80001FF00003FFFF007FFFE00FF803FFFFFFFFC0000FFFFC0000FF803FE00FF803FE00000000000000000000FF803FE0000003FFFF007FC0000FFFFC0000FF80001FF00003FFFF007FFFFFFFF803FE00FF8000000FFFFC01FFFF8000000FF803FE00FF803FE00FF8000000007FC0000007FC0000FF80001FF0000001FFFFFFC01FF00003FFFFFF80001FF007FC0000FFFFFFE00FF803FE0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FF803FE00007FFFFFF007FC0000FF80001FFFF803FFFFFF80001FF00003FFFF007FFFE00FF803FFFFFFFFC0000FFFFC0000FF803FE00FF803FE00000000000000000000FF803FE0000003FFFF007FC0000FFFFC0000FF80001FF00003FFFF007FFFFFFFF803FE00FF8000000FFFFC01FFFF8000000FF803FE00FF803FE00FF8000000007FC0000007FC0000FF80001FF0000001FFFFFFC01FF00003FFFFFF80001FF007FC0000FFFFFFE00FF803FE0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FF803FE00007FFFFFF007FC0000FF80001FFFF803FFFFFF80001FF00003FFFF007FFFE00FF803FFFFFFFFC0000FFFFC0000FF803FE00FF803FE00000000000000000000FF803FE0000003FFFF007FC0000FFFFC0000FF80001FF00003FFFF007FFFFFFFF803FE00FF8000000FFFFC01FFFF8000000FF803FE00FF803FE00FF8000000007FC0000007FC0000FF80001FF0000001FFFFFFC01FF00003FFFFFF80001FF007FC0000FFFFFFE00FF803FE0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FF803FE0000003FFFF007FC0000FFFFC0000FF80001FF00003FFFF007FFFFFFFF803FE00FF8000000FFFFC01FFFF8000000FF803FE00FF803FE00FF8000000007FC0000007FC0000FF80001FF0000001FFFFFFC01FF00003FFFFFF80001FF007FC0000FFFFFFE00FF803FE0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FF803FE0000003FFFF007FC0000FFFFC0000FF80001FF00003FFFF007FFFFFFFF803FE00FF8000000FFFFC01FFFF8000000FF803FE00FF803FE00FF8000000007FC0000007FC0000FF80001FF0000001FFFFFFC01FF00003FFFFFF80001FF007FC0000FFFFFFE00FF803FE0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FF803FE0000003FFFF007FC0000FFFFC0000FF80001FF00003FFFF007FFFFFFFF803FE00FF8000000FFFFC01FFFF8000000FF803FE00FF803FE00FF8000000007FC0000007FC0000FF80001FF0000001FFFFFFC01FF00003FFFFFF80001FF007FC0000FFFFFFE00FF803FE0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FF803FE0000003FFFF007FC0000FFFFC0000FF80001FF00003FFFF007FFFFFFFF803FE00FF8000000FFFFC01FFFF8000000FF803FE00FF803FE00FF8000000007FC0000007FC0000FF80001FF0000001FFFFFFC01FF00003FFFFFF80001FF007FC0000FFFFFFE00FF803FE0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FF803FE0000003FFFF007FC0000FFFFC0000FF80001FF00003FFFF007FFFFFFFF803FE00FF8000000FFFFC01FFFF8000000FF803FE00FF803FE00FF8000000007FC0000007FC0000Line truncated
|
||||
^XZ
|
||||