From f9a5def4f64bec3602c046d9e4866e94c741a6db Mon Sep 17 00:00:00 2001 From: James Maes Date: Thu, 12 Nov 2026 14:48:44 -0600 Subject: [PATCH 1/2] feat(renderer): scene 2 barcodes: whole-dot module fit, alignment in the box, decode tests (P3a) A `barcode` element of a version 2 scene is now drawn into the picture of the label instead of being refused as RENDERER_TOO_OLD. - ModuleFit: the module is the largest whole number of dots at which the symbol, quiet zones included, fits its box; never under `minModule` (default 0.25 mm linear, 0.375 mm two-dimensional, taken up to whole dots) and never over `maxModule`. Integer arithmetic only. - A linear symbol is fitted along its width and its bars take the whole height of the box; qr, datamatrix, pdf417 and aztec are fitted on both axes. The fitted symbol is placed by `align` and `valign`, turned by `rotation`, and paints its own white ground, quiet zones included. - A value that does not fit at the minimum module is BARCODE_DOES_NOT_FIT, validated or rendered, with the modules and dots it needs and the dots the box has. A smaller module is drawn only when the scene states one, and then with the warning BARCODE_MODULE_BELOW_RECOMMENDED every time. - The thirteen fit-qualified symbologies, `value` and `valuePath`, the dynamic symbology allowlist and the scanner profile are carried over from version 1, whose own barcode path is not changed. Every refusal is a diagnostic of numbers; what an encoder says about a value is dropped. - `gs1` and `hri` stay declared; an element that states either is refused as RENDERER_TOO_OLD until the package that draws them. Tests: every symbology at 203, 300 and 600 dpi for a short, a typical and the longest value, rebuilt from the emitted ZPL and decoded by ZXing (GS1-128 with FNC1 as `]C1`); seeded properties of the fit and of the picture; goldens for a GS1-128 on 4 x 1 in, linear and two-dimensional symbols; the version 2 mutation campaign extended to barcode elements. Co-Authored-By: Claude Fable 5.2 --- docs/design/LABEL-LAYOUT-FONTS-PREVIEW.md | 38 + docs/security/fuzzing.md | 25 +- renderer/BARCODE-FIT.md | 2 + renderer/README.md | 8 +- renderer/schema/zpl-scene-2.schema.json | 25 +- .../scene2/barcode/BarcodeElement.java | 511 ++++++++++++ .../scene2/barcode/BarcodeFactory.java | 20 +- .../renderer/scene2/barcode/ModuleFit.java | 143 ++++ .../qbits/renderer/MutationCampaignTest.java | 121 ++- .../qbits/renderer/scene2/Scene2Test.java | 14 +- .../scene2/barcode/BarcodeDecodeTest.java | 324 ++++++++ .../scene2/barcode/BarcodeElementTest.java | 735 ++++++++++++++++++ .../scene2/barcode/BarcodeFixture.java | 290 +++++++ .../scene2/barcode/BarcodePropertyTest.java | 208 +++++ .../scene2/barcode/ModuleFitTest.java | 195 +++++ .../resources/scene2/cases/barcode-gs1.json | 11 + .../scene2/cases/barcode-linear.json | 16 + .../scene2/cases/barcode-matrix.json | 12 + .../scene2/golden/barcode-gs1-203.png | Bin 0 -> 271 bytes .../scene2/golden/barcode-gs1-203.zpl | 4 + .../scene2/golden/barcode-gs1-300.png | Bin 0 -> 487 bytes .../scene2/golden/barcode-gs1-300.zpl | 4 + .../scene2/golden/barcode-gs1-600.png | Bin 0 -> 1242 bytes .../scene2/golden/barcode-gs1-600.zpl | 4 + .../scene2/golden/barcode-linear-203.png | Bin 0 -> 295 bytes .../scene2/golden/barcode-linear-203.zpl | 3 + .../scene2/golden/barcode-linear-300.png | Bin 0 -> 417 bytes .../scene2/golden/barcode-linear-300.zpl | 4 + .../scene2/golden/barcode-linear-600.png | Bin 0 -> 1101 bytes .../scene2/golden/barcode-linear-600.zpl | 4 + .../scene2/golden/barcode-matrix-203.png | Bin 0 -> 622 bytes .../scene2/golden/barcode-matrix-203.zpl | 3 + .../scene2/golden/barcode-matrix-300.png | Bin 0 -> 772 bytes .../scene2/golden/barcode-matrix-300.zpl | 3 + .../scene2/golden/barcode-matrix-600.png | Bin 0 -> 1372 bytes .../scene2/golden/barcode-matrix-600.zpl | 3 + 36 files changed, 2689 insertions(+), 41 deletions(-) create mode 100644 renderer/src/main/java/com/kingsrook/qbits/renderer/scene2/barcode/BarcodeElement.java create mode 100644 renderer/src/main/java/com/kingsrook/qbits/renderer/scene2/barcode/ModuleFit.java create mode 100644 renderer/src/test/java/com/kingsrook/qbits/renderer/scene2/barcode/BarcodeDecodeTest.java create mode 100644 renderer/src/test/java/com/kingsrook/qbits/renderer/scene2/barcode/BarcodeElementTest.java create mode 100644 renderer/src/test/java/com/kingsrook/qbits/renderer/scene2/barcode/BarcodeFixture.java create mode 100644 renderer/src/test/java/com/kingsrook/qbits/renderer/scene2/barcode/BarcodePropertyTest.java create mode 100644 renderer/src/test/java/com/kingsrook/qbits/renderer/scene2/barcode/ModuleFitTest.java create mode 100644 renderer/src/test/resources/scene2/cases/barcode-gs1.json create mode 100644 renderer/src/test/resources/scene2/cases/barcode-linear.json create mode 100644 renderer/src/test/resources/scene2/cases/barcode-matrix.json create mode 100644 renderer/src/test/resources/scene2/golden/barcode-gs1-203.png create mode 100644 renderer/src/test/resources/scene2/golden/barcode-gs1-203.zpl create mode 100644 renderer/src/test/resources/scene2/golden/barcode-gs1-300.png create mode 100644 renderer/src/test/resources/scene2/golden/barcode-gs1-300.zpl create mode 100644 renderer/src/test/resources/scene2/golden/barcode-gs1-600.png create mode 100644 renderer/src/test/resources/scene2/golden/barcode-gs1-600.zpl create mode 100644 renderer/src/test/resources/scene2/golden/barcode-linear-203.png create mode 100644 renderer/src/test/resources/scene2/golden/barcode-linear-203.zpl create mode 100644 renderer/src/test/resources/scene2/golden/barcode-linear-300.png create mode 100644 renderer/src/test/resources/scene2/golden/barcode-linear-300.zpl create mode 100644 renderer/src/test/resources/scene2/golden/barcode-linear-600.png create mode 100644 renderer/src/test/resources/scene2/golden/barcode-linear-600.zpl create mode 100644 renderer/src/test/resources/scene2/golden/barcode-matrix-203.png create mode 100644 renderer/src/test/resources/scene2/golden/barcode-matrix-203.zpl create mode 100644 renderer/src/test/resources/scene2/golden/barcode-matrix-300.png create mode 100644 renderer/src/test/resources/scene2/golden/barcode-matrix-300.zpl create mode 100644 renderer/src/test/resources/scene2/golden/barcode-matrix-600.png create mode 100644 renderer/src/test/resources/scene2/golden/barcode-matrix-600.zpl diff --git a/docs/design/LABEL-LAYOUT-FONTS-PREVIEW.md b/docs/design/LABEL-LAYOUT-FONTS-PREVIEW.md index cd3905e..c8e3b19 100644 --- a/docs/design/LABEL-LAYOUT-FONTS-PREVIEW.md +++ b/docs/design/LABEL-LAYOUT-FONTS-PREVIEW.md @@ -501,6 +501,44 @@ 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. +- **`QUIET_ZONE_INTRUDED` is not raised yet.** It needs the finished picture, after every element has painted; + the element contract has no such step and validation paints nothing. Ink painted over a symbol by a later + element is therefore not reported. It belongs with `ELEMENTS_OVERLAP`. +- **Refusals that depend on the record.** No code was added to the vocabulary. A bound value, symbology or + scanner profile that is absent is `VALUE_MISSING` with the pointer of the binding property. A value the + symbology cannot hold is `GS1_INVALID` for `gs1-128` and `SCENE_INVALID` at `value` or `valuePath` otherwise, + with a count of characters; what the encoder says about the value is dropped. A symbology outside + `allowedSymbologies`, and a scanner profile that does not name the symbology drawn, are `SCENE_INVALID` at that + property. A code of its own for "the record's value cannot be encoded" would read better to an operator. +- **A bound barcode is never proven at publish.** The design gives a barcode no `maxLength`, so validation + cannot fit a bound value and the element always carries `FIT_NOT_PROVEN`. A literal value in a stated + symbology is fitted when the scene is validated. +- **The numbers of a refusal.** `BARCODE_DOES_NOT_FIT` carries `measured.modules`, `quietModules`, `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. No minimum bar height is enforced: the design names none. + ## 5. Graphics - **Image**: an asset (PNG or JPEG, as today) in a physical box. `fit` is `contain` (scale to the box, keep the diff --git a/docs/security/fuzzing.md b/docs/security/fuzzing.md index 6603cfd..e4442d2 100644 --- a/docs/security/fuzzing.md +++ b/docs/security/fuzzing.md @@ -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 diff --git a/renderer/BARCODE-FIT.md b/renderer/BARCODE-FIT.md index 66705d8..d6a4c17 100644 --- a/renderer/BARCODE-FIT.md +++ b/renderer/BARCODE-FIT.md @@ -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. diff --git a/renderer/README.md b/renderer/README.md index bad950d..ca5f82f 100644 --- a/renderer/README.md +++ b/renderer/README.md @@ -129,7 +129,13 @@ 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 `quiet` blank modules on each side (default and least: 10 for a linear symbol, 4 for a two-dimensional one; most: 64). It is never under `minModule` (default `0.25mm` linear, `0.375mm` two-dimensional; taken up to whole dots that is 2 and 3 dots at 203 dpi, 3 and 5 at 300 dpi, 6 and 9 at 600 dpi) and never over `maxModule`, which goes up to a whole dot by the same rule. A linear symbol is fitted along its width and its bars take the whole height of the box; `qr`, `datamatrix`, `pdf417` and `aztec` are fitted on both axes. Example: a 44-character GS1-128 is 319 modules with its quiet zones; in a box of `90mm` at 203 dpi (719 dots) the module is `719 / 319 = 2` dots (0.25 mm), the symbol 638 dots, and centred it leaves 40 and 41 dots. + - **Place.** The fitted symbol sits in the box by `align` and `valign` (default `left`, `top`), the odd dot to the right and below, and paints its own white ground, quiet zones included, so ink under 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. + - **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.quietModules`, `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`. 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 4000 characters is `VALUE_TOO_LONG`. A value the symbology cannot hold is `GS1_INVALID` for `gs1-128` and `SCENE_INVALID` at `value` or `valuePath` otherwise, with `measured.characters` and never the value; 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. Validation reads no record, so anything bound is not fitted then and warns `FIT_NOT_PROVEN`; a literal value in a stated symbology is fitted when the scene is validated. + - **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. diff --git a/renderer/schema/zpl-scene-2.schema.json b/renderer/schema/zpl-scene-2.schema.json index e7a74bc..8e51b94 100644 --- a/renderer/schema/zpl-scene-2.schema.json +++ b/renderer/schema/zpl-scene-2.schema.json @@ -126,10 +126,29 @@ }, { "$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; a two-dimensional symbol (`qr`, `datamatrix`, `pdf417`, `aztec`) is fitted on both axes. The fitted symbol is placed in the box by `align` and `valign`, and paints its own white ground, quiet zones included. The renderer is the authority: it also refuses a value the symbology cannot hold (GS1_INVALID for `gs1-128`, SCENE_INVALID at the value otherwise, with a count of characters and never the value), a quiet zone under 10 when any symbology the element may draw is linear, and a maximum module under the minimum. A value, symbology or scanner profile bound to the record is not known when a scene is validated: the warning FIT_NOT_PROVEN says so.", "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. 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."}, + "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 4000 characters VALUE_TOO_LONG. No record is read when a scene is validated."}, + "minModule": {"$ref": "#/$defs/absolute", "description": "The smallest module, more than nothing, taken up to a whole dot. Default: 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). A stated minimum that is fewer dots than the default 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 and least: 10 for a linear symbol, 4 for a two-dimensional one."}, + "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: a retail value arrives with its check digit, which is verified."}, + "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", diff --git a/renderer/src/main/java/com/kingsrook/qbits/renderer/scene2/barcode/BarcodeElement.java b/renderer/src/main/java/com/kingsrook/qbits/renderer/scene2/barcode/BarcodeElement.java new file mode 100644 index 0000000..a96de55 --- /dev/null +++ b/renderer/src/main/java/com/kingsrook/qbits/renderer/scene2/barcode/BarcodeElement.java @@ -0,0 +1,511 @@ +/* + * 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.BarcodeFit; +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.Refusal; +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 + ** symbol as modules; `ModuleFit` chooses the module, a whole number of dots; + ** the symbol, quiet zones included, is placed in the box by `align` and + ** `valign` and painted as a white ground and black bars, so that nothing + ** painted under it can close a space or a quiet zone. + ** + ** 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. + ** + ** 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. + ** + ** Nothing bound to the record is known when a scene is validated, so + ** validation reads no record: it fits a barcode whose symbology and value are + ** both literal, and warns `FIT_NOT_PROVEN` for any other. + *******************************************************************************/ +final class BarcodeElement implements SceneElement +{ + static final int MAXIMUM_CHARACTERS = 4000; + static final int MAXIMUM_POINTER = 256; + + private static final int MICROMETRES_PER_INCH = 25_400; + + private final String id; + private final Placement placement; + private final int rotation; + private final List 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 Integer quiet; + private final ElementSource source; + + + + /*************************************************************************** + ** One run of ink in a symbol, in modules: `length` modules from column + ** `column` of row `row`. + ***************************************************************************/ + private record Run(int column, int row, int length) + { + } + + + + /*************************************************************************** + ** A fitted symbol on the label, in dots: the area it takes, quiet zones + ** included, and its bars. + ***************************************************************************/ + private record Symbol(Frame area, List 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; + + 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(!stated.isTextual() || !BarcodeFit.TYPES.contains(stated.textValue())) + { + throw source.invalid("symbology"); + } + if(allowed != null) + { + throw source.invalid("allowedSymbologies"); + } + symbologies = List.of(stated.textValue()); + } + else + { + if(allowed == null || !allowed.isArray() || allowed.isEmpty()) + { + throw source.invalid("allowedSymbologies"); + } + List listed = new ArrayList<>(); + for(JsonNode entry : allowed) + { + if(!entry.isTextual() || !BarcodeFit.TYPES.contains(entry.textValue()) || listed.contains(entry.textValue())) + { + throw source.invalid("allowedSymbologies"); + } + listed.add(entry.textValue()); + } + 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 = module(source, "minModule"); + maxModule = module(source, "maxModule"); + JsonNode zone = source.value("quiet"); + int fewest = symbologies.stream().mapToInt(ModuleFit::quiet).max().orElseThrow(); + if(zone != null && (!zone.isIntegralNumber() || !zone.canConvertToInt() || zone.intValue() < fewest || zone.intValue() > ModuleFit.MAXIMUM_QUIET_MODULES)) + { + throw source.invalid("quiet"); + } + quiet = zone == null ? null : Integer.valueOf(zone.intValue()); + } + + + + /*************************************************************************** + ** 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, more than nothing, or null when the element states none. + ***************************************************************************/ + private static Length module(ElementSource source, String name) + { + Length stated = source.length(name, null, ElementSource.ABSOLUTE); + if(stated != null && stated.thousandths() <= 0) + { + throw source.invalid(name); + } + return (stated); + } + + + + /*************************************************************************** + ** 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); + } + + + + /*************************************************************************** + ** 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 something bound to the record is only being + ** validated. + ***************************************************************************/ + private Symbol compose(Frame frame, RenderInput input, Diagnostics diagnostics) + { + int dpi = input.targetDpi == null ? input.stock.dpi() : input.targetDpi; + boolean bound = symbologyPath != null || valuePath != null || scannerPath != null; + if(bound) + { + diagnostics.warn("FIT_NOT_PROVEN", id, null, frame, null, null); + } + if(bound && "VALIDATE".equals(input.operation)) + { + limits(symbologies, dpi, frame, diagnostics); + return (null); + } + + String symbology = symbology(frame, input, diagnostics); + int[] limits = limits(List.of(symbology), dpi, frame, diagnostics); + String payload = payload(symbology, frame, input, diagnostics); + int zone = quiet == null ? ModuleFit.quiet(symbology) : quiet; + BufferedImage modules; + try + { + modules = Barcodes.renderForFormat("ZPL", symbology, payload, 1, zone, Wire.JSON.createObjectNode()); + } + catch(IllegalArgumentException | OkapiInputException rejected) + { + ////////////////////////////////////////////////////////////////////////////////////// + // the encoder cannot draw this value in this symbology. What it says may quote the // + // value, so nothing of it is kept: the refusal is the count of characters // + ////////////////////////////////////////////////////////////////////////////////////// + throw unusable("gs1-128".equals(symbology) ? "GS1_INVALID" : "SCENE_INVALID", payload, frame, diagnostics); + } + + boolean matrix = ModuleFit.matrix(symbology); + boolean upright = rotation % 180 == 0; + ModuleFit.Request request = new ModuleFit.Request(modules.getWidth(), matrix ? modules.getHeight() : 0, upright ? frame.width() : frame.height(), upright ? frame.height() : frame.width(), + limits[0], limits[1]); + ModuleFit.Fit fit = ModuleFit.fit(request); + Map measured = Map.of("modules", request.columns(), "quietModules", zone, "moduleDots", fit.module(), "widthDots", fit.width(), "heightDots", fit.height(), + "moduleMicrometres", Math.toIntExact(((long) fit.module() * MICROMETRES_PER_INCH + dpi / 2) / dpi)); + Map 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); + } + diagnostics.inform("BARCODE_MODULE", id, null, frame, measured, limit); + + Frame area = Layout.align(frame, upright ? fit.width() : fit.height(), upright ? fit.height() : fit.width(), placement.align(), placement.valign()); + List bars = new ArrayList<>(); + for(Run run : runs(modules, matrix)) + { + int x = run.column() * fit.module(); + int y = run.row() * fit.module(); + int width = run.length() * fit.module(); + int height = matrix ? fit.module() : fit.height(); + bars.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, bars)); + } + + + + /*************************************************************************** + ** The smallest and the largest module the scene allows at this resolution, + ** in dots, for the symbologies that may be drawn. A minimum the scene + ** states below the default 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 int[] limits(List drawn, int dpi, Frame frame, Diagnostics diagnostics) + { + int recommended = drawn.stream().mapToInt(symbology -> ModuleFit.recommended(symbology, 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)); + } + int maximum = maxModule == null ? Integer.MAX_VALUE : maxModule.module(dpi); + if(maximum < minimum) + { + throw diagnostics.refuse("SCENE_INVALID", id, pointer("maxModule"), frame, Map.of("maxModuleDots", maximum), Map.of("minModuleDots", minimum)); + } + return (new int[] {minimum, maximum}); + } + + + + /*************************************************************************** + ** The symbology to draw: the one the scene states, or the one the record + ** names, which must be one the scene allows. + ***************************************************************************/ + private String 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); + } + if(!symbologies.contains(named.textValue())) + { + throw diagnostics.refuse("SCENE_INVALID", id, pointer("symbologyPath"), frame, null, null); + } + return (named.textValue()); + } + + + + /*************************************************************************** + ** What the encoder is given: the literal, or the text, number or true/false + ** of the record as JSON writes it, held to the scanner profile when the + ** scene names one. An element string built from `gs1` entries would enter + ** here. + ***************************************************************************/ + private String payload(String symbology, 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(); + } + if(text.length() > MAXIMUM_CHARACTERS) + { + throw diagnostics.refuse("VALUE_TOO_LONG", id, null, frame, Map.of("characters", text.length()), Map.of("characters", MAXIMUM_CHARACTERS)); + } + if(scannerPath == null) + { + return (text); + } + 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, 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 unusable("SCENE_INVALID", text, frame, diagnostics); + } + } + + + + /*************************************************************************** + ** The refusal of a value this barcode cannot hold, at the property that + ** gives the value, with only how many characters it has. + ***************************************************************************/ + private Refusal unusable(String code, String text, Frame frame, Diagnostics diagnostics) + { + return (diagnostics.refuse(code, id, pointer(valuePath == null ? "value" : "valuePath"), frame, Map.of("characters", text.length()), null)); + } + + + + /*************************************************************************** + ** The ink of a symbol the encoder drew at one dot to the module. 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. + ***************************************************************************/ + private static List runs(BufferedImage modules, boolean matrix) + { + List runs = new ArrayList<>(); + int first = matrix ? 0 : modules.getHeight() / 2; + int last = matrix ? modules.getHeight() - 1 : first; + for(int row = first; row <= last; row++) + { + int start = -1; + for(int column = 0; column <= modules.getWidth(); column++) + { + boolean ink = column < modules.getWidth() && (modules.getRGB(column, row) & 0xFFFFFF) == 0; + if(ink && start < 0) + { + start = column; + } + if(!ink && start >= 0) + { + runs.add(new Run(start, matrix ? row : 0, column - start)); + start = -1; + } + } + } + return (runs); + } +} diff --git a/renderer/src/main/java/com/kingsrook/qbits/renderer/scene2/barcode/BarcodeFactory.java b/renderer/src/main/java/com/kingsrook/qbits/renderer/scene2/barcode/BarcodeFactory.java index 768056f..4c6e4cc 100644 --- a/renderer/src/main/java/com/kingsrook/qbits/renderer/scene2/barcode/BarcodeFactory.java +++ b/renderer/src/main/java/com/kingsrook/qbits/renderer/scene2/barcode/BarcodeFactory.java @@ -30,17 +30,17 @@ 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`. ** - ** 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 properties() @@ -51,11 +51,15 @@ public final class BarcodeFactory implements ElementFactory /*************************************************************************** - ** 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)); } } diff --git a/renderer/src/main/java/com/kingsrook/qbits/renderer/scene2/barcode/ModuleFit.java b/renderer/src/main/java/com/kingsrook/qbits/renderer/scene2/barcode/ModuleFit.java new file mode 100644 index 0000000..87b181a --- /dev/null +++ b/renderer/src/main/java/com/kingsrook/qbits/renderer/scene2/barcode/ModuleFit.java @@ -0,0 +1,143 @@ +/* + * 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.Set; +import com.kingsrook.qbits.renderer.scene2.Length; + + +/******************************************************************************* + ** 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 linear symbol is fitted along its width only; its bars take the whole + ** height of the box. A two-dimensional symbol is fitted on both axes. Integer + ** arithmetic only: no fraction of a dot takes part. + *******************************************************************************/ +final class ModuleFit +{ + ///////////////////////////////////////////////////////////////////////////////////////////////// + // the smallest module a symbol is drawn at unless the scene states its own `minModule`: 0.25 // + // mm for a linear symbol and 0.375 mm for a two-dimensional one. Taken up to whole dots that // + // is 2 and 3 dots at 203 dpi, 3 and 5 at 300 dpi and 6 and 9 at 600 dpi. They are the // + // proposals of the design, to be confirmed by printing and scanning before a final release // + ///////////////////////////////////////////////////////////////////////////////////////////////// + static final Length LINEAR_MINIMUM_MODULE = new Length(Length.Unit.MM, 250); + static final Length MATRIX_MINIMUM_MODULE = new Length(Length.Unit.MM, 375); + + ///////////////////////////////////////////////////////////////////////////////////////////////// + // quiet zones, in modules on each side: at least ten for a linear symbol and four for a // + // two-dimensional one, the minimums version 1 holds its encoders to, and at most 64 // + ///////////////////////////////////////////////////////////////////////////////////////////////// + static final int LINEAR_QUIET_MODULES = 10; + static final int MATRIX_QUIET_MODULES = 4; + static final int MAXIMUM_QUIET_MODULES = 64; + + private static final Set MATRIX = Set.of("qr", "datamatrix", "aztec", "pdf417"); + + + + /*************************************************************************** + ** Utility class. + ***************************************************************************/ + private ModuleFit() + { + } + + + + /*************************************************************************** + ** What is fitted: a symbol of `columns` modules across, quiet zones + ** included, and for a two-dimensional symbol `rows` modules down (0 for a + ** linear one), 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. + ***************************************************************************/ + record Request(int columns, int rows, int along, int across, int minimum, int maximum) + { + } + + + + /*************************************************************************** + ** 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. + ***************************************************************************/ + record Fit(int module, int width, int height, boolean fits) + { + } + + + + /*************************************************************************** + ** Whether a symbology is two-dimensional: fitted on both axes, with the + ** larger minimum module and the smaller quiet zone. + ***************************************************************************/ + static boolean matrix(String symbology) + { + return (MATRIX.contains(symbology)); + } + + + + /*************************************************************************** + ** The smallest module of a symbology at a resolution, in dots, when the + ** scene states none: the physical default taken up to a whole dot. + ***************************************************************************/ + static int recommended(String symbology, int dpi) + { + return ((matrix(symbology) ? MATRIX_MINIMUM_MODULE : LINEAR_MINIMUM_MODULE).module(dpi)); + } + + + + /*************************************************************************** + ** The fewest quiet modules a symbology may have on each side. + ***************************************************************************/ + static int quiet(String symbology) + { + return (matrix(symbology) ? MATRIX_QUIET_MODULES : LINEAR_QUIET_MODULES); + } + + + + /*************************************************************************** + ** 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. + ***************************************************************************/ + static Fit fit(Request request) + { + int module = Math.min(request.maximum(), request.along() / request.columns()); + if(request.rows() > 0) + { + module = Math.min(module, request.across() / request.rows()); + } + boolean fits = module >= request.minimum(); + int drawn = fits ? module : request.minimum(); + return (new Fit(module, request.columns() * drawn, request.rows() > 0 ? request.rows() * drawn : request.across(), fits)); + } +} diff --git a/renderer/src/test/java/com/kingsrook/qbits/renderer/MutationCampaignTest.java b/renderer/src/test/java/com/kingsrook/qbits/renderer/MutationCampaignTest.java index 81107b0..d03bebf 100644 --- a/renderer/src/test/java/com/kingsrook/qbits/renderer/MutationCampaignTest.java +++ b/renderer/src/test/java/com/kingsrook/qbits/renderer/MutationCampaignTest.java @@ -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 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 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 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\"", "\\\\", "\\\\<", "\"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 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") : 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,78 @@ 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 that are + ** more than nothing in a physical unit or dots; a quiet zone of at least + ** the 10 modules of a linear symbol (4 when every symbology it may draw is + ** two-dimensional) and at most 64; and a quarter-turn rotation. An element + ** that asks for `gs1` or `hri` is never accepted. + ***************************************************************************/ + 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 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)); + } + for(String module : List.of("minModule", "maxModule")) + { + 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().allMatch(Set.of("qr", "datamatrix", "pdf417", "aztec")::contains) ? 4 : 10; + 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")); } } diff --git a/renderer/src/test/java/com/kingsrook/qbits/renderer/scene2/Scene2Test.java b/renderer/src/test/java/com/kingsrook/qbits/renderer/scene2/Scene2Test.java index 4af86c4..dc695c1 100644 --- a/renderer/src/test/java/com/kingsrook/qbits/renderer/scene2/Scene2Test.java +++ b/renderer/src/test/java/com/kingsrook/qbits/renderer/scene2/Scene2Test.java @@ -508,10 +508,11 @@ class Scene2Test /*************************************************************************** - ** Barcodes and images are parsed and placed by this renderer and then + ** An image is 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. + ** example scene with a logo beside it is the case: validated, the logo is + ** its one error; rendered with an empty record, its barcode is refused + ** first, for the value the record lacks, and its caption for the same. ***************************************************************************/ @Test void anElementThisRendererCannotDrawIsRefusedByName() throws Exception @@ -523,14 +524,15 @@ class Scene2Test + "{\"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 code = Map.of("code", "RENDERER_TOO_OLD", "severity", "ERROR", "element", "code", "box", List.of(6, 4, 751, 87)); + Map code = Map.of("code", "VALUE_MISSING", "severity", "ERROR", "element", "code", "pointer", "/elements/0/valuePath", "box", List.of(6, 4, 751, 87)); Map caption = Map.of("code", "VALUE_MISSING", "severity", "ERROR", "element", "caption", "box", List.of(6, 100, 751, 51)); - assertEquals(code, refused(example)); + Map 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 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)), code, 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)), diff --git a/renderer/src/test/java/com/kingsrook/qbits/renderer/scene2/barcode/BarcodeDecodeTest.java b/renderer/src/test/java/com/kingsrook/qbits/renderer/scene2/barcode/BarcodeDecodeTest.java new file mode 100644 index 0000000..c0c574a --- /dev/null +++ b/renderer/src/test/java/com/kingsrook/qbits/renderer/scene2/barcode/BarcodeDecodeTest.java @@ -0,0 +1,324 @@ +/* + * 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. + *******************************************************************************/ +class BarcodeDecodeTest +{ + private static final String LETTERS = "TEST0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + private static final String DIGITS = "0012345678"; + private static final String GTIN = "[01]00012345678905[11]260101"; + private static final char SEPARATOR = (char) 29; + + + + /*************************************************************************** + ** One symbology: how it is read back, its box, 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 symbology, BarcodeFormat format, String width, String height, IntFunction value, UnaryOperator decoded, int fewest, int typical, int most, int step) + { + /************************************************************************ + ** The name of the profile in a test report. + ************************************************************************/ + @Override + public String toString() + { + return (symbology); + } + } + + + + /*************************************************************************** + ** 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; their check digits here were worked out by hand. + ***************************************************************************/ + private static List profiles() + { + UnaryOperator same = value -> value; + return (List.of( + new Profile("code128", BarcodeFormat.CODE_128, "90mm", "12mm", length -> repeated(LETTERS, length), same, 1, 12, 80, 1), + new Profile("code39", BarcodeFormat.CODE_39, "90mm", "12mm", length -> repeated(LETTERS, length), same, 1, 10, 80, 1), + new Profile("code93", BarcodeFormat.CODE_93, "90mm", "12mm", length -> repeated(LETTERS, length), same, 1, 10, 80, 1), + new Profile("itf", BarcodeFormat.ITF, "90mm", "12mm", length -> repeated(DIGITS, length), same, 6, 14, 120, 2), + new Profile("gs1-128", BarcodeFormat.CODE_128, "90mm", "12mm", BarcodeDecodeTest::elementString, BarcodeDecodeTest::scanned, 1, 8, 40, 1), + new Profile("ean13", BarcodeFormat.EAN_13, "40mm", "12mm", length -> "001234567890", value -> value + "5", 12, 12, 12, 1), + new Profile("ean8", BarcodeFormat.EAN_8, "40mm", "12mm", length -> "1234567", value -> value + "0", 7, 7, 7, 1), + new Profile("upca", BarcodeFormat.UPC_A, "40mm", "12mm", length -> "01234567890", value -> value + "5", 11, 11, 11, 1), + new Profile("upce", BarcodeFormat.UPC_E, "40mm", "12mm", length -> "0425261", value -> value + "4", 7, 7, 7, 1), + new Profile("qr", BarcodeFormat.QR_CODE, "40mm", "40mm", length -> repeated(LETTERS, length), same, 1, 40, 2000, 1), + new Profile("datamatrix", BarcodeFormat.DATA_MATRIX, "40mm", "40mm", length -> repeated(LETTERS, length), same, 1, 40, 2000, 1), + new Profile("pdf417", BarcodeFormat.PDF_417, "90mm", "30mm", length -> repeated(LETTERS, length), same, 1, 40, 1000, 1), + new Profile("aztec", BarcodeFormat.AZTEC, "40mm", "40mm", length -> repeated(LETTERS, length), same, 1, 40, 2000, 1))); + } + + + + /*************************************************************************** + ** A barcode in the middle of the label, centred in its box. + ***************************************************************************/ + 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); + return (element.put("anchor", "center").put("width", profile.width()).put("height", profile.height()).put("align", "center").put("valign", "middle").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 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, with the + ** quiet zones blank and the symbol centred within one dot. + ***************************************************************************/ + @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 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); + } + StringBuilder report = new StringBuilder(profile.symbology()); + 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 answer = BarcodeFixture.answer(BarcodeFixture.request("RENDER", dpi, element(profile, value), Map.of())); + Map 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 quiet = BarcodeFixture.number(chosen, "measured", "quietModules"); + 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 // + ////////////////////////////////////////////////////////////////////////////////////// + int minimum = matrix ? Map.of(203, 3, 300, 5, 600, 9).get(dpi) : Map.of(203, 2, 300, 3, 600, 6).get(dpi); + assertEquals(matrix ? 4 : 10, quiet, 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()), about + ": a larger module fits"); + assertTrue(width <= box.width() && height <= box.height(), about + ": the symbol leaves its box"); + + ////////////////////////////////////////////////////////////////////////////////////// + // the picture: ink only inside the box less the quiet zones, the symbol centred // + // 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 >= quiet * module && right >= quiet * module, about + ": quiet zones " + left + " and " + right); + boolean edged = !"aztec".equals(profile.symbology()); + assertTrue(left >= (box.width() - width) / 2 + quiet * module && (!edged || left == (box.width() - width) / 2 + quiet * module), about + ": left margin " + left); + if(matrix) + { + assertTrue(top >= quiet * module && bottom >= quiet * module, about + ": quiet zones " + top + " and " + bottom); + assertTrue(top >= (box.height() - height) / 2 + quiet * module && (!edged || top == (box.height() - height) / 2 + quiet * module), about + ": top margin " + top); + } + else + { + assertTrue(Math.abs(left - right) <= 1, about + ": margins " + left + " and " + right); + assertEquals(box.height(), ink.height(), about); + assertEquals(0, top, about); + List runs = BarcodeFixture.runs(page[(ink.top() + ink.bottom()) / 2]); + assertEquals(module, runs.stream().mapToInt(Integer::intValue).min().orElseThrow(), about); + assertEquals((modules - 2 * quiet) * 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); + } +} diff --git a/renderer/src/test/java/com/kingsrook/qbits/renderer/scene2/barcode/BarcodeElementTest.java b/renderer/src/test/java/com/kingsrook/qbits/renderer/scene2/barcode/BarcodeElementTest.java new file mode 100644 index 0000000..6519b6c --- /dev/null +++ b/renderer/src/test/java/com/kingsrook/qbits/renderer/scene2/barcode/BarcodeElementTest.java @@ -0,0 +1,735 @@ +/* + * 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.LinkedHashMap; +import java.util.List; +import java.util.Map; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.zxing.BarcodeFormat; +import com.kingsrook.qbits.renderer.BarcodeFit; +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.assertArrayEquals; +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.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + + +/******************************************************************************* + ** A `barcode` element from scene to label: what it reads, what it refuses and + ** with which numbers, where the fitted symbol sits in its box, and that the + ** label still decodes. Values are obviously fake. The label is the example + ** stock of the design, 4 x 1 in with 3 mm margins. + *******************************************************************************/ +class BarcodeElementTest +{ + //////////////////////////////////////////////////////////////////////////////////////////////// + // GS1 element strings in the brackets the encoder reads. The typical one is 44 characters as // + // a person writes it: (01)00012345678905(11)260101(21)TEST00010002 // + //////////////////////////////////////////////////////////////////////////////////////////////// + private static final String SHORT = "[01]00012345678905"; + private static final String TYPICAL = "[01]00012345678905[11]260101[21]TEST00010002"; + private static final String LONG = "[01]00012345678905[11]260101[21]TEST0001000200030004"; + private static final String SCANNED = "]C1010001234567890511260101" + "21TEST00010002"; + + + + /*************************************************************************** + ** A barcode named `code` with a literal value. + ***************************************************************************/ + private static ObjectNode barcode(String symbology, String value) + { + return (Wire.JSON.createObjectNode().put("id", "code").put("type", "barcode").put("symbology", symbology).put("value", value)); + } + + + + /*************************************************************************** + ** The same, 90 mm by 12 mm at the top of the label, centred in its box. + ***************************************************************************/ + private static ObjectNode centred(String symbology, String value, String width) + { + return (barcode(symbology, value).put("anchor", "top-center").put("width", width).put("height", "12mm").put("align", "center").put("valign", "middle")); + } + + + + /*************************************************************************** + ** A request for these elements on the example stock at a render target, + ** with a record. + ***************************************************************************/ + private static ObjectNode request(String operation, int dpi, String elements, Map record) + { + ObjectNode request = SceneFixture.request(operation, SceneFixture.scene(elements)).put("targetDpi", dpi); + request.set("context", Wire.JSON.valueToTree(Map.of("record", record))); + return (request); + } + + + + /*************************************************************************** + ** The one diagnostic a request is refused with. + ***************************************************************************/ + private static Map refused(String operation, int dpi, Object elements, Map record) + { + return (SceneFixture.refusal(() -> RendererMain.process(request(operation, dpi, elements.toString(), record)))); + } + + + + /*************************************************************************** + ** The diagnostic of a property of `code` that is not what the schema says. + ***************************************************************************/ + private static Map invalid(String property) + { + return (Map.of("code", "SCENE_INVALID", "severity", "ERROR", "element", "code", "pointer", "/elements/0/" + property)); + } + + + + /*************************************************************************** + ** A part of a page, as rows of ink. + ***************************************************************************/ + private static boolean[][] crop(boolean[][] page, BarcodeFixture.Ink ink) + { + boolean[][] part = new boolean[ink.height()][ink.width()]; + for(int y = 0; y < part.length; y++) + { + System.arraycopy(page[ink.top() + y], ink.left(), part[y], 0, part[y].length); + } + return (part); + } + + + + /*************************************************************************** + ** 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); + } + + + + /*************************************************************************** + ** The worked example of the fit rule: a 44-character GS1-128 in a box 90 mm + ** wide at 203 dpi. The box is 719 dots; the symbol is 319 modules with its + ** quiet zones; 719 / 319 is 2.25, so the module is 2 dots (0.25 mm) and the + ** symbol 638 dots. Centred, 40 dots are left blank on the left and 41 on the + ** right, and with the quiet zones the bars start 60 dots in and end 61 dots + ** short. The bars are the whole height of the box, and the label decodes to + ** the value with FNC1 read as `]C1`. + ***************************************************************************/ + @Test + void aGs1SymbolIsFittedInWholeDotsAndCentredInItsBox() throws Exception + { + Map answer = BarcodeFixture.answer(request("RENDER", 203, centred("gs1-128", TYPICAL, "90mm").toString(), Map.of())); + assertEquals(List.of(Map.of("code", "BARCODE_MODULE", "severity", "INFO", "element", "code", "box", List.of(22, 0, 719, 95), + "measured", Map.of("modules", 319, "quietModules", 10, "moduleDots", 2, "moduleMicrometres", 250, "widthDots", 638, "heightDots", 95), + "limit", Map.of("widthDots", 719, "heightDots", 95, "minModuleDots", 2))), answer.get("diagnostics")); + + boolean[][] page = BarcodeFixture.page(answer); + assertEquals(new BarcodeFixture.Ink(24 + 22 + 60, 24, 24 + 22 + 719 - 61 - 1, 24 + 94), BarcodeFixture.ink(page)); + List runs = BarcodeFixture.runs(page[24 + 47]); + assertEquals(2, runs.stream().mapToInt(Integer::intValue).min().orElseThrow()); + assertEquals(299 * 2, runs.stream().mapToInt(Integer::intValue).sum()); + assertEquals(SCANNED, BarcodeFixture.decode(page, BarcodeFormat.CODE_128, true).getText()); + + Map> modules = Map.of(300, List.of(3, 254, 957, 1062), 600, List.of(6, 254, 1914, 2125)); + for(Map.Entry> expected : modules.entrySet()) + { + Map other = BarcodeFixture.answer(request("RENDER", expected.getKey(), centred("gs1-128", TYPICAL, "90mm").toString(), Map.of())); + Map chosen = BarcodeFixture.finding(other, "BARCODE_MODULE"); + assertEquals(expected.getValue(), List.of(BarcodeFixture.number(chosen, "measured", "moduleDots"), BarcodeFixture.number(chosen, "measured", "moduleMicrometres"), + BarcodeFixture.number(chosen, "measured", "widthDots"), BarcodeFixture.number(chosen, "limit", "widthDots"))); + assertEquals(SCANNED, BarcodeFixture.decode(BarcodeFixture.page(other), BarcodeFormat.CODE_128, true).getText()); + } + } + + + + /*************************************************************************** + ** Carried over from the alignment tests of version 1: a centred symbol + ** leaves the same blank width on both sides of its box, within one dot, for + ** a short, a typical and a long value at every resolution, at the largest + ** module that fits. + ***************************************************************************/ + @Test + void aCentredSymbolLeavesEqualMarginsWithinOneDot() throws Exception + { + for(int dpi : Scene2.RESOLUTIONS) + { + for(String value : List.of(SHORT, TYPICAL, LONG)) + { + Map answer = BarcodeFixture.answer(request("RENDER", dpi, centred("gs1-128", value, "100%").toString(), Map.of())); + Map chosen = BarcodeFixture.finding(answer, "BARCODE_MODULE"); + Frame box = BarcodeFixture.box(chosen, dpi); + BarcodeFixture.Ink ink = BarcodeFixture.ink(BarcodeFixture.page(answer)); + int left = ink.left() - box.x(); + int right = box.right() - 1 - ink.right(); + int module = BarcodeFixture.number(chosen, "measured", "moduleDots"); + String about = value.length() + " characters at " + dpi + " dpi: " + left + " and " + right; + assertTrue(Math.abs(left - right) <= 1 && left >= 10 * module, about); + assertTrue(BarcodeFixture.number(chosen, "measured", "modules") * (module + 1) > box.width(), about); + assertEquals(box.height(), ink.height(), about); + } + } + } + + + + /*************************************************************************** + ** Carried over as well: with no `align` the symbol starts at the origin of + ** its box, as every element does; `right` and `bottom` put it against the + ** far edges; `center` and `middle` split what is left over, the odd dot to + ** the right and below. A two-dimensional symbol shows both axes: a 29 + ** module QR code in a 20 mm by 12 mm box at 203 dpi is 3 dots to the + ** module, 87 dots square in a box of 159 by 95. + ***************************************************************************/ + @Test + void alignmentPlacesTheSymbolInItsBox() throws Exception + { + Map> corners = new LinkedHashMap<>(); + corners.put("", List.of(12, 12)); + corners.put("left top", List.of(12, 12)); + corners.put("center middle", List.of(36 + 12, 4 + 12)); + corners.put("right bottom", List.of(72 + 12, 8 + 12)); + corners.put("right top", List.of(72 + 12, 12)); + corners.put("left bottom", List.of(12, 8 + 12)); + for(Map.Entry> corner : corners.entrySet()) + { + ObjectNode element = barcode("qr", "TEST-0001").put("x", "10mm").put("y", "2mm").put("width", "20mm").put("height", "12mm"); + if(!corner.getKey().isEmpty()) + { + element.put("align", corner.getKey().split(" ", -1)[0]).put("valign", corner.getKey().split(" ", -1)[1]); + } + Map answer = BarcodeFixture.answer(request("RENDER", 203, element.toString(), Map.of())); + Map chosen = BarcodeFixture.finding(answer, "BARCODE_MODULE"); + assertEquals(List.of(80, 16, 159, 95), chosen.get("box")); + assertEquals(Map.of("modules", 29, "quietModules", 4, "moduleDots", 3, "moduleMicrometres", 375, "widthDots", 87, "heightDots", 87), chosen.get("measured")); + BarcodeFixture.Ink ink = BarcodeFixture.ink(BarcodeFixture.page(answer)); + assertEquals(List.of(24 + 80 + corner.getValue().get(0), 24 + 16 + corner.getValue().get(1), 63, 63), List.of(ink.left(), ink.top(), ink.width(), ink.height()), corner.getKey()); + assertEquals("TEST-0001", BarcodeFixture.decode(BarcodeFixture.page(answer), BarcodeFormat.QR_CODE, false).getText()); + } + + ObjectNode linear = barcode("code128", "TEST-0001").put("x", "10mm").put("y", "2mm").put("width", "60mm").put("height", "12mm"); + for(String align : List.of("left", "center", "right")) + { + Map answer = BarcodeFixture.answer(request("RENDER", 300, linear.put("align", align).put("valign", "bottom").toString(), Map.of())); + Map chosen = BarcodeFixture.finding(answer, "BARCODE_MODULE"); + Frame box = BarcodeFixture.box(chosen, 300); + int spare = box.width() - BarcodeFixture.number(chosen, "measured", "widthDots"); + int quiet = 10 * BarcodeFixture.number(chosen, "measured", "moduleDots"); + BarcodeFixture.Ink ink = BarcodeFixture.ink(BarcodeFixture.page(answer)); + assertEquals(box.x() + quiet + Map.of("left", 0, "center", spare / 2, "right", spare).get(align), ink.left(), align); + assertEquals(List.of(box.y(), box.bottom() - 1), List.of(ink.top(), ink.bottom()), align); + } + } + + + + /*************************************************************************** + ** `rotation` turns the symbol 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 picture is the upright one turned, dot for dot. Every turn decodes + ** once the page is turned back. + ** The label here is 4 x 2 in, tall enough for a symbol on its side. + ***************************************************************************/ + @Test + void rotationTurnsTheSymbolClockwiseInItsBox() throws Exception + { + for(String[] symbol : List.of(new String[] {"code128", "TEST-0001", "CODE_128"}, new String[] {"datamatrix", "TEST-0001-0002", "DATA_MATRIX"}, new String[] {"pdf417", "TEST-0001", "PDF_417"})) + { + boolean[][] expected = null; + for(int rotation : List.of(0, 90, 180, 270)) + { + boolean upright = rotation % 180 == 0; + ObjectNode element = barcode(symbol[0], symbol[1]).put("x", "30mm").put("y", "1mm").put("width", upright ? "40mm" : "12mm").put("height", upright ? "12mm" : "40mm").put("rotation", rotation); + element.put("minModule", "0.2mm").put("align", "center").put("valign", "middle"); + Map answer = BarcodeFixture.answer(BarcodeFixture.request("RENDER", 300, element.toString(), Map.of())); + Map chosen = BarcodeFixture.finding(answer, "BARCODE_MODULE"); + boolean[][] page = BarcodeFixture.page(answer); + Frame box = BarcodeFixture.box(chosen, 300); + BarcodeFixture.Ink ink = BarcodeFixture.ink(page); + String about = symbol[0] + " turned " + rotation; + assertEquals(upright ? List.of(472, 141) : List.of(141, 472), List.of(box.width(), box.height()), about); + assertEquals(List.of(472, 141), List.of(BarcodeFixture.number(chosen, "limit", "widthDots"), BarcodeFixture.number(chosen, "limit", "heightDots")), about); + assertTrue(ink.left() >= box.x() && ink.right() < box.right() && ink.top() >= box.y() && ink.bottom() < box.bottom(), about); + expected = expected == null ? crop(page, ink) : turned(expected); + assertArrayEquals(expected, crop(page, ink), about); + boolean[][] righted = crop(page, new BarcodeFixture.Ink(box.x(), box.y(), box.right() - 1, box.bottom() - 1)); + for(int turn = rotation; turn % 360 != 0; turn += 90) + { + righted = turned(righted); + } + assertEquals(symbol[1], BarcodeFixture.decode(righted, BarcodeFormat.valueOf(symbol[2]), false).getText(), about); + } + } + } + + + + /*************************************************************************** + ** A value that does not fit at the minimum module is refused, when the + ** scene is validated as when it is rendered, with the modules it needs, + ** the dots they need at the minimum, the largest module that would fit and + ** the dots the box has. The label is never drawn smaller. + ***************************************************************************/ + @Test + void aValueThatDoesNotFitAtTheMinimumModuleIsRefusedWithItsNumbers() throws Exception + { + ObjectNode narrow = centred("gs1-128", TYPICAL, "60mm"); + Map expected = Map.of("code", "BARCODE_DOES_NOT_FIT", "severity", "ERROR", "element", "code", "box", List.of(142, 0, 479, 95), + "measured", Map.of("modules", 319, "quietModules", 10, "moduleDots", 1, "moduleMicrometres", 125, "widthDots", 638, "heightDots", 95), + "limit", Map.of("widthDots", 479, "heightDots", 95, "minModuleDots", 2)); + assertEquals(expected, refused("RENDER", 203, narrow, Map.of())); + assertEquals(expected, refused("VALIDATE", 203, narrow, Map.of())); + Map at300 = refused("RENDER", 300, narrow, Map.of()); + assertEquals(Map.of("modules", 319, "quietModules", 10, "moduleDots", 2, "moduleMicrometres", 169, "widthDots", 957, "heightDots", 141), at300.get("measured")); + assertEquals(Map.of("widthDots", 708, "heightDots", 141, "minModuleDots", 3), at300.get("limit")); + + Map collected = RendererMain.process(request("RENDER", 203, narrow + ",{\"id\":\"rule\",\"type\":\"box\",\"anchor\":\"bottom-left\",\"width\":\"10mm\",\"height\":\"1mm\",\"fill\":true}", Map.of()) + .put("diagnostics", "COLLECT")); + assertEquals("REFUSED", collected.get("outcome")); + assertEquals(List.of(expected), collected.get("diagnostics")); + assertFalse(collected.containsKey("base64")); + + /////////////////////////////////////////////////////////////////////////////////////////// + // a two-dimensional symbol is refused on the axis that is short: 29 modules need 87 // + // dots at 3 dots, and a box 5 mm high has 39; not one dot fits a box of 2 mm // + /////////////////////////////////////////////////////////////////////////////////////////// + Map flat = refused("RENDER", 203, barcode("qr", "TEST-0001").put("width", "40mm").put("height", "5mm"), Map.of()); + assertEquals(Map.of("modules", 29, "quietModules", 4, "moduleDots", 1, "moduleMicrometres", 125, "widthDots", 87, "heightDots", 87), flat.get("measured")); + assertEquals(Map.of("widthDots", 319, "heightDots", 39, "minModuleDots", 3), flat.get("limit")); + Map flatter = refused("RENDER", 203, barcode("qr", "TEST-0001").put("width", "40mm").put("height", "2mm"), Map.of()); + assertEquals(0, BarcodeFixture.number(flatter, "measured", "moduleDots")); + assertEquals(0, BarcodeFixture.number(flatter, "measured", "moduleMicrometres")); + } + + + + /*************************************************************************** + ** An author who accepts a smaller module states it: the symbol is then + ** drawn below the default and the label carries a warning every time, + ** validated or rendered. A minimum is never passed: a larger stated one + ** refuses a symbol the default would have drawn. + ***************************************************************************/ + @Test + void aSmallerModuleIsDrawnOnlyWhenTheAuthorStatesItAndAlwaysWithAWarning() throws Exception + { + String scene = "{\"schema\":\"qqq-zpl-scene/2\",\"design\":{\"width\":\"4in\",\"height\":\"1in\"},\"elements\":[" + centred("gs1-128", TYPICAL, "60mm") + + "],\"overrides\":[{\"when\":{\"dpi\":[203]},\"set\":{\"code\":{\"minModule\":\"1dot\"}}}]}"; + Map warning = Map.of("code", "BARCODE_MODULE_BELOW_RECOMMENDED", "severity", "WARNING", "element", "code", "box", List.of(142, 0, 479, 95), + "measured", Map.of("minModuleDots", 1), "limit", Map.of("minModuleDots", 2)); + Map rendered = BarcodeFixture.answer(SceneFixture.request("RENDER", scene)); + assertEquals(warning, BarcodeFixture.finding(rendered, "BARCODE_MODULE_BELOW_RECOMMENDED")); + Map chosen = BarcodeFixture.finding(rendered, "BARCODE_MODULE"); + assertEquals(Map.of("modules", 319, "quietModules", 10, "moduleDots", 1, "moduleMicrometres", 125, "widthDots", 319, "heightDots", 95), chosen.get("measured")); + assertEquals(Map.of("widthDots", 479, "heightDots", 95, "minModuleDots", 1), chosen.get("limit")); + assertEquals(SCANNED, BarcodeFixture.decode(BarcodeFixture.page(rendered), BarcodeFormat.CODE_128, true).getText()); + assertEquals(rendered.get("diagnostics"), BarcodeFixture.answer(SceneFixture.request("VALIDATE", scene)).get("diagnostics")); + assertEquals("BARCODE_DOES_NOT_FIT", SceneFixture.refusal(() -> RendererMain.process(SceneFixture.request("RENDER", scene).put("targetDpi", 300))).get("code")); + + /////////////////////////////////////////////////////////////////////////////////////////// + // stated in the element, a `dot` length is also reported as a device unit. A physical // + // minimum is compared in dots: 0.2 mm is the default 2 dots at 203 dpi and 5 dots, // + // under the default 6, at 600 dpi // + /////////////////////////////////////////////////////////////////////////////////////////// + Map stated = BarcodeFixture.answer(request("VALIDATE", 203, centred("gs1-128", TYPICAL, "60mm").put("minModule", "1dot").toString(), Map.of())); + assertEquals(Map.of("code", "DEVICE_UNIT_USED", "severity", "WARNING", "element", "code", "pointer", "/elements/0/minModule"), BarcodeFixture.finding(stated, "DEVICE_UNIT_USED")); + assertEquals(warning, BarcodeFixture.finding(stated, "BARCODE_MODULE_BELOW_RECOMMENDED")); + ObjectNode physical = centred("gs1-128", TYPICAL, "90mm").put("minModule", "0.2mm"); + assertEquals(List.of("BARCODE_MODULE"), codes(BarcodeFixture.answer(request("RENDER", 203, physical.toString(), Map.of())))); + Map fine = BarcodeFixture.answer(request("RENDER", 600, physical.toString(), Map.of())); + assertEquals(List.of("BARCODE_MODULE_BELOW_RECOMMENDED", "BARCODE_MODULE"), codes(fine)); + assertEquals(Map.of("minModuleDots", 5), BarcodeFixture.finding(fine, "BARCODE_MODULE_BELOW_RECOMMENDED").get("measured")); + assertEquals(6, BarcodeFixture.number(BarcodeFixture.finding(fine, "BARCODE_MODULE"), "measured", "moduleDots")); + + Map larger = refused("RENDER", 203, centred("gs1-128", TYPICAL, "90mm").put("minModule", "0.5mm"), Map.of()); + assertEquals("BARCODE_DOES_NOT_FIT", larger.get("code")); + assertEquals(Map.of("widthDots", 719, "heightDots", 95, "minModuleDots", 4), larger.get("limit")); + assertEquals(319 * 4, BarcodeFixture.number(larger, "measured", "widthDots")); + } + + + + /*************************************************************************** + ** The codes of the findings of an answer, in order. + ***************************************************************************/ + @SuppressWarnings("unchecked") + private static List codes(Map answer) + { + return (((List>) answer.get("diagnostics")).stream().map(finding -> finding.get("code")).toList()); + } + + + + /*************************************************************************** + ** `maxModule` caps the module. It goes up to a whole dot as the minimum + ** does, so the same length for both is one module at every resolution. A + ** maximum under the minimum is refused where the scene states it. + ***************************************************************************/ + @Test + void aMaximumModuleCapsTheSymbol() throws Exception + { + ObjectNode capped = centred("code128", "TEST-0001", "90mm").put("maxModule", "0.5mm"); + assertEquals(4, BarcodeFixture.number(BarcodeFixture.finding(BarcodeFixture.answer(request("RENDER", 203, capped.toString(), Map.of())), "BARCODE_MODULE"), "measured", "moduleDots")); + assertEquals(5, BarcodeFixture.number(BarcodeFixture.finding(BarcodeFixture.answer(request("RENDER", 203, capped.remove(List.of("maxModule")).toString(), Map.of())), "BARCODE_MODULE"), "measured", "moduleDots")); + ObjectNode exact = centred("code128", "TEST-0001", "90mm").put("minModule", "0.3mm").put("maxModule", "0.3mm"); + for(Map.Entry expected : Map.of(203, 3, 300, 4, 600, 8).entrySet()) + { + Map chosen = BarcodeFixture.finding(BarcodeFixture.answer(request("RENDER", expected.getKey(), exact.toString(), Map.of())), "BARCODE_MODULE"); + assertEquals(expected.getValue(), BarcodeFixture.number(chosen, "measured", "moduleDots")); + } + + Map under = refused("VALIDATE", 203, centred("code128", "TEST-0001", "90mm").put("maxModule", "0.1mm"), Map.of()); + assertEquals(Map.of("code", "SCENE_INVALID", "severity", "ERROR", "element", "code", "pointer", "/elements/0/maxModule", "box", List.of(22, 0, 719, 95), + "measured", Map.of("maxModuleDots", 1), "limit", Map.of("minModuleDots", 2)), under); + String overridden = "{\"schema\":\"qqq-zpl-scene/2\",\"design\":{\"width\":\"4in\",\"height\":\"1in\"},\"elements\":[" + centred("code128", "TEST-0001", "90mm") + + "],\"overrides\":[{\"when\":{\"dpi\":[203]},\"set\":{\"code\":{\"maxModule\":\"1dot\"}}}]}"; + assertEquals("/overrides/0/set/code/maxModule", SceneFixture.refusal(() -> RendererMain.process(SceneFixture.request("RENDER", overridden))).get("pointer")); + } + + + + /*************************************************************************** + ** A property that is not what the schema says is refused at that property, + ** whatever the record holds. + ***************************************************************************/ + @Test + void aBarcodeThatIsNotWhatTheSchemaSaysIsRefusedAtTheProperty() throws Exception + { + String tooLong = "A".repeat(4001); + Map mistakes = new LinkedHashMap<>(); + mistakes.put("{\"value\":\"TEST-0001\"}", "symbology"); + mistakes.put("{\"symbology\":\"code128\",\"symbologyPath\":\"/record/type\",\"allowedSymbologies\":[\"code128\"],\"value\":\"TEST-0001\"}", "symbology"); + mistakes.put("{\"symbology\":128,\"value\":\"TEST-0001\"}", "symbology"); + mistakes.put("{\"symbology\":\"codabar\",\"value\":\"TEST-0001\"}", "symbology"); + mistakes.put("{\"symbology\":\"code128\",\"allowedSymbologies\":[\"code128\"],\"value\":\"TEST-0001\"}", "allowedSymbologies"); + mistakes.put("{\"symbologyPath\":\"/record/type\",\"value\":\"TEST-0001\"}", "allowedSymbologies"); + mistakes.put("{\"symbologyPath\":\"/record/type\",\"allowedSymbologies\":\"code128\",\"value\":\"TEST-0001\"}", "allowedSymbologies"); + mistakes.put("{\"symbologyPath\":\"/record/type\",\"allowedSymbologies\":[],\"value\":\"TEST-0001\"}", "allowedSymbologies"); + mistakes.put("{\"symbologyPath\":\"/record/type\",\"allowedSymbologies\":[128],\"value\":\"TEST-0001\"}", "allowedSymbologies"); + mistakes.put("{\"symbologyPath\":\"/record/type\",\"allowedSymbologies\":[\"code128\",\"codabar\"],\"value\":\"TEST-0001\"}", "allowedSymbologies"); + mistakes.put("{\"symbologyPath\":\"/record/type\",\"allowedSymbologies\":[\"qr\",\"qr\"],\"value\":\"TEST-0001\"}", "allowedSymbologies"); + mistakes.put("{\"symbologyPath\":\"record/type\",\"allowedSymbologies\":[\"qr\"],\"value\":\"TEST-0001\"}", "symbologyPath"); + mistakes.put("{\"symbologyPath\":7,\"allowedSymbologies\":[\"qr\"],\"value\":\"TEST-0001\"}", "symbologyPath"); + mistakes.put("{\"symbology\":\"code128\"}", "value"); + mistakes.put("{\"symbology\":\"code128\",\"value\":\"TEST-0001\",\"valuePath\":\"/record/value\"}", "value"); + mistakes.put("{\"symbology\":\"code128\",\"value\":17}", "value"); + mistakes.put("{\"symbology\":\"code128\",\"value\":\"\"}", "value"); + mistakes.put("{\"symbology\":\"code128\",\"value\":\"" + tooLong + "\"}", "value"); + mistakes.put("{\"symbology\":\"code128\",\"valuePath\":\"/" + tooLong.substring(0, 256) + "\"}", "valuePath"); + mistakes.put("{\"symbology\":\"code128\",\"value\":\"TEST-0001\",\"scannerSymbologyPath\":\"/record/label\"}", "aimIdentifierPath"); + mistakes.put("{\"symbology\":\"code128\",\"value\":\"TEST-0001\",\"aimIdentifierPath\":\"/record/aim\"}", "scannerSymbologyPath"); + mistakes.put("{\"symbology\":\"code128\",\"value\":\"TEST-0001\",\"scannerSymbologyPath\":\"\",\"aimIdentifierPath\":\"/record/aim\"}", "scannerSymbologyPath"); + mistakes.put("{\"symbology\":\"code128\",\"value\":\"TEST-0001\",\"minModule\":\"0mm\"}", "minModule"); + mistakes.put("{\"symbology\":\"code128\",\"value\":\"TEST-0001\",\"minModule\":\"1%\"}", "minModule"); + mistakes.put("{\"symbology\":\"code128\",\"value\":\"TEST-0001\",\"minModule\":0.25}", "minModule"); + mistakes.put("{\"symbology\":\"code128\",\"value\":\"TEST-0001\",\"maxModule\":\"0pt\"}", "maxModule"); + mistakes.put("{\"symbology\":\"code128\",\"value\":\"TEST-0001\",\"quiet\":9}", "quiet"); + mistakes.put("{\"symbology\":\"code128\",\"value\":\"TEST-0001\",\"quiet\":65}", "quiet"); + mistakes.put("{\"symbology\":\"code128\",\"value\":\"TEST-0001\",\"quiet\":\"10\"}", "quiet"); + mistakes.put("{\"symbology\":\"code128\",\"value\":\"TEST-0001\",\"quiet\":10.5}", "quiet"); + mistakes.put("{\"symbology\":\"code128\",\"value\":\"TEST-0001\",\"quiet\":4294967306}", "quiet"); + mistakes.put("{\"symbology\":\"qr\",\"value\":\"TEST-0001\",\"quiet\":3}", "quiet"); + mistakes.put("{\"symbologyPath\":\"/record/type\",\"allowedSymbologies\":[\"qr\",\"code128\"],\"value\":\"TEST-0001\",\"quiet\":4}", "quiet"); + mistakes.put("{\"symbology\":\"code128\",\"value\":\"TEST-0001\",\"rotation\":45}", "rotation"); + for(Map.Entry mistake : mistakes.entrySet()) + { + ObjectNode element = (ObjectNode) Wire.JSON.readTree(mistake.getKey()); + element.put("id", "code").put("type", "barcode"); + assertEquals(invalid(mistake.getValue()), refused("VALIDATE", 203, element, Map.of("type", "qr", "value", "TEST-0001", "label", "QRCODE", "aim", "]Q1")), mistake.getKey().length() > 200 ? mistake.getValue() : mistake.getKey()); + } + assertEquals(Map.of("code", "SCENE_INVALID", "severity", "ERROR", "element", "code", "pointer", "/elements/0", "measured", Map.of("unknownProperties", 1), "limit", Map.of("unknownProperties", 0)), + refused("VALIDATE", 203, barcode("code128", "TEST-0001").put("scale", 2), Map.of())); + + /////////////////////////////////////////////////////////////////////////////////////////// + // the widest quiet zone, and a quiet zone of four where every symbology allowed is two- // + // dimensional, are what the schema says // + /////////////////////////////////////////////////////////////////////////////////////////// + assertEquals(64, BarcodeFixture.number(BarcodeFixture.finding(BarcodeFixture.answer(request("RENDER", 203, barcode("code128", "TEST-0001").put("quiet", 64).toString(), Map.of())), "BARCODE_MODULE"), + "measured", "quietModules")); + assertEquals(5, BarcodeFixture.number(BarcodeFixture.finding(BarcodeFixture.answer(request("RENDER", 203, barcode("qr", "TEST-0001").put("quiet", 5).toString(), Map.of())), "BARCODE_MODULE"), + "measured", "quietModules")); + } + + + + /*************************************************************************** + ** The human-readable line and the element string built from application + ** identifiers are not drawn by this renderer: an element that asks for + ** either is placed and refused by name, never drawn without it. + ***************************************************************************/ + @Test + void aBarcodeThatAsksForWhatThisRendererLacksIsRefusedByName() throws Exception + { + Map expected = Map.of("code", "RENDERER_TOO_OLD", "severity", "ERROR", "element", "code", "box", List.of(22, 0, 719, 95)); + ObjectNode caption = centred("gs1-128", TYPICAL, "90mm"); + caption.putObject("hri").put("position", "below"); + assertEquals(expected, refused("VALIDATE", 203, caption, Map.of())); + assertEquals(expected, refused("RENDER", 203, caption, Map.of())); + ObjectNode built = Wire.JSON.createObjectNode().put("id", "code").put("type", "barcode").put("symbology", "gs1-128").put("anchor", "top-center").put("width", "90mm").put("height", "12mm"); + built.putArray("gs1").addObject().put("ai", "01").put("valuePath", "/record/gtin"); + assertEquals(expected, refused("RENDER", 203, built, Map.of("gtin", "00012345678905"))); + } + + + + /*************************************************************************** + ** A value bound to the record: text is encoded as it is, a number or + ** true/false as JSON writes it; anything else, and nothing at all, is + ** `VALUE_MISSING` at the property that binds it. Validation reads no + ** record, so it cannot fit a bound value and says so every time. + ***************************************************************************/ + @Test + void aBoundValueIsReadFromTheRecordWhenRenderingAndNotProvenWhenValidating() throws Exception + { + ObjectNode element = centred("code128", "", "90mm"); + element.remove("value"); + element.put("valuePath", "/record/value"); + Map unproven = Map.of("code", "FIT_NOT_PROVEN", "severity", "WARNING", "element", "code", "box", List.of(22, 0, 719, 95)); + assertEquals(List.of(unproven), BarcodeFixture.answer(request("VALIDATE", 203, element.toString(), Map.of())).get("diagnostics")); + for(Map.Entry value : Map.of("TEST-0001", "TEST-0001", 12345, "12345", true, "true").entrySet()) + { + Map answer = BarcodeFixture.answer(request("RENDER", 203, element.toString(), Map.of("value", value.getKey()))); + assertEquals(List.of("FIT_NOT_PROVEN", "BARCODE_MODULE"), codes(answer)); + assertEquals(value.getValue(), BarcodeFixture.decode(BarcodeFixture.page(answer), BarcodeFormat.CODE_128, false).getText()); + } + Map missing = Map.of("code", "VALUE_MISSING", "severity", "ERROR", "element", "code", "pointer", "/elements/0/valuePath", "box", List.of(22, 0, 719, 95)); + assertEquals(missing, refused("RENDER", 203, element, Map.of())); + assertEquals(missing, refused("RENDER", 203, element, Map.of("value", ""))); + assertEquals(missing, refused("RENDER", 203, element, Map.of("value", List.of("TEST-0001")))); + assertEquals(Map.of("code", "VALUE_TOO_LONG", "severity", "ERROR", "element", "code", "box", List.of(22, 0, 719, 95), "measured", Map.of("characters", 4001), "limit", Map.of("characters", 4000)), + refused("RENDER", 203, element, Map.of("value", "A".repeat(4001)))); + + /////////////////////////////////////////////////////////////////////////////////////////// + // a literal value in a stated symbology is fitted when the scene is validated, so what // + // validation finds is what rendering finds // + /////////////////////////////////////////////////////////////////////////////////////////// + String literal = centred("code128", "TEST-0001", "90mm").toString(); + assertEquals(BarcodeFixture.answer(request("RENDER", 600, literal, Map.of())).get("diagnostics"), BarcodeFixture.answer(request("VALIDATE", 600, literal, Map.of())).get("diagnostics")); + } + + + + /*************************************************************************** + ** A symbology read from the record must be one the scene allows. The + ** symbol is then fitted by the rules of the symbology the record names. + ***************************************************************************/ + @Test + void aBoundSymbologyIsHeldToTheListTheSceneAllows() throws Exception + { + ObjectNode element = Wire.JSON.createObjectNode().put("id", "code").put("type", "barcode").put("symbologyPath", "/record/type").put("value", "TEST-0001").put("width", "40mm").put("height", "15mm"); + element.putArray("allowedSymbologies").add("code128").add("qr"); + Map linear = BarcodeFixture.answer(request("RENDER", 203, element.toString(), Map.of("type", "code128"))); + assertEquals(List.of(10, 2), List.of(BarcodeFixture.number(BarcodeFixture.finding(linear, "BARCODE_MODULE"), "measured", "quietModules"), BarcodeFixture.number(BarcodeFixture.finding(linear, "BARCODE_MODULE"), "limit", "minModuleDots"))); + assertEquals("TEST-0001", BarcodeFixture.decode(BarcodeFixture.page(linear), BarcodeFormat.CODE_128, false).getText()); + Map matrix = BarcodeFixture.answer(request("RENDER", 203, element.toString(), Map.of("type", "qr"))); + assertEquals(List.of(4, 3), List.of(BarcodeFixture.number(BarcodeFixture.finding(matrix, "BARCODE_MODULE"), "measured", "quietModules"), BarcodeFixture.number(BarcodeFixture.finding(matrix, "BARCODE_MODULE"), "limit", "minModuleDots"))); + assertEquals("TEST-0001", BarcodeFixture.decode(BarcodeFixture.page(matrix), BarcodeFormat.QR_CODE, false).getText()); + + List box = List.of(0, 0, 319, 119); + assertEquals(Map.of("code", "SCENE_INVALID", "severity", "ERROR", "element", "code", "pointer", "/elements/0/symbologyPath", "box", box), refused("RENDER", 203, element, Map.of("type", "datamatrix"))); + assertEquals(Map.of("code", "VALUE_MISSING", "severity", "ERROR", "element", "code", "pointer", "/elements/0/symbologyPath", "box", box), refused("RENDER", 203, element, Map.of())); + assertEquals(Map.of("code", "VALUE_MISSING", "severity", "ERROR", "element", "code", "pointer", "/elements/0/symbologyPath", "box", box), refused("RENDER", 203, element, Map.of("type", 128))); + + /////////////////////////////////////////////////////////////////////////////////////////// + // validated, the symbology is not known: the fit is not proven, and a stated minimum is // + // held against the largest default of the symbologies allowed // + /////////////////////////////////////////////////////////////////////////////////////////// + assertEquals(List.of("FIT_NOT_PROVEN"), codes(BarcodeFixture.answer(request("VALIDATE", 203, element.toString(), Map.of())))); + Map small = BarcodeFixture.answer(request("VALIDATE", 203, element.put("minModule", "0.25mm").toString(), Map.of())); + assertEquals(List.of("FIT_NOT_PROVEN", "BARCODE_MODULE_BELOW_RECOMMENDED"), codes(small)); + assertEquals(Map.of("minModuleDots", 3), BarcodeFixture.finding(small, "BARCODE_MODULE_BELOW_RECOMMENDED").get("limit")); + assertEquals(List.of("FIT_NOT_PROVEN", "BARCODE_MODULE"), codes(BarcodeFixture.answer(request("RENDER", 203, element.toString(), Map.of("type", "code128"))))); + } + + + + /*************************************************************************** + ** The scanner profile of version 1, carried over: the symbology and AIM + ** identifier a scanner reported must name the symbology drawn, and the + ** value is held to what the profile can reproduce. A retail value arrives + ** with its check digit, which is verified and not encoded twice. + ***************************************************************************/ + @Test + void aScannerProfileHoldsTheValueToWhatWasScanned() throws Exception + { + ObjectNode element = Wire.JSON.createObjectNode().put("id", "code").put("type", "barcode").put("symbology", "code128").put("valuePath", "/record/value").put("width", "60mm").put("height", "12mm"); + element.put("scannerSymbologyPath", "/record/label").put("aimIdentifierPath", "/record/aim"); + Map answer = BarcodeFixture.answer(request("RENDER", 203, element.toString(), Map.of("value", "001234", "label", "LABEL-TYPE-CODE128", "aim", "]C0"))); + assertEquals("001234", BarcodeFixture.decode(BarcodeFixture.page(answer), BarcodeFormat.CODE_128, false).getText()); + assertEquals(List.of("FIT_NOT_PROVEN"), codes(BarcodeFixture.answer(request("VALIDATE", 203, element.toString(), Map.of())))); + + ObjectNode retail = element.deepCopy().put("symbology", "ean13"); + Map checked = BarcodeFixture.answer(request("RENDER", 203, retail.toString(), Map.of("value", "0012345678905", "label", "EAN13", "aim", "]E0"))); + assertEquals("0012345678905", BarcodeFixture.decode(BarcodeFixture.page(checked), BarcodeFormat.EAN_13, false).getText()); + + List box = List.of(0, 0, 479, 95); + Map profile = Map.of("code", "SCENE_INVALID", "severity", "ERROR", "element", "code", "pointer", "/elements/0/scannerSymbologyPath", "box", box); + assertEquals(profile, refused("RENDER", 203, element, Map.of("value", "001234", "label", "CODE128", "aim", "]C1"))); + assertEquals(profile, refused("RENDER", 203, element, Map.of("value", "001234", "label", "QRCODE", "aim", "]Q1"))); + Map unusable = Map.of("code", "SCENE_INVALID", "severity", "ERROR", "element", "code", "pointer", "/elements/0/valuePath", "box", box, "measured", Map.of("characters", 13)); + assertEquals(unusable, refused("RENDER", 203, retail, Map.of("value", "0012345678901", "label", "EAN13", "aim", "]E0"))); + assertEquals(Map.of("code", "VALUE_MISSING", "severity", "ERROR", "element", "code", "pointer", "/elements/0/scannerSymbologyPath", "box", box), + refused("RENDER", 203, element, Map.of("value", "001234", "aim", "]C0"))); + assertEquals(Map.of("code", "VALUE_MISSING", "severity", "ERROR", "element", "code", "pointer", "/elements/0/aimIdentifierPath", "box", box), + refused("RENDER", 203, element, Map.of("value", "001234", "label", "CODE128"))); + } + + + + /*************************************************************************** + ** A value the symbology cannot hold is refused at the property that gives + ** it, with a count of characters. Neither the diagnostic nor the exception + ** holds any of the value, or anything the encoder said about it. + ***************************************************************************/ + @Test + void aValueTheSymbologyCannotHoldIsRefusedWithoutTheValue() throws Exception + { + String secret = "TEST-SECRET-0001"; + for(String[] unusable : List.of(new String[] {"itf", "SCENE_INVALID"}, new String[] {"ean13", "SCENE_INVALID"}, new String[] {"gs1-128", "GS1_INVALID"})) + { + Refusal literal = assertThrows(Refusal.class, () -> RendererMain.process(request("VALIDATE", 203, barcode(unusable[0], secret).toString(), Map.of()))); + assertEquals(Map.of("code", unusable[1], "severity", "ERROR", "element", "code", "pointer", "/elements/0/value", "box", List.of(0, 0, 764, 155), "measured", Map.of("characters", 16)), + literal.diagnostic().wire()); + ObjectNode bound = barcode(unusable[0], ""); + bound.remove("value"); + Refusal read = assertThrows(Refusal.class, () -> RendererMain.process(request("RENDER", 203, bound.put("valuePath", "/record/value").toString(), Map.of("value", secret)))); + assertEquals("/elements/0/valuePath", read.diagnostic().pointer()); + for(Refusal refusal : List.of(literal, read)) + { + assertEquals(unusable[1], refusal.getMessage()); + assertNull(refusal.getCause()); + assertEquals(0, refusal.getSuppressed().length); + assertFalse(refusal.toString().contains("SECRET") || refusal.diagnostic().wire().toString().contains("SECRET")); + } + } + String overridden = "{\"schema\":\"qqq-zpl-scene/2\",\"design\":{\"width\":\"4in\",\"height\":\"1in\"},\"elements\":[" + barcode("code128", "TEST-0001") + + "],\"overrides\":[{\"when\":{\"dpi\":[203,300,600]},\"set\":{\"code\":{\"symbology\":\"itf\"}}}]}"; + assertEquals("/elements/0/value", SceneFixture.refusal(() -> RendererMain.process(SceneFixture.request("RENDER", overridden))).get("pointer")); + } + + + + /*************************************************************************** + ** Whatever the value, every fit-qualified symbology ends as a label or as a + ** refusal with a diagnostic, bound or literal: an encoder's own exception + ** never leaves the element. The values are the awkward ones: characters no + ** symbology here has, control characters, half of a surrogate pair, escape + ** sequences the encoders read, broken element strings, the longest value + ** and one of every length a retail symbol could take. + ***************************************************************************/ + @Test + void everyValueEndsAsALabelOrARefusal() throws Exception + { + String accented = Character.toString(0xE9); + List values = new ArrayList<>(List.of(" ", "\u0000", "\t\r\n", "\u007f", accented, Character.toString(0xFF), Character.toString(0x100), Character.toString(0x2603), String.valueOf((char) 0xD83D), Character.toString(0x1F600), String.valueOf((char) 0xFFFF), "~", "~1", "\\", "\\", "\\0100012345678905", + "\\", "\\<", "\\x", "\\d065", "\\u00e9", "\\eci", "^ECI000003", "[", "]", "[]", "[0", "[01]", "[01]1", "[01]00012345678905[", "[01]0001234567890A", "[01]00012345678906", "[99]", "[9999]TEST", "[01][01]", "(01)00012345678905", + "[01]00012345678905[10]", "[21]" + "A".repeat(21), "[00]123456789012345678", "*", "*TEST*", "test", "Test-0001", "+", "%", "$", "/", ".", "-", "TEST 0001", "A".repeat(4000), "0".repeat(4000), "01".repeat(600), + accented.repeat(200), "[01]00012345678905".repeat(40))); + for(int digits = 1; digits <= 15; digits++) + { + values.add("0123456789012345".substring(0, digits)); + values.add("9".repeat(digits)); + } + int labels = 0; + int refusals = 0; + for(String symbology : BarcodeFit.TYPES) + { + for(String value : values) + { + ObjectNode literal = barcode(symbology, value).put("minModule", "1dot"); + ObjectNode bound = barcode(symbology, ""); + bound.remove("value"); + bound.put("valuePath", "/record/value").put("minModule", "1dot"); + for(ObjectNode request : List.of(request("RENDER", 600, literal.toString(), Map.of()), request("VALIDATE", 203, literal.toString(), Map.of()), request("RENDER", 300, bound.toString(), Map.of("value", value)))) + { + try + { + assertEquals("OK", RendererMain.process(request).get("outcome")); + labels++; + } + catch(Refusal refusal) + { + refusals++; + assertTrue(List.of("BARCODE_DOES_NOT_FIT", "SCENE_INVALID", "GS1_INVALID").contains(refusal.diagnostic().code()), symbology + " refused a value of " + value.length() + " characters as " + refusal.diagnostic().code()); + assertEquals("code", refusal.diagnostic().element()); + } + } + } + } + assertTrue(labels > 500 && refusals > 500, labels + " labels, " + refusals + " refusals"); + } + + + + /*************************************************************************** + ** A symbol is its own white ground: over a filled box its quiet zones and + ** spaces are white, so it decodes, and the box is untouched beside it. + ***************************************************************************/ + @Test + void theSymbolClearsItsOwnGroundQuietZonesIncluded() throws Exception + { + String ground = "{\"id\":\"ground\",\"type\":\"box\",\"width\":\"94mm\",\"height\":\"14mm\",\"fill\":true},"; + Map answer = BarcodeFixture.answer(request("RENDER", 203, ground + centred("gs1-128", TYPICAL, "90mm"), Map.of())); + boolean[][] page = BarcodeFixture.page(answer); + int left = 24 + 22 + 40; + for(int y = 24; y < 24 + 95; y++) + { + assertTrue(page[y][left - 1] && page[y][left + 638], "the ground beside the symbol, row " + y); + for(int x = left; x < left + 20; x++) + { + assertFalse(page[y][x] || page[y][x + 618], "a quiet zone at " + x + ", " + y); + } + assertTrue(page[y][left + 20] && page[y][left + 617], "the outer bars, row " + y); + } + assertTrue(page[24 + 95][left + 30] && page[24 + 100][left + 300], "the ground below the box"); + for(int y = 0; y < page.length; y++) + { + for(int x = 0; x < page[y].length; x++) + { + page[y][x] &= x >= left && x < left + 638 && y < 24 + 95; + } + } + assertEquals(SCANNED, BarcodeFixture.decode(page, BarcodeFormat.CODE_128, true).getText()); + } +} diff --git a/renderer/src/test/java/com/kingsrook/qbits/renderer/scene2/barcode/BarcodeFixture.java b/renderer/src/test/java/com/kingsrook/qbits/renderer/scene2/barcode/BarcodeFixture.java new file mode 100644 index 0000000..42a2b07 --- /dev/null +++ b/renderer/src/test/java/com/kingsrook/qbits/renderer/scene2/barcode/BarcodeFixture.java @@ -0,0 +1,290 @@ +/* + * 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 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 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 answer(JsonNode request) throws Exception + { + Map answer = RendererMain.process(request); + assertEquals("OK", answer.get("outcome")); + return (answer); + } + + + + /*************************************************************************** + ** The page a printer would draw from an answer. + ***************************************************************************/ + static boolean[][] page(Map answer) + { + return (SceneFixture.raster(Base64.getDecoder().decode((String) answer.get("base64")))); + } + + + + /*************************************************************************** + ** The one finding of a code in an answer. + ***************************************************************************/ + @SuppressWarnings("unchecked") + static Map finding(Map answer, String code) + { + List> found = ((List>) 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 finding, String part, String name) + { + return (((Map) finding.get(part)).get(name)); + } + + + + /*************************************************************************** + ** The box of a finding, moved from the printable area onto the page. + ***************************************************************************/ + @SuppressWarnings("unchecked") + static Frame box(Map finding, int dpi) + { + List box = (List) 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 runs(boolean[] row) + { + List 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 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)); + } +} diff --git a/renderer/src/test/java/com/kingsrook/qbits/renderer/scene2/barcode/BarcodePropertyTest.java b/renderer/src/test/java/com/kingsrook/qbits/renderer/scene2/barcode/BarcodePropertyTest.java new file mode 100644 index 0000000..08ba760 --- /dev/null +++ b/renderer/src/test/java/com/kingsrook/qbits/renderer/scene2/barcode/BarcodePropertyTest.java @@ -0,0 +1,208 @@ +/* + * 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, 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 SYMBOLOGIES = List.of("code128", "code39", "itf", "gs1-128", "qr", "datamatrix"); + private static final Map 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 the default 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, and centred leaves + ** equal margins within one dot; a linear symbol takes the whole of the + ** box across its bars; 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; + for(int iteration = 0; iteration < 240; 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; + String align = List.of("left", "center", "right").get(random.nextInt(3)); + String valign = List.of("top", "middle", "bottom").get(random.nextInt(3)); + int along = random.nextInt(12_000, upright ? 94_000 : 43_000); + int across = random.nextInt(4000, 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("align", align).put("valign", valign).put("rotation", rotation); + if(stated) + { + element.put("minModule", "1dot"); + } + String about = "iteration " + iteration + ": " + element + " at " + dpi + " dpi"; + int recommended = matrix ? Map.of(203, 3, 300, 5, 600, 9).get(dpi) : Map.of(203, 2, 300, 3, 600, 6).get(dpi); + int quiet = matrix ? 4 : 10; + + Map answer; + try + { + answer = BarcodeFixture.answer(BarcodeFixture.request("RENDER", dpi, element.toString(), Map.of())); + } + catch(Refusal refusal) + { + refused++; + Map 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") + || (matrix && 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 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, "center", spareX / 2, "right", spareX).get(align), area.x() - box.x(), about); + assertEquals(Map.of("top", 0, "middle", spareY / 2, "bottom", spareY).get(valign), area.y() - box.y(), about); + assertTrue(!"center".equals(align) || Math.abs((area.x() - box.x()) - (box.right() - area.right())) <= 1, about); + assertTrue(!"middle".equals(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"); + 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, drawn + " drawn, " + refused + " refused, " + below + " below the default module"); + } +} diff --git a/renderer/src/test/java/com/kingsrook/qbits/renderer/scene2/barcode/ModuleFitTest.java b/renderer/src/test/java/com/kingsrook/qbits/renderer/scene2/barcode/ModuleFitTest.java new file mode 100644 index 0000000..838496f --- /dev/null +++ b/renderer/src/test/java/com/kingsrook/qbits/renderer/scene2/barcode/ModuleFitTest.java @@ -0,0 +1,195 @@ +/* + * 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.BarcodeFit; +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. + *******************************************************************************/ +class ModuleFitTest +{ + private static final int MICROMETRES_PER_INCH = 25_400; + + + + /*************************************************************************** + ** The defaults are the design's: 0.25 mm for a linear symbol and 0.375 mm + ** for a two-dimensional one, which taken up to whole dots is 2 and 3 dots + ** at 203 dpi, 3 and 5 at 300 dpi and 6 and 9 at 600 dpi; quiet zones are at + ** least 10 and 4 modules. Every fit-qualified symbology is one or the other. + ***************************************************************************/ + @Test + void theDefaultsAreTheMinimumsOfTheDesign() + { + assertEquals(250, ModuleFit.LINEAR_MINIMUM_MODULE.micrometres()); + assertEquals(375, ModuleFit.MATRIX_MINIMUM_MODULE.micrometres()); + assertEquals(List.of(2, 3, 6), Scene2.RESOLUTIONS.stream().map(dpi -> ModuleFit.recommended("gs1-128", dpi)).toList()); + assertEquals(List.of(3, 5, 9), Scene2.RESOLUTIONS.stream().map(dpi -> ModuleFit.recommended("qr", dpi)).toList()); + List matrix = BarcodeFit.TYPES.stream().filter(ModuleFit::matrix).toList(); + assertEquals(List.of("qr", "datamatrix", "pdf417", "aztec"), matrix); + for(String symbology : BarcodeFit.TYPES) + { + assertEquals(matrix.contains(symbology) ? 4 : 10, ModuleFit.quiet(symbology), symbology); + assertEquals(matrix.contains(symbology) ? 3 : 2, ModuleFit.recommended(symbology, 203), symbology); + } + assertEquals(64, ModuleFit.MAXIMUM_QUIET_MODULES); + } + + + + /*************************************************************************** + ** 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(new ModuleFit.Request(319, 0, 719, 87, 2, Integer.MAX_VALUE))); + assertEquals(new ModuleFit.Fit(2, 638, 87, true), ModuleFit.fit(new ModuleFit.Request(319, 0, 638, 87, 2, Integer.MAX_VALUE))); + assertEquals(new ModuleFit.Fit(1, 638, 87, false), ModuleFit.fit(new ModuleFit.Request(319, 0, 637, 87, 2, Integer.MAX_VALUE))); + assertEquals(new ModuleFit.Fit(0, 638, 87, false), ModuleFit.fit(new ModuleFit.Request(319, 0, 318, 87, 2, Integer.MAX_VALUE))); + assertEquals(new ModuleFit.Fit(1, 319, 87, true), ModuleFit.fit(new ModuleFit.Request(319, 0, 637, 87, 1, Integer.MAX_VALUE))); + assertEquals(new ModuleFit.Fit(7, 2233, 1, true), ModuleFit.fit(new ModuleFit.Request(319, 0, 2400, 1, 6, Integer.MAX_VALUE))); + } + + + + /*************************************************************************** + ** 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, Integer.MAX_VALUE))); + assertEquals(new ModuleFit.Fit(11, 319, 319, true), ModuleFit.fit(new ModuleFit.Request(29, 29, 319, 400, 3, Integer.MAX_VALUE))); + assertEquals(new ModuleFit.Fit(4, 116, 116, true), ModuleFit.fit(new ModuleFit.Request(29, 29, 319, 400, 3, 4))); + assertEquals(new ModuleFit.Fit(2, 87, 87, false), ModuleFit.fit(new ModuleFit.Request(29, 29, 319, 86, 3, Integer.MAX_VALUE))); + assertEquals(new ModuleFit.Fit(3, 957, 87, true), ModuleFit.fit(new ModuleFit.Request(319, 0, 2400, 87, 2, 3))); + } + + + + /*************************************************************************** + ** 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 and one dot more would not fit; 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; + for(int iteration = 0; iteration < 20_000; iteration++) + { + boolean matrix = random.nextBoolean(); + int columns = random.nextInt(21, 600); + 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) : Integer.MAX_VALUE; + ModuleFit.Request request = new ModuleFit.Request(columns, rows, along, across, minimum, maximum); + 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(fit.module() == maximum || columns * (fit.module() + 1) > along || (matrix && rows * (fit.module() + 1) > across), about); + } + else + { + refused++; + assertTrue(fit.module() < minimum && fit.module() >= 0, about); + assertEquals(columns * minimum, fit.width(), about); + assertTrue(fit.width() > along || (matrix && fit.height() > across), about); + } + } + assertTrue(fitted > 2000 && refused > 2000, fitted + " fitted, " + refused + " refused"); + } + + + + /*************************************************************************** + ** Property, over seeded symbols and physical boxes at the default minimum: + ** 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 = matrix ? ModuleFit.MATRIX_MINIMUM_MODULE : ModuleFit.LINEAR_MINIMUM_MODULE; + 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 fits = new ArrayList<>(); + for(int dpi : Scene2.RESOLUTIONS) + { + int along = box.size(dpi, 0); + ModuleFit.Fit fit = ModuleFit.fit(new ModuleFit.Request(columns, matrix ? columns : 0, along, along, minimum.module(dpi), Integer.MAX_VALUE)); + 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"); + } +} diff --git a/renderer/src/test/resources/scene2/cases/barcode-gs1.json b/renderer/src/test/resources/scene2/cases/barcode-gs1.json new file mode 100644 index 0000000..613de30 --- /dev/null +++ b/renderer/src/test/resources/scene2/cases/barcode-gs1.json @@ -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", "align": "center", "valign": "middle"}, + {"id": "rule", "type": "box", "anchor": "bottom-center", "y": "1mm", "width": "94mm", "height": "0.5pt", "thickness": "0.5pt"} + ] + } +} diff --git a/renderer/src/test/resources/scene2/cases/barcode-linear.json b/renderer/src/test/resources/scene2/cases/barcode-linear.json new file mode 100644 index 0000000..fdd817c --- /dev/null +++ b/renderer/src/test/resources/scene2/cases/barcode-linear.json @@ -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", "align": "center", "valign": "middle"}, + {"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", "align": "center", "valign": "middle", "rotation": 90} + ], + "overrides": [ + {"when": {"dpi": [600]}, "set": {"capped": {"maxModule": "0.3mm"}}} + ] + } +} diff --git a/renderer/src/test/resources/scene2/cases/barcode-matrix.json b/renderer/src/test/resources/scene2/cases/barcode-matrix.json new file mode 100644 index 0000000..160929c --- /dev/null +++ b/renderer/src/test/resources/scene2/cases/barcode-matrix.json @@ -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", "align": "center", "valign": "middle"}, + {"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} + ] + } +} diff --git a/renderer/src/test/resources/scene2/golden/barcode-gs1-203.png b/renderer/src/test/resources/scene2/golden/barcode-gs1-203.png new file mode 100644 index 0000000000000000000000000000000000000000..1e2df1979f5d248c81ccb83270d3dbc2ce656cb3 GIT binary patch literal 271 zcmeAS@N?(olHy`uVBq!ia0y~yVAcV$Pcs4uh9xmt(}C1APZ!6KinzCD4Z98-Ft`LZ zcjvO~>QY#%7;e78^oJJH_O>@>nK6788y@~G>Z-i_TDbS+t~)|%9KtP}hgcGu3>;@D z_*~d-oT^itz4&`ncBEJJnNs_#%%?JsZLTf;zt&s&^~STsD{udlczy5r;;omCNz~r2 zj@|zJsYvs|1U+UpAZZ}MgNyo;{cIZ81_4wH1=L<}&n(`xdA@mZJRj9Xl@;VuPgg&ebxsLQ0QY%lIRF3v literal 0 HcmV?d00001 diff --git a/renderer/src/test/resources/scene2/golden/barcode-gs1-203.zpl b/renderer/src/test/resources/scene2/golden/barcode-gs1-203.zpl new file mode 100644 index 0000000..695555b --- /dev/null +++ b/renderer/src/test/resources/scene2/golden/barcode-gs1-203.zpl @@ -0,0 +1,4 @@ +^XA^PW812^LL203^LH0,0^CI28 +^FO128,28^GFA,7210,7210,70,F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0F30FC3FCCFCF0F3C3CF0F0F0F3C3F3CFCCFCF0300CF0F3CFF3030F0F030C3F0C3CF0F3C3C3CF0F3F0C33FCFCF3F03303CC0F3F303CFC0CCFCFF3CF0F0F0F3C30303CF03F33C0^FS +^FO30,170^GFA,94,94,94,FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE^FS +^XZ diff --git a/renderer/src/test/resources/scene2/golden/barcode-gs1-300.png b/renderer/src/test/resources/scene2/golden/barcode-gs1-300.png new file mode 100644 index 0000000000000000000000000000000000000000..99c74961257fff6ff2978ca507835104226c585b GIT binary patch literal 487 zcmeAS@N?(olHy`uVBq!ia0y~yVA;UHz^KCr6kzy$to$#Kv(D4SF{C2y?YWJ)w-W@~ z64`XFL~?Unkb1Xc(x!%FzXeloRPr)KDX4NE>MQa{jp}As^uMC0{L4S8rJm=`nST1MNuA&CVk7;>wYSfozWz7aw*JrE zncH9gxg-B=`}K!AzuDQoyZ!X~rhPuI-){LIqj&iA(>*r+*S5d>__HRx;>`X3f5pYb z!dkDkwzh^QmL?`9hDO9jL_~;&(M7nlWy2(LAm7zopr0Ey1fO#lD@ literal 0 HcmV?d00001 diff --git a/renderer/src/test/resources/scene2/golden/barcode-gs1-300.zpl b/renderer/src/test/resources/scene2/golden/barcode-gs1-300.zpl new file mode 100644 index 0000000..23e30cc --- /dev/null +++ b/renderer/src/test/resources/scene2/golden/barcode-gs1-300.zpl @@ -0,0 +1,4 @@ +^XA^PW1200^LL300^LH0,0^CI28 +^FO184,42^GFA,15912,15912,104,FC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7EFC703FE07FF8E3FE3F03F1F81F8FC0FC0FC0FC7E07FC7E3FE38FF8FC01C0038FC0FC7E3FFC700703F03F0070381FF0381F8FC0FC7E07E07E3F03F1FF0381C7FF8FF8FC7FC01C7007E3803F1FF1C01F8FF8038E3FE3FFC7E3F03F03F03F1F81C01C01F8FC01FF1C7E^FS +^FO45,251^GFA,278,278,139,FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC^FS +^XZ diff --git a/renderer/src/test/resources/scene2/golden/barcode-gs1-600.png b/renderer/src/test/resources/scene2/golden/barcode-gs1-600.png new file mode 100644 index 0000000000000000000000000000000000000000..03ebbd1ef955bb690e9104ea2b2eed8ed0cc3ff8 GIT binary patch literal 1242 zcmeAS@N?(olHy`uVBq!ia0y~y;7nj(V2WS_3NW0Lc<_^ffn}kmi(^Pd+}jHq1Cyr-wIqH;&yN0YwKR({V9@991MeB-t=zl$XO*C)qs<6n1s_u(q@^S3Qu zU%x$V%nc??d{ob)SLJZHnDPx=|pTu7-bBOKv)DbIC3>PNE|Ip z7Rz^=W_ z+?_9P4-n1@pS9ULdN=RBwU_VQy3!_a>eBU{_Lfp$FspK=-p=$by-RW?ev2h`yY#ec8dhUWpXxBo&C= m{?D>aCeOT)k?|`-EMwjA58|8xUekL(A>iri=d#Wzp$P!Y8i4Hp literal 0 HcmV?d00001 diff --git a/renderer/src/test/resources/scene2/golden/barcode-linear-203.zpl b/renderer/src/test/resources/scene2/golden/barcode-linear-203.zpl new file mode 100644 index 0000000..26de142 --- /dev/null +++ b/renderer/src/test/resources/scene2/golden/barcode-linear-203.zpl @@ -0,0 +1,3 @@ +^XA^PW406^LL203^LH0,0^CI28 +^FO16,16^GFA,7849,7849,47,0000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000001FFFFFFFFFFFFFC0000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000001FFFFFFFFFFFFFC0000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000001FFFFFFFFFFFFFC0000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000001FFFFFFFFFFFFFC0000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000001FFFFFFFFFFFFFC0000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000001FFFFFFFFFFFFFC0000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000001FFFFFFFFFFFFFC0000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000001FFFFFFFFFFFFFC0000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000001FFFFFFFFFFFFFC0000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000001FFFFFFFFFFFFFC0000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000001FFFFFFFFFFFFFC0000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000001FFFFFFFFFFFFFC0000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000001FFFFFFFFFFFFFC0000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000001FFFFFFFFFFFFFC0000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000000000000000000000000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000001FFFFFFFFFFFFFC0000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000001FFFFFFFFFFFFFC0000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000001FFFFFFFFFFFFFC0000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000001FFFFFFFFFFFFFC0000000F30C03CFC0CC0F303CFCC0F3F0330F3F0CFCFF3CF0F0F0F3C3C3F30F03F33C00000000001FFFFFFFFFFFFFC00000000000000000000000000000000000000000000000000000000000000000000000000000001FFFFFFFFFFFFFC00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001FFFFFFFFFFFFFC00000000000000000000000000000000000000000000000000000000000000000000000000000001FFFFFFFFFFFFFC0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001FFFFFFFFFFFFFC00000000000000000000000000000000000000000000000000000000000000000000000000000001FFFFFFFFFFFFFC00000000000000000000000000000000000000000000000000000000000000000000000000000001FFFFFFFFFFFFFC00000000000000000000000000000000000000000000000000000000000000000000000000000001FFFFFFFFFFFFFC00000000000000000000000000000000000000000000000000000000000000000000000000000001FFFFFFFFFFFFFC00000000000000000000000000000000000000000000000000000000000000000000000000000001FFFFFFFFFFFFFCFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE0001FFFFFFFFFFFFFCFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE0001FFFFFFFFFFFFFCFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE000000000000000000FFFFFFFFFFFFFFFFFC00000000381C7E3F1C71C7E3F038FC7038E3F1C0E3F1F8E000000001FE000000000000000000FFFFFFFFFFFFFFFFFC00000000381C7E3F1C71C7E3F038FC7038E3F1C0E3F1F8E000000001FE000000000000000000FFFFFFFFFFFFFFFFFC00000000381C7E3F1C71C7E3F038FC7038E3F1C0E3F1F8E000000001FE0001FFFFFFFFFFFFFCFFFFFFFFFFFFFFFFFC00000000381C7E3F1C71C7E3F038FC7038E3F1C0E3F1F8E000000001FE0001FFFFFFFFFFFFFCFFFFFFFFFFFFFFFFFC00000000381C7E3F1C71C7E3F038FC7038E3F1C0E3F1F8E000000001FE000000000000000000FFFFFFFFFFFFFFFFFC00000000381C7E3F1C71C7E3F038FC7038E3F1C0E3F1F8E000000001FE000000000000000000FFFFFFFFFFFFFFFFFC00000000381C7E3F1C71C7E3F038FC7038E3F1C0E3F1F8E000000001FE0001FFFFFFFFFFFFFCFFFFFFFFFFFFFFFFFC00000000381C7E3F1C71C7E3F038FC7038E3F1C0E3F1F8E000000001FE0001FFFFFFFFFFFFFCFFFFFFFFFFFFFFFFFC00000000381C7E3F1C71C7E3F038FC7038E3F1C0E3F1F8E000000001FE000000000000000000FFFFFFFFFFFFFFFFFC00000000381C7E3F1C71C7E3F038FC7038E3F1C0E3F1F8E000000001FE000000000000000000FFFFFFFFFFFFFFFFFC00000000381C7E3F1C71C7E3F038FC7038E3F1C0E3F1F8E000000001FE0001FFFFFFFFFFFFFCFFFFFFFFFFFFFFFFFC00000000381C7E3F1C71C7E3F038FC7038E3F1C0E3F1F8E000000001FE0001FFFFFFFFFFFFFCFFFFFFFFFFFFFFFFFC00000000381C7E3F1C71C7E3F038FC7038E3F1C0E3F1F8E000000001FE0001FFFFFFFFFFFFFCFFFFFFFFFFFFFFFFFC00000000381C7E3F1C71C7E3F038FC7038E3F1C0E3F1F8E000000001FE0001FFFFFFFFFFFFFCFFFFFFFFFFFFFFFFFC00000000381C7E3F1C71C7E3F038FC7038E3F1C0E3F1F8E000000001FE0001FFFFFFFFFFFFFCFFFFFFFFFFFFFFFFFC00000000381C7E3F1C71C7E3F038FC7038E3F1C0E3F1F8E000000001FE0001FFFFFFFFFFFFFCFFFFFFFFFFFFFFFFFC00000000381C7E3F1C71C7E3F038FC7038E3F1C0E3F1F8E000000001FE000000000000000000FFFFFFFFFFFFFFFFFC00000000381C7E3F1C71C7E3F038FC7038E3F1C0E3F1F8E000000001FE000000000000000000FFFFFFFFFFFFFFFFFC00000000381C7E3F1C71C7E3F038FC7038E3F1C0E3F1F8E000000001FE000000000000000000FFFFFFFFFFFFFFFFFC00000000381C7E3F1C71C7E3F038FC7038E3F1C0E3F1F8E000000001FE000000000000000000FFFFFFFFFFFFFFFFFC00000000381C7E3F1C71C7E3F038FC7038E3F1C0E3F1F8E000000001FE000000000000000000FFFFFFFFFFFFFFFFFC00000000381C7E3F1C71C7E3F038FC7038E3F1C0E3F1F8E000000001FE000000000000000000FFFFFFFFFFFFFFFFFC00000000381C7E3F1C71C7E3F038FC7038E3F1C0E3F1F8E000000001FE0001FFFFFFFFFFFFFCFFFFFFFFFFFFFFFFFC00000000381C7E3F1C71C7E3F038FC7038E3F1C0E3F1F8E000000001FE0001FFFFFFFFFFFFFCFFFFFFFFFFFFFFFFFC00000000381C7E3F1C71C7E3F038FC7038E3F1C0E3F1F8E000000001FE0001FFFFFFFFFFFFFCFFFFFFFFFFFFFFFFFC00000000381C7E3F1C71C7E3F038FC7038E3F1C0E3F1F8E000000001FE0001FFFFFFFFFFFFFCFFFFFFFFFFFFFFFFFC00000000381C7E3F1C71C7E3F038FC7038E3F1C0E3F1F8E000000001FE0001FFFFFFFFFFFFFCFFFFFFFFFFFFFFFFFC00000000381C7E3F1C71C7E3F038FC7038E3F1C0E3F1F8E000000001FE0001FFFFFFFFFFFFFCFFFFFFFFFFFFFFFFFC00000000381C7E3F1C71C7E3F038FC7038E3F1C0E3F1F8E000000001FE000000000000000000FFFFFFFFFFFFFFFFFC00000000381C7E3F1C71C7E3F038FC7038E3F1C0E3F1F8E000000001FE000000000000000000FFFFFFFFFFFFFFFFFC00000000381C7E3F1C71C7E3F038FC7038E3F1C0E3F1F8E000000001FE0001FFFFFFFFFFFFFCFFFFFFFFFFFFFFFFFC00000000381C7E3F1C71C7E3F038FC7038E3F1C0E3F1F8E000000001FE0001FFFFFFFFFFFFFCFFFFFFFFFFFFFFFFFC00000000381C7E3F1C71C7E3F038FC7038E3F1C0E3F1F8E000000001FE000000000000000000FFFFFFFFFFFFFFFFFC00000000381C7E3F1C71C7E3F038FC7038E3F1C0E3F1F8E000000001FE000000000000000000FFFFFFFFFFFFFFFFFC00000000381C7E3F1C71C7E3F038FC7038E3F1C0E3F1F8E000000001FE000000000000000000FFFFFFFFFFFFFFFFFC00000000381C7E3F1C71C7E3F038FC7038E3F1C0E3F1F8E000000001FE000000000000000000FFFFFFFFFFFFFFFFFC00000000381C7E3F1C71C7E3F038FC7038E3F1C0E3F1F8E000000001FE000000000000000000FFFFFFFFFFFFFFFFFC00000000381C7E3F1C71C7E3F038FC7038E3F1C0E3F1F8E000000001FE000000000000000000FFFFFFFFFFFFFFFFFC00000000381C7E3F1C71C7E3F038FC7038E3F1C0E3F1F8E000000001FE000000000000000000FFFFFFFFFFFFFFFFFC00000000381C7E3F1C71C7E3F038FC7038E3F1C0E3F1F8E000000001FE000000000000000000FFFFFFFFFFFFFFFFFC00000000381C7E3F1C71C7E3F038FC7038E3F1C0E3F1F8E000000001FE000000000000000000FFFFFFFFFFFFFFFFFC00000000381C7E3F1C71C7E3F038FC7038E3F1C0E3F1F8E000000001FE000000000000000000FFFFFFFFFFFFFFFFFC00000000381C7E3F1C71C7E3F038FC7038E3F1C0E3F1F8E000000001FE000000000000000000FFFFFFFFFFFFFFFFFC00000000381C7E3F1C71C7E3F038FC7038E3F1C0E3F1F8E000000001FE000000000000000000FFFFFFFFFFFFFFFFFC00000000381C7E3F1C71C7E3F038FC7038E3F1C0E3F1F8E000000001FE000000000000000000FFFFFFFFFFFFFFFFFC00000000381C7E3F1C71C7E3F038FC7038E3F1C0E3F1F8E000000001FE000000000000000000FFFFFFFFFFFFFFFFFC00000000381C7E3F1C71C7E3F038FC7038E3F1C0E3F1F8E000000001FE000000000000000000FFFFFFFFFFFFFFFFFC00000000381C7E3F1C71C7E3F038FC7038E3F1C0E3F1F8E000000001FE000000000000000000FFFFFFFFFFFFFFFFFC00000000381C7E3F1C71C7E3F038FC7038E3F1C0E3F1F8E000000001FE000000000000000000FFFFFFFFFFFFFFFFFC00000000381C7E3F1C71C7E3F038FC7038E3F1C0E3F1F8E000000001FE000000000000000000FFFFFFFFFFFFFFFFFC00000000381C7E3F1C71C7E3F038FC7038E3F1C0E3F1F8E000000001FE000000000000000000FFFFFFFFFFFFFFFFFC00000000381C7E3F1C71C7E3F038FC7038E3F1C0E3F1F8E000000001FE000000000000000000FFFFFFFFFFFFFFFFFC00000000381C7E3F1C71C7E3F038FC7038E3F1C0E3F1F8E000000001FE000000000000000000FFFFFFFFFFFFFFFFFC00000000381C7E3F1C71C7E3F038FC7038E3F1C0E3F1F8E000000001FE000000000000000000FFFFFFFFFFFFFFFFFC00000000381C7E3F1C71C7E3F038FC7038E3F1C0E3F1F8E000000001FE000000000000000000FFFFFFFFFFFFFFFFFC00000000381C7E3F1C71C7E3F038FC7038E3F1C0E3F1F8E000000001FE000000000000000000FFFFFFFFFFFFFFFFFC00000000381C7E3F1C71C7E3F038FC7038E3F1C0E3F1F8E000000001FE000000000000000000FFFFFFFFFFFFFFFFFC00000000381C7E3F1C71C7E3F038FC7038E3F1C0E3F1F8E000000001FE000000000000000000FFFFFFFFFFFFFFFFFC00000000381C7E3F1C71C7E3F038FC7038E3F1C0E3F1F8E000000001FE000000000000000000FFFFFFFFFFFFFFFFFC00000000381C7E3F1C71C7E3F038FC7038E3F1C0E3F1F8E000000001FE000000000000000000FFFFFFFFFFFFFFFFFC00000000381C7E3F1C71C7E3F038FC7038E3F1C0E3F1F8E000000001FE000000000000000000FFFFFFFFFFFFFFFFFC00000000381C7E3F1C71C7E3F038FC7038E3F1C0E3F1F8E000000001FE000000000000000000FFFFFFFFFFFFFFFFFC00000000381C7E3F1C71C7E3F038FC7038E3F1C0E3F1F8E000000001FE000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE000000000000000000^FS +^XZ diff --git a/renderer/src/test/resources/scene2/golden/barcode-linear-300.png b/renderer/src/test/resources/scene2/golden/barcode-linear-300.png new file mode 100644 index 0000000000000000000000000000000000000000..1de8563eba771ca91a9dfd42c4d56e008ad5226c GIT binary patch literal 417 zcmeAS@N?(olHy`uVBq!ia0y~yV2WU1VANp*3NV7R21x*ajN{BrD-zt_=(VZBx;q+5^ z^OmLD-s02rbYdfRd2P45_0`a1{o&t*@4hd7&$d7F{Oz6bXZID?UjH@yT7Fwv*7f{@ zx$cR7T3V;JV?m;aZZ2K5HKjCa+Hb9^ldfh)zFQsQ9kx7m=lUSs$hp&=?%S|o+Ln}v zh{~rC)5`lpH-Fu)qbmurdVA!w^7vJ%t1oZ5BR*~2?~Qf#B{!osS?QjxotkxW!@BaL zS&y!3%G6lf$^QBCrc7_nvSanO*5>^8&u86=KbE$}o9p%Y-Rn;v+o-RzMpai*ENqVd z)a1x}wqn;NOa3%5Op-W#^Obd)cpe}%JePrmZ602KC~u6{1-oD!M<;LFGq literal 0 HcmV?d00001 diff --git a/renderer/src/test/resources/scene2/golden/barcode-linear-300.zpl b/renderer/src/test/resources/scene2/golden/barcode-linear-300.zpl new file mode 100644 index 0000000..324f19e --- /dev/null +++ b/renderer/src/test/resources/scene2/golden/barcode-linear-300.zpl @@ -0,0 +1,4 @@ +^XA^PW600^LL300^LH0,0^CI28 +^FO63,24^GFA,6955,6955,65,FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F8000000000000001FFFFFFFFFFFFFFFFFFFF80FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F8000000000000001FFFFFFFFFFFFFFFFFFFF80FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F8000000000000001FFFFFFFFFFFFFFFFFFFF80FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F8000000000000001FFFFFFFFFFFFFFFFFFFF80FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F8000000000000001FFFFFFFFFFFFFFFFFFFF80FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F8000000000000001FFFFFFFFFFFFFFFFFFFF80FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F8000000000000001FFFFFFFFFFFFFFFFFFFF80FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F8000000000000001FFFFFFFFFFFFFFFFFFFF80FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F8000000000000001FFFFFFFFFFFFFFFFFFFF80FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F8000000000000001FFFFFFFFFFFFFFFFFFFF80FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F8000000000000001FFFFFFFFFFFFFFFFFFFF80FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F8000000000000001FFFFFFFFFFFFFFFFFFFF80FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F8000000000000001FFFFFFFFFFFFFFFFFFFF80FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F8000000000000001FFFFFFFFFFFFFFFFFFFF80FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F8000000000000001FFFFFFFFFFFFFFFFFFFF80FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F8000000000000001FFFFFFFFFFFFFFFFFFFF80FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F8000000000000001FFFFFFFFFFFFFFFFFFFF80FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F8000000000000001FFFFFFFFFFFFFFFFFFFF80FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F8000000000000001FFFFFFFFFFFFFFFFFFFF80FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F8000000000000001FFFFFFFFFFFFFFFFFFFF80FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F8000000000000001FFFFFFFFFFFFFFFFFFFF80FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F80000000000000000000000000000000000000FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F8000000000000001FFFFFFFFFFFFFFFFFFFF80FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F8000000000000001FFFFFFFFFFFFFFFFFFFF80FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F8000000000000001FFFFFFFFFFFFFFFFFFFF80FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F8000000000000001FFFFFFFFFFFFFFFFFFFF80FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F8000000000000001FFFFFFFFFFFFFFFFFFFF80FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F8000000000000001FFFFFFFFFFFFFFFFFFFF80FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F8000000000000001FFFFFFFFFFFFFFFFFFFF80FC7038007E3FE00E3803F1C01F8FF8E00FC7FC01C703F1FF038FF8FFF1F8FC0FC0FC0FC7E07E07FC703F007FC71F8000000000000001FFFFFFFFFFFFFFFFFFFF80000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001FFFFFFFFFFFFFFFFFFFF80^FS +^FO24,140^GFA,9108,9108,69,0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003FFFFFFFFFFFFFFFFFFFF0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003FFFFFFFFFFFFFFFFFFFF0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003FFFFFFFFFFFFFFFFFFFF0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003FFFFFFFFFFFFFFFFFFFF0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003FFFFFFFFFFFFFFFFFFFF0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003FFFFFFFFFFFFFFFFFFFF0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003FFFFFFFFFFFFFFFFFFFF0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003FFFFFFFFFFFFFFFFFFFF0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003FFFFFFFFFFFFFFFFFFFF0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003FFFFFFFFFFFFFFFFFFFF0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF000003FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF000003FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF000003FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF000003FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000000000000000000000FFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF00000000000000000000000000FFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF00000000000000000000000000FFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF00000000000000000000000000FFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF00000000000000000000000000FFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF000003FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF000003FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF000003FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF00000000000000000000000000FFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF00000000000000000000000000FFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF00000000000000000000000000FFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF000003FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF000003FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF000003FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF00000000000000000000000000FFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF00000000000000000000000000FFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF00000000000000000000000000FFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF000003FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF000003FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF000003FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF000003FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF000003FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF000003FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF000003FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF000003FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF000003FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF00000000000000000000000000FFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF00000000000000000000000000FFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF00000000000000000000000000FFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF00000000000000000000000000FFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF00000000000000000000000000FFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF00000000000000000000000000FFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF00000000000000000000000000FFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF00000000000000000000000000FFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF00000000000000000000000000FFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF000003FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF000003FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF000003FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF000003FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF000003FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF000003FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF000003FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF000003FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF000003FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF00000000000000000000000000FFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF00000000000000000000000000FFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF00000000000000000000000000FFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF000003FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF000003FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF000003FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF00000000000000000000000000FFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF00000000000000000000000000FFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF00000000000000000000000000FFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF00000000000000000000000000FFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF00000000000000000000000000FFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF00000000000000000000000000FFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF00000000000000000000000000FFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF00000000000000000000000000FFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF00000000000000000000000000FFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF00000000000000000000000000FFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF00000000000000000000000000FFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF00000000000000000000000000FFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF00000000000000000000000000FFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF00000000000000000000000000FFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF00000000000000000000000000FFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF00000000000000000000000000FFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF00000000000000000000000000FFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF00000000000000000000000000FFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF00000000000000000000000000FFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF00000000000000000000000000FFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF00000000000000000000000000FFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF00000000000000000000000000FFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF00000000000000000000000000FFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF00000000000000000000000000FFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF00000000000000000000000000FFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF00000000000000000000000000FFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF00000000000000000000000000FFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF00000000000000000000000000FFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF00000000000000000000000000FFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF00000000000000000000000000FFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF00000000000000000000000000FFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF00000000000000000000000000FFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF00000000000000000000000000FFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF00000000000000000000000000FFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF00000000000000000000000000FFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF00000000000000000000000000FFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF00000000000000000000000000FFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF00000000000000000000000000FFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF00000000000000000000000000FFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF00000000000000000000000000FFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF00000000000000000000000000FFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF00000000000000000000000000FFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF00000000000000000000000000FFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF00000000000000000000000000FFFFFFFFFFFFFFFC000000000000003E007C1FF83FF07C1F07C1FF83FF003E0FFC1F003E0F83FF07C00F83FF07FE0F8000000000000007FF00000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000000000000000000000^FS +^XZ diff --git a/renderer/src/test/resources/scene2/golden/barcode-linear-600.png b/renderer/src/test/resources/scene2/golden/barcode-linear-600.png new file mode 100644 index 0000000000000000000000000000000000000000..45ac9b10d8ba535f1d1fc11b3cbf5c3f2b26a037 GIT binary patch literal 1101 zcmeAS@N?(olHy`uVBq!ia0y~yVA;UHz!bp<6kv#&tF#>`AmZub7*Y}U_PS%9w5Nc} z#b;f|F9beH(>r^%MnpDoTGUG2w9ErJS4);NAD-y_!?Tg5fE5@e$Do$FV}xvy7u+wpQ)DX^PhkJuD|^H@~@>oH|p4* zo8J8V>EHR&H(!2T6cw*$A3yE#(+4l#&cFV-a_^o;_10@YJ}b?9K6L#vT*#dU)4XS|KbH2CpWiL08E*_EanCpWW0f|iIw}>;>~Kc zc_1s;t~mnjs4BU(wzCJouIN8E>&s4{JAgXFBBq1g()jgm1Upa%_q9BrhNi93 zP*ct2L5^(NdT195NImzO_s6U_uGJ^{3SorX8pX%6&xf;t!gP%^D>!JE+n)s4gc{7N zBInottQVK(du`tQ)WrC;Ld4E(jy(rYZRJhk30s#dkX%0r|BZQc_5YFI{qo1|UoV$WujJzn^UGve?S&PW++P!2^EuGICn0pDyxWFV zSNN49LSMCC>;?LD*SW-lr!;%t%?1bE8jrsQO`%KcB<^;U3aoi8BerJFr5F1mC);gL zIM@Z%Cb&lH>tT+tslO6|_Cb{&Tor_5`eNygd`+cu4>wH>mIBJ)#COfJFXh{9NN|1C zPS{XVoD6i`CA#Bl*RPp%7^27GhN9eNiRP)<(nloL#N1Xm298BG?d!Od{!Tb6hLNU- d6;Mn4%e0Qe_;g$08xBx8;pyt_Di@+SZQ literal 0 HcmV?d00001 diff --git a/renderer/src/test/resources/scene2/golden/barcode-linear-600.zpl b/renderer/src/test/resources/scene2/golden/barcode-linear-600.zpl new file mode 100644 index 0000000..938657b --- /dev/null +++ b/renderer/src/test/resources/scene2/golden/barcode-linear-600.zpl @@ -0,0 +1,4 @@ +^XA^PW1200^LL600^LH0,0^CI28 +^FO127,48^GFA,27348,27348,129,FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000007FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000007FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000007FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000007FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000007FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000007FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000007FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000007FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000007FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000007FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000007FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000007FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000007FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000007FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000007FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000007FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000007FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000007FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000007FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000007FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000007FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000007FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000007FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000007FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000007FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000007FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000007FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000007FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000007FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000007FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000007FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000007FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000007FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000007FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000007FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000007FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000007FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000007FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000007FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000007FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000007FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000007FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000007FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000007FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000007FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000007FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000007FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000007FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000007FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000007FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000007FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000007FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000007FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000007FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000007FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000007FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000007FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000007FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000007FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000007FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000007FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000007FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000007FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000007FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000007FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000007FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000007FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000007FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000007FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000007FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000FFF03F000FC000003FFC0FFFFC0000FC0FC0000FFF03F00003FFC0FFFFC0FC0000FFF03FFFF00003F03F000FFF03FFFF000FC0FFFFC0FFFFFF03FFC0FFF000FFF000FFF000FFF03FFC003FFC003FFFF03F000FFF00003FFFF03F03FFC0000000000000000000000000000000000000000000000000000000000000000000000000^FS +^FO48,278^GFA,36974,36974,139,0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF80000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF80000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF80000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF80000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF80000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF80000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF80000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF80000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF80000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF80000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF80000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF80000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF80000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF80000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF80000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF80000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF80000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF80000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF80000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF80000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF80000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF80000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF80000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF80000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF80000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF80000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF80000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF80000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF80000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF80000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF80000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF80000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF80000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF80000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF80000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF80000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF80000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF80000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF80000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF80000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF80000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF80000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF80000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF80000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF80000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF80000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF80000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF80000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF80000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF80000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF80000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF80000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF80000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF80000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF80000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF80000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF80000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF80000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF80000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF80000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF80000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF80000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF80000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000000000000000000003FC0003FC03FFFC03FFFC03FC03FC03FC03FFFC03FFFC0003FC03FFFC03FC0003FC03FC03FFFC03FC0003FC03FFFC03FFFC03FC000000000000000000000003FFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF800000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF800000000000000000000000000000000000000000000000000000^FS +^XZ diff --git a/renderer/src/test/resources/scene2/golden/barcode-matrix-203.png b/renderer/src/test/resources/scene2/golden/barcode-matrix-203.png new file mode 100644 index 0000000000000000000000000000000000000000..1d9e224a280017d8440e63ea4b28d20de953cd5d GIT binary patch literal 622 zcmV-!0+IcRP)Gt^Qs zR&y7}b%gEArL*^09_2lU!!iv2>QqH%?j8G>cP>f;GC6*x<^C*QMAgd1aYr5xR zIoF`?5MFgKd@@V=P9$e>uOhtmk7lwWeJ7F=-m552XF)6neyIrOCQ>`!sDlNu9DV3T zI5&~nS=_4#sU2M?5zbAdb{6+4LUPCUBAD_dcbdihz?0m$!&$LGa_5bH9m~;0FT%Nr zYh0J35QPPf{InA6q|cb21uDFXa5Ucy?Dyu=q<5NH(frq>~Krt zeg-|+f|m{6>@ijkE-_B$+fj00DKk!?gvw&Z&E|B1H8`sOXV|Ng+vb?p3G z<4y$5S-o`QKf~B#_ip{Jo?>}8wDa_gb0+IgFkUZ-wR;n|;PixNiSHkuz0$hl&4rq+ zaULt&>b5DldajF(p0aw|x7*zoLc7JR#D8jvKMe?<(EV&;u)Rw7^*cPDtoEG#a{K7l z)3fw~_HQcv`Cj}~@y9Ed_SN>kxoy4f6yKi^<1>kQ^Pd=nOMP^5D%3l!>>)31bMoqk zQ!AQGA9n11v$^xy`cE-m4}7*TnZ0%9+V1Y$*R3q8E*k51D6YA9bJvQ{rAlV8zM_6< z+w@K?yw`0Jv`b@Se0EajpN`X?Z2V-ow|`yzU8%4B-KzN?Yps)~UwLshPFw1pgVwx@ zra;|0-|g*KY$N{m+LqMgzpu((zhvCHvv%6*zWJBCia9qgT>BwgHt}3Rr)${7PyZUS zyRNT)6f?PD4qXYXIVWa(>izbx{Y{-#pj>lypAgmG4rIBK0;3NOEA_L!H`YIUb!%mObWwSs;fuTT)#em&bd d@%>}Ju3x=P=akfXy`!My?dj_0vd$@?2>{QUimw0w literal 0 HcmV?d00001 diff --git a/renderer/src/test/resources/scene2/golden/barcode-matrix-300.zpl b/renderer/src/test/resources/scene2/golden/barcode-matrix-300.zpl new file mode 100644 index 0000000..26ba81f --- /dev/null +++ b/renderer/src/test/resources/scene2/golden/barcode-matrix-300.zpl @@ -0,0 +1,3 @@ +^XA^PW600^LL300^LH0,0^CI28 +^FO54,78^GFA,10044,10044,62,FFFFFFFFE0F83E0003FFFFFFFF800000000000000000000000000000000000000000000000000000000000000000000000000003F000FFFFC003F0000000FFFFFFFFE0F83E0003FFFFFFFF800000000000000000000000000000000000000000000000000000000000000000000000000003F000FFFFC003F0000000FFFFFFFFE0F83E0003FFFFFFFF8000000000000000000000000007FFFFFFFFF80000000000000000000000000000000000000003F000FFFFC003F0000000FFFFFFFFE0F83E0003FFFFFFFF8000000000000000000000000007FFFFFFFFF80000000000000000000000000000000000000003F000FFFFC003F0000000FFFFFFFFE0F83E0003FFFFFFFF8000000000000000000000000007FFFFFFFFF80000000000000000000000000000000000000003F000FFFFC003F0000000F8000003E007C1FF83E000000F8000000000000000000000000007FFFFFFFFF80000000000000000000000000000000000000003F000FFFFC003F0000000F8000003E007C1FF83E000000F8000000000000000000000000007FFFFFFFFF80000000000000000000000000000000000003F03F000FFFFFFFFF03FFC00F8000003E007C1FF83E000000F8000000000000000000000000007C1F07C00000000000000000000000000000000000000003F03F000FFFFFFFFF03FFC00F8000003E007C1FF83E000000F8000000000000000000000000007C1F07C00000000000000000000000000000000000000003F03F000FFFFFFFFF03FFC00F8000003E007C1FF83E000000F8000000000000000000000000007C1F07C00000000000000000000000000000000000000003F03F000FFFFFFFFF03FFC00F83FFF83E007C00F83E0FFFE0F8000000000000000000000000007C1F07C00000000000000000000000000000000000000003F03F000FFFFFFFFF03FFC00F83FFF83E007C00F83E0FFFE0F8000000000000000000000000007C1F07C00000000000000000000000000000000000000003F03F000FFFFFFFFF03FFC00F83FFF83E007C00F83E0FFFE0F8000000000000000000000000007C00FFC1FF80000000000000000000000000000000000003F03FFC000003F00003FFC00F83FFF83E007C00F83E0FFFE0F8000000000000000000000000007C00FFC1FF80000000000000000000000000000000000003F03FFC000003F00003FFC00F83FFF83E007C00F83E0FFFE0F8000000000000000000000000007C00FFC1FF80000000000000000000000000000000000003F03FFC000003F00003FFC00F83FFF83E0F8000F83E0FFFE0F8000000000000000000000000007C00FFC1FF80000000000000000000000000000000000003F03FFC000003F00003FFC00F83FFF83E0F8000F83E0FFFE0F8000000000000000000000000007C00FFC1FF80000000000000000000000000000000000003F03FFC000003F00003FFC00F83FFF83E0F8000F83E0FFFE0F8000000000000000000000000007FE00001F000000000000000000000000000000000000003F03FFC000003F00003FFC00F83FFF83E0F8000F83E0FFFE0F8000000000000000000000000007FE00001F000000000000000000000000000000000000003F03FFFFFFFFFFFFFFFFFFF0F83FFF83E0F8000F83E0FFFE0F8000000000000000000000000007FE00001F000000000000000000000000000000000000003F03FFFFFFFFFFFFFFFFFFF0F83FFF83E007C1F003E0FFFE0F8000000000000000000000000007FE00001F000000000000000000000000000000000000003F03FFFFFFFFFFFFFFFFFFF0F83FFF83E007C1F003E0FFFE0F8000000000000000000000000007FE00001F000000000000000000000000000000000000003F03FFFFFFFFFFFFFFFFFFF0F83FFF83E007C1F003E0FFFE0F8000000000000000000000000007C00FFC00F80000000000000000000000000000000000003F03FFFFFFFFFFFFFFFFFFF0F83FFF83E007C1F003E0FFFE0F8000000000000000000000000007C00FFC00F80000000000000000000000000000000000003F03FFFFFFFFFFFFFFFFFFF0F83FFF83E007C1F003E0FFFE0F8000000000000000000000000007C00FFC00F800000000000000000000000000000000000000FC0FC0000000000FC003F0F8000003E0003FF003E000000F8000000000000000000000000007C00FFC00F800000000000000000000000000000000000000FC0FC0000000000FC003F0F8000003E0003FF003E000000F8000000000000000000000000007C00FFC00F800000000000000000000000000000000000000FC0FC0000000000FC003F0F8000003E0003FF003E000000F8000000000000000000000000007C00F80000000000000000000000000000000000000000000FC0FC0000000000FC003F0F8000003E0003FF003E000000F8000000000000000000000000007C00F80000000000000000000000000000000000000000000FC0FC0000000000FC003F0F8000003E0003FF003E000000F8000000000000000000000000007C00F80000000000000000000000000000000000000000000FC0FC0000000000FC003F0FFFFFFFFE0F83E0F83FFFFFFFF8000000000000000000000000007C00F8000000000000000000000000000000000000000003F000FC0FFFFFFFC0FC0FC00FFFFFFFFE0F83E0F83FFFFFFFF8000000000000000000000000007C00F8000000000000000000000000000000000000000003F000FC0FFFFFFFC0FC0FC00FFFFFFFFE0F83E0F83FFFFFFFF8000000000000000000000000007FFF07FE0F80000000000000000000000000000000000003F000FC0FFFFFFFC0FC0FC00FFFFFFFFE0F83E0F83FFFFFFFF8000000000000000000000000007FFF07FE0F80000000000000000000000000000000000003F000FC0FFFFFFFC0FC0FC00FFFFFFFFE0F83E0F83FFFFFFFF8000000000000000000000000007FFF07FE0F80000000000000000000000000000000000003F000FC0FFFFFFFC0FC0FC00000000000007FE0F80000000000000000000000000000000000007FFF07FE0F80000000000000000000000000000000000003F000FC0FFFFFFFC0FC0FC00000000000007FE0F80000000000000000000000000000000000007FFF07FE0F80000000000000000000000000000000000003F000FC0FC0000FC0FFF03F0000000000007FE0F80000000000000000000000000000000000007C0007C1F000000000000000000000000000000000000003F000FC0FC0000FC0FFF03F0000000000007FE0F80000000000000000000000000000000000007C0007C1F000000000000000000000000000000000000003F000FC0FC0000FC0FFF03F0000000000007FE0F80000000000000000000000000000000000007C0007C1F000000000000000000000000000000000000003F000FC0FC0000FC0FFF03F0003E0FFFE0F83E0FFC0007C00F8000000000000000000000000007C0007C1F000000000000000000000000000000000000003F000FC0FC0000FC0FFF03F0003E0FFFE0F83E0FFC0007C00F8000000000000000000000000007C0007C1F000000000000000000000000000000000000003F000FC0FC0000FC0FFF03F0003E0FFFE0F83E0FFC0007C00F8000000000000000000000000007C1F003E0F80000000000000000000000000000000000003F03FFC0FC0FC0FC0FFFFFF0003E0FFFE0F83E0FFC0007C00F8000000000000000000000000007C1F003E0F80000000000000000000000000000000000003F03FFC0FC0FC0FC0FFFFFF0003E0FFFE0F83E0FFC0007C00F8000000000000000000000000007C1F003E0F80000000000000000000000000000000000003F03FFC0FC0FC0FC0FFFFFF007C1FF8000F801F07FFF07FE0F8000000000000000000000000007C1F003E0F80000000000000000000000000000000000003F03FFC0FC0FC0FC0FFFFFF007C1FF8000F801F07FFF07FE0F8000000000000000000000000007C1F003E0F80000000000000000000000000000000000003F03FFC0FC0FC0FC0FFFFFF007C1FF8000F801F07FFF07FE0F8000000000000000000000000007FFF07FE0000000000000000000000000000000000000003F03FFC0FC0FC0FC0FFFFFF007C1FF8000F801F07FFF07FE0F8000000000000000000000000007FFF07FE00000000000000000000000000000000000000000FFFFC0FC0000FC0FFF000007C1FF8000F801F07FFF07FE0F8000000000000000000000000007FFF07FE00000000000000000000000000000000000000000FFFFC0FC0000FC0FFF000007C00FFFE0F801F00000F801FF8000000000000000000000000007FFF07FE00000000000000000000000000000000000000000FFFFC0FC0000FC0FFF000007C00FFFE0F801F00000F801FF8000000000000000000000000007FFF07FE00000000000000000000000000000000000000000FFFFC0FC0000FC0FFF000007C00FFFE0F801F00000F801FF8000000000000000000000000007FE007C00F800000000000000000000000000000000000000FFFFC0FC0000FC0FFF000007C00FFFE0F801F00000F801FF8000000000000000000000000007FE007C00F800000000000000000000000000000000000000FFFFC0FC0000FC0FFF000007C00FFFE0F801F00000F801FF8000000000000000000000000007FE007C00F800000000000000000000000000000000000000FFFFC0FFFFFFFC0FC003F0F801F00000FFFFFFFFFF07C1FF8000000000000000000000000007FE007C00F800000000000000000000000000000000000000FFFFC0FFFFFFFC0FC003F0F801F00000FFFFFFFFFF07C1FF8000000000000000000000000007FE007C00F800000000000000000000000000000000000000FFFFC0FFFFFFFC0FC003F0F801F00000FFFFFFFFFF07C1FF8000000000000000000000000007C00F83E00000000000000000000000000000000000000000FFFFC0FFFFFFFC0FC003F0F801F00000FFFFFFFFFF07C1FF8000000000000000000000000007C00F83E00000000000000000000000000000000000000000FFFFC0FFFFFFFC0FC003F0F801F00000FFFFFFFFFF07C1FF8000000000000000000000000007C00F83E00000000000000000000000000000000000000000FFFFC0FFFFFFFC0FC003F0FFFFFF83FFFFC1F003E0FFFE0F8000000000000000000000000007C00F83E00000000000000000000000000000000000000000FC0FC0000000000FFFFFF0FFFFFF83FFFFC1F003E0FFFE0F8000000000000000000000000007C00F83E00000000000000000000000000000000000000000FC0FC0000000000FFFFFF0FFFFFF83FFFFC1F003E0FFFE0F8000000000000000000000000007FE0FFC1FF800000000000000000000000000000000000000FC0FC0000000000FFFFFF0FFFFFF83FFFFC1F003E0FFFE0F8000000000000000000000000007FE0FFC1FF800000000000000000000000000000000000000FC0FC0000000000FFFFFF0FFFFFF83FFFFC1F003E0FFFE0F8000000000000000000000000007FE0FFC1FF800000000000000000000000000000000000000FC0FC0000000000FFFFFF00000000000FFC0007C1FFFC0000000000000000000000000000007FE0FFC1FF800000000000000000000000000000000000000FC0FC0000000000FFFFFF00000000000FFC0007C1FFFC0000000000000000000000000000007FE0FFC1FF800000000000000000000000000000000000000000FFFFFFFFFFFFFFF00000000000000FFC0007C1FFFC0000000000000000000000000000007C1F07FE00000000000000000000000000000000000000000000FFFFFFFFFFFFFFF00000000000000FFC0007C1FFFC0000000000000000000000000000007C1F07FE00000000000000000000000000000000000000000000FFFFFFFFFFFFFFF00000000000000FFC0007C1FFFC0000000000000000000000000000007C1F07FE00000000000000000000000000000000000000000000FFFFFFFFFFFFFFF0000FFFFFFFFE007C00FFFE0FFC1F00000000000000000000000000007C1F07FE00000000000000000000000000000000000000000000FFFFFFFFFFFFFFF0000FFFFFFFFE007C00FFFE0FFC1F00000000000000000000000000007C1F07FE00000000000000000000000000000000000000000000FFFFFFFFFFFFFFF0000FFFFFFFFE007C00FFFE0FFC1F00000000000000000000000000007C0007FE0F800000000000000000000000000000000000000000000FC0FC003F0000000FFFFFFFFE007C00FFFE0FFC1F00000000000000000000000000007C0007FE0F800000000000000000000000000000000000000000000FC0FC003F0000000FFFFFFFFE007C00FFFE0FFC1F00000000000000000000000000007C0007FE0F800000000000000000000000000000000000000000000FC0FC003F0000000F8000003E0FFC1F0001F003FF00000000000000000000000000007C0007FE0F800000000000000000000000000000000000000000000FC0FC003F0000000F8000003E0FFC1F0001F003FF00000000000000000000000000007C0007FE0F800000000000000000000000000000000000000000000FC0FC003F0000000F8000003E0FFC1F0001F003FF00000000000000000000000000007C1F07C1F0000000000000000000000000000000000000000000000FC0FC003F0000000F8000003E0FFC1F0001F003FF00000000000000000000000000007C1F07C1F000000000000000000000000000000000000003F000FFF03F03F03F03FFFF0F8000003E0FFC1F0001F003FF00000000000000000000000000007C1F07C1F000000000000000000000000000000000000003F000FFF03F03F03F03FFFF0F83FFF83E0F83E0FFFE0F83FFF8000000000000000000000000007C1F07C1F000000000000000000000000000000000000003F000FFF03F03F03F03FFFF0F83FFF83E0F83E0FFFE0F83FFF8000000000000000000000000007C1F07C1F000000000000000000000000000000000000003F000FFF03F03F03F03FFFF0F83FFF83E0F83E0FFFE0F83FFF8000000000000000000000000007FFFFFFFFF80000000000000000000000000000000000003F000FFF03F03F03F03FFFF0F83FFF83E0F83E0FFFE0F83FFF8000000000000000000000000007FFFFFFFFF80000000000000000000000000000000000003F000FFF03F03F03F03FFFF0F83FFF83E0F83E0FFFE0F83FFF8000000000000000000000000007FFFFFFFFF80000000000000000000000000000000000000003FFFF000000000000FFF0F83FFF83E007FE0003FF003FF00000000000000000000000000007FFFFFFFFF80000000000000000000000000000000000000003FFFF000000000000FFF0F83FFF83E007FE0003FF003FF00000000000000000000000000007FFFFFFFFF80000000000000000000000000000000000000003FFFF000000000000FFF0F83FFF83E007FE0003FF003FF00000000000000000000000000007C1F07C00000000000000000000000000000000000000000003FFFF000000000000FFF0F83FFF83E007FE0003FF003FF00000000000000000000000000007C1F07C00000000000000000000000000000000000000000003FFFF000000000000FFF0F83FFF83E007FE0003FF003FF00000000000000000000000000007C1F07C00000000000000000000000000000000000000000003FFFF000000000000FFF0F83FFF83E0F83E0FFC00FFFE0F8000000000000000000000000007C1F07C0000000000000000000000000000000000000000000000000000000000000000F83FFF83E0F83E0FFC00FFFE0F8000000000000000000000000007C1F07C0000000000000000000000000000000000000000000000000000000000000000F83FFF83E0F83E0FFC00FFFE0F8000000000000000000000000007FFFFFC00F8000000000000000000000000000000000000000000000000000000000000F83FFF83E0F83E0FFC00FFFE0F8000000000000000000000000007FFFFFC00F8000000000000000000000000000000000000000000000000000000000000F83FFF83E0F83E0FFC00FFFE0F8000000000000000000000000007FFFFFC00F8000000000000000000000000000000000000000000000000000000000000F8000003E0003E007FFF0001FF8000000000000000000000000007FFFFFC00F8000000000000000000000000000000000000000000000000000000000000F8000003E0003E007FFF0001FF8000000000000000000000000007FFFFFC00F8000000000000000000000000000000000000000000000000000000000000F8000003E0003E007FFF0001FF8000000000000000000000000007C1F07C1F00000000000000000000000000000000000000000000000000000000000000F8000003E0003E007FFF0001FF8000000000000000000000000007C1F07C1F00000000000000000000000000000000000000000000000000000000000000F8000003E0003E007FFF0001FF8000000000000000000000000007C1F07C1F00000000000000000000000000000000000000000000000000000000000000FFFFFFFFE007FFFFFC00F83E0F8000000000000000000000000007C1F07C1F00000000000000000000000000000000000000000000000000000000000000FFFFFFFFE007FFFFFC00F83E0F8000000000000000000000000007C1F07C1F00000000000000000000000000000000000000000000000000000000000000FFFFFFFFE007FFFFFC00F83E0F8000000000000000000000000007FFF0001FF8000000000000000000000000000000000000000000000000000000000000FFFFFFFFE007FFFFFC00F83E0F8000000000000000000000000007FFF0001FF8000000000000000000000000000000000000000000000000000000000000FFFFFFFFE007FFFFFC00F83E0F8000000000000000000000000007FFF0001FF8000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007FFF0001FF8000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007FFF0001FF8000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007C00F800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007C00F800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007C00F800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007C00F800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007C00F800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007FFFF83FFF8000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007FFFF83FFF8000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007FFFF83FFF8000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007FFFF83FFF8000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007FFFF83FFF8000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007C0007C1F00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007C0007C1F00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007C0007C1F00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007C0007C1F00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007C0007C1F00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007C000001FF8000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007C000001FF8000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007C000001FF8000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007C000001FF8000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007C000001FF8000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007C1F003E000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007C1F003E000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007C1F003E000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007C1F003E000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007C1F003E000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007C00F8000F8000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007C00F8000F8000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007C00F8000F8000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007C00F8000F8000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007C00F8000F8000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007C1F07C0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007C1F07C0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007C1F07C0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007C1F07C0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007C1F07C0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007FFFF801FF8000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007FFFF801FF8000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007FFFF801FF8000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007FFFF801FF8000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007FFFF801FF8000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007FFFFFFFF00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007FFFFFFFF00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007FFFFFFFF00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007FFFFFFFF00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007FFFFFFFF00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007FFFF83E0F8000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007FFFF83E0F8000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007FFFF83E0F8000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007FFFF83E0F8000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007FFFF83E0F8000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007C1F07C1F00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007C1F07C1F00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007C1F07C1F00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007C1F07C1F00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007C1F07C1F00000000000000000000000000000000000000000000000000000000000000^FS +^XZ diff --git a/renderer/src/test/resources/scene2/golden/barcode-matrix-600.png b/renderer/src/test/resources/scene2/golden/barcode-matrix-600.png new file mode 100644 index 0000000000000000000000000000000000000000..aee117e44a2b5f9712fc25510017fcb886a88e1a GIT binary patch literal 1372 zcmeAS@N?(olHy`uVBq!ia0y~yVA;UHz!bp<6kv#&tF)bgfmPYl#WAEJ?(JR2ut#1J z4i8^FKA`BGpsB+C?zXh-?I`!8Q(eon`U%q>{D^xmi;+o+5$4>pe`uofG<0pT; zT(dv3IBja!M~2rI(>8gXPI>!irbhObn~NfUuDzW-Uy4mWa#PCLwyBrCZ4ofHpC780 za`(4lj(pBj;yLWTj-q`uAypvS3#nu?@f3LDJIHc$Ob@kteQ%=r$WgYp|`*r7% zryEW=SV#z;{`OZ@F!&@;>8_ibuWgLD=U2jC{#f?NrU=DF&F8JeQe)rhh1L8ESsquI zRPcY+KP5)H0yTVJV_nbdJQUBKQgQ+Jc zm0t8u;JsC9U9zI{kaEBavNz22?CN2lbzT4_B~CvsEU z$z2{{+^hE+*iOy9czS96)Aw`x`2P!p$=1yJ3pwBaIOaUrQ@m16Q*7a~540kQ}LH>X}b-<)2q zw?78XIWY Date: Thu, 12 Nov 2026 18:19:02 -0600 Subject: [PATCH 2/2] fix(renderer): scene 2 barcodes after independent review: no symbol reads as another value, rules by symbology, nothing painted over a symbol A label is scanned, so a symbol must never read as something other than the value it was given. Record values are untrusted and stay out of every diagnostic and exception. - No value, literal or bound, may hold the two characters an escape of the encoder begins with. \ in a plain Code 128 value drew a GS1-128 of the record's choosing, \ set the reader-programming flag, and QR dropped the escape. Refused in every symbology, with a count only. - Interleaved 2 of 5 of an odd number of digits is refused, not padded. - EAN and UPC values are the whole number with its check digit, which is verified; a shorter number is refused, never completed or padded. - The check digit of every GS1 key in a gs1-128 element string is verified: the identifiers the GS1 Syntax Dictionary marks, as the bundled Zint carries it. - VALUE_NOT_ENCODABLE joins the errors for these and for a value a symbology cannot hold; SCENE_INVALID is no longer used for a value. A symbol of more modules than the encoder draws is BARCODE_DOES_NOT_FIT. - Quiet zones and the smallest module are a table by symbology (scene2/barcode/Symbology), each number with its source. barcodeProfile names the rule: none, gs1-general or gs1-logistics; not stated, the strictest written for the symbology. A GS1-128 is so held to 0.495 mm unless its scene says otherwise, at which a GTIN with a date and a serial number fits no label 4 in wide. - The bars of a linear symbol must be as tall as its symbology asks, and the height is part of the fit. minHeight overrides, with the standing warning BARCODE_HEIGHT_BELOW_RECOMMENDED. - QUIET_ZONE_INTRUDED is an error: an element later in the list whose ink would enter a symbol or its quiet zones is refused, when a scene is validated as when it is rendered. - A barcode sits in the middle of its box unless the scene says otherwise. - A bound barcode takes maxLength and is proven at validation in a linear symbology. FIT_NOT_PROVEN stays for no length and for two-dimensional symbols, whose encoders have no largest symbol of a length. Codes and property names are added in one change: foundation's FailureCode and warning set, the preview's sentences, the bridge's accepted refusals and schema vocabulary, the renderer's vocabulary and the schema. The three barcode goldens are unchanged byte for byte; barcode-retail is new. Co-Authored-By: Claude Fable 5.2 --- .../renderer/RendererVersion.java | 5 +- .../renderer/SceneFacts.java | 1 + .../renderer/BridgeTest.java | 2 +- .../renderer/SceneFactsTest.java | 2 +- docs/design/LABEL-LAYOUT-FONTS-PREVIEW.md | 130 ++- .../documentgeneration/GenerationPorts.java | 6 +- .../GenerationPreviewWidget.java | 4 +- .../GenerationPortsContractTest.java | 5 +- renderer/README.md | 21 +- renderer/schema/zpl-scene-2.schema.json | 19 +- .../qbits/renderer/scene2/BoxElement.java | 32 +- .../qbits/renderer/scene2/Diagnostic.java | 5 +- .../qbits/renderer/scene2/Frame.java | 10 + .../qbits/renderer/scene2/GroupElement.java | 11 + .../qbits/renderer/scene2/Scene2.java | 49 +- .../qbits/renderer/scene2/SceneElement.java | 24 + .../scene2/barcode/BarcodeElement.java | 405 +++++++--- .../scene2/barcode/BarcodeFactory.java | 6 +- .../renderer/scene2/barcode/ModuleFit.java | 119 ++- .../renderer/scene2/barcode/Payload.java | 200 +++++ .../renderer/scene2/barcode/Symbology.java | 320 ++++++++ .../qbits/renderer/MutationCampaignTest.java | 23 +- .../renderer/scene2/DiagnosticsTest.java | 5 +- .../qbits/renderer/scene2/Scene2Test.java | 22 +- .../scene2/barcode/BarcodeDecodeTest.java | 111 ++- .../scene2/barcode/BarcodeElementTest.java | 742 +++++++++++++++--- .../scene2/barcode/BarcodeFixture.java | 20 + .../scene2/barcode/BarcodePropertyTest.java | 70 +- .../scene2/barcode/ModuleFitTest.java | 129 +-- .../renderer/scene2/barcode/PayloadTest.java | 313 ++++++++ .../scene2/barcode/SymbologyTest.java | 291 +++++++ .../resources/scene2/cases/barcode-gs1.json | 2 +- .../scene2/cases/barcode-linear.json | 4 +- .../scene2/cases/barcode-matrix.json | 2 +- .../scene2/cases/barcode-retail.json | 12 + .../scene2/golden/barcode-retail-203.png | Bin 0 -> 272 bytes .../scene2/golden/barcode-retail-203.zpl | 3 + .../scene2/golden/barcode-retail-300.png | Bin 0 -> 456 bytes .../scene2/golden/barcode-retail-300.zpl | 3 + .../scene2/golden/barcode-retail-600.png | Bin 0 -> 1415 bytes .../scene2/golden/barcode-retail-600.zpl | 4 + 41 files changed, 2655 insertions(+), 477 deletions(-) create mode 100644 renderer/src/main/java/com/kingsrook/qbits/renderer/scene2/barcode/Payload.java create mode 100644 renderer/src/main/java/com/kingsrook/qbits/renderer/scene2/barcode/Symbology.java create mode 100644 renderer/src/test/java/com/kingsrook/qbits/renderer/scene2/barcode/PayloadTest.java create mode 100644 renderer/src/test/java/com/kingsrook/qbits/renderer/scene2/barcode/SymbologyTest.java create mode 100644 renderer/src/test/resources/scene2/cases/barcode-retail.json create mode 100644 renderer/src/test/resources/scene2/golden/barcode-retail-203.png create mode 100644 renderer/src/test/resources/scene2/golden/barcode-retail-203.zpl create mode 100644 renderer/src/test/resources/scene2/golden/barcode-retail-300.png create mode 100644 renderer/src/test/resources/scene2/golden/barcode-retail-300.zpl create mode 100644 renderer/src/test/resources/scene2/golden/barcode-retail-600.png create mode 100644 renderer/src/test/resources/scene2/golden/barcode-retail-600.zpl diff --git a/bridges/renderer/src/main/java/com/kingsrook/qbits/documentgeneration/renderer/RendererVersion.java b/bridges/renderer/src/main/java/com/kingsrook/qbits/documentgeneration/renderer/RendererVersion.java index 5163828..ee4d9b9 100644 --- a/bridges/renderer/src/main/java/com/kingsrook/qbits/documentgeneration/renderer/RendererVersion.java +++ b/bridges/renderer/src/main/java/com/kingsrook/qbits/documentgeneration/renderer/RendererVersion.java @@ -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; diff --git a/bridges/renderer/src/main/java/com/kingsrook/qbits/documentgeneration/renderer/SceneFacts.java b/bridges/renderer/src/main/java/com/kingsrook/qbits/documentgeneration/renderer/SceneFacts.java index 20e06e7..1f2ef6f 100644 --- a/bridges/renderer/src/main/java/com/kingsrook/qbits/documentgeneration/renderer/SceneFacts.java +++ b/bridges/renderer/src/main/java/com/kingsrook/qbits/documentgeneration/renderer/SceneFacts.java @@ -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()); diff --git a/bridges/renderer/src/test/java/com/kingsrook/qbits/documentgeneration/renderer/BridgeTest.java b/bridges/renderer/src/test/java/com/kingsrook/qbits/documentgeneration/renderer/BridgeTest.java index cfab90b..8838312 100644 --- a/bridges/renderer/src/test/java/com/kingsrook/qbits/documentgeneration/renderer/BridgeTest.java +++ b/bridges/renderer/src/test/java/com/kingsrook/qbits/documentgeneration/renderer/BridgeTest.java @@ -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) -> diff --git a/bridges/renderer/src/test/java/com/kingsrook/qbits/documentgeneration/renderer/SceneFactsTest.java b/bridges/renderer/src/test/java/com/kingsrook/qbits/documentgeneration/renderer/SceneFactsTest.java index 6051361..e2352cf 100644 --- a/bridges/renderer/src/test/java/com/kingsrook/qbits/documentgeneration/renderer/SceneFactsTest.java +++ b/bridges/renderer/src/test/java/com/kingsrook/qbits/documentgeneration/renderer/SceneFactsTest.java @@ -60,7 +60,7 @@ class SceneFactsTest Set 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()); } diff --git a/docs/design/LABEL-LAYOUT-FONTS-PREVIEW.md b/docs/design/LABEL-LAYOUT-FONTS-PREVIEW.md index c8e3b19..5de6093 100644 --- a/docs/design/LABEL-LAYOUT-FONTS-PREVIEW.md +++ b/docs/design/LABEL-LAYOUT-FONTS-PREVIEW.md @@ -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 @@ -519,25 +526,94 @@ fit-qualified symbology, for short, typical and longest values. 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. -- **`QUIET_ZONE_INTRUDED` is not raised yet.** It needs the finished picture, after every element has painted; - the element contract has no such step and validation paints nothing. Ink painted over a symbol by a later - element is therefore not reported. It belongs with `ELEMENTS_OVERLAP`. -- **Refusals that depend on the record.** No code was added to the vocabulary. A bound value, symbology or - scanner profile that is absent is `VALUE_MISSING` with the pointer of the binding property. A value the - symbology cannot hold is `GS1_INVALID` for `gs1-128` and `SCENE_INVALID` at `value` or `valuePath` otherwise, - with a count of characters; what the encoder says about the value is dropped. A symbology outside - `allowedSymbologies`, and a scanner profile that does not name the symbology drawn, are `SCENE_INVALID` at that - property. A code of its own for "the record's value cannot be encoded" would read better to an operator. -- **A bound barcode is never proven at publish.** The design gives a barcode no `maxLength`, so validation - cannot fit a bound value and the element always carries `FIT_NOT_PROVEN`. A literal value in a stated - symbology is fitted when the scene is validated. -- **The numbers of a refusal.** `BARCODE_DOES_NOT_FIT` carries `measured.modules`, `quietModules`, `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. +- **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. No minimum bar height is enforced: the design names none. + 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 `\` to `\` 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 @@ -822,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`. diff --git a/foundation/src/main/java/com/kingsrook/qbits/documentgeneration/GenerationPorts.java b/foundation/src/main/java/com/kingsrook/qbits/documentgeneration/GenerationPorts.java index 94b2d69..ad0e813 100644 --- a/foundation/src/main/java/com/kingsrook/qbits/documentgeneration/GenerationPorts.java +++ b/foundation/src/main/java/com/kingsrook/qbits/documentgeneration/GenerationPorts.java @@ -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 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 INFORMATION = java.util.Set.of("TEXT_SHRUNK", "TEXT_WRAPPED", "BARCODE_MODULE", "IMAGE_SCALED"); /************************************************************************ diff --git a/foundation/src/main/java/com/kingsrook/qbits/documentgeneration/GenerationPreviewWidget.java b/foundation/src/main/java/com/kingsrook/qbits/documentgeneration/GenerationPreviewWidget.java index 33e523b..5c6d7f4 100644 --- a/foundation/src/main/java/com/kingsrook/qbits/documentgeneration/GenerationPreviewWidget.java +++ b/foundation/src/main/java/com/kingsrook/qbits/documentgeneration/GenerationPreviewWidget.java @@ -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."), diff --git a/foundation/src/test/java/com/kingsrook/qbits/documentgeneration/GenerationPortsContractTest.java b/foundation/src/test/java/com/kingsrook/qbits/documentgeneration/GenerationPortsContractTest.java index 3adf239..7b9f524 100644 --- a/foundation/src/test/java/com/kingsrook/qbits/documentgeneration/GenerationPortsContractTest.java +++ b/foundation/src/test/java/com/kingsrook/qbits/documentgeneration/GenerationPortsContractTest.java @@ -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()); diff --git a/renderer/README.md b/renderer/README.md index ca5f82f..a358d48 100644 --- a/renderer/README.md +++ b/renderer/README.md @@ -130,10 +130,23 @@ A source with schema `qqq-zpl-scene/2` is a layout in physical units that render - **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`** 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 `quiet` blank modules on each side (default and least: 10 for a linear symbol, 4 for a two-dimensional one; most: 64). It is never under `minModule` (default `0.25mm` linear, `0.375mm` two-dimensional; taken up to whole dots that is 2 and 3 dots at 203 dpi, 3 and 5 at 300 dpi, 6 and 9 at 600 dpi) and never over `maxModule`, which goes up to a whole dot by the same rule. A linear symbol is fitted along its width and its bars take the whole height of the box; `qr`, `datamatrix`, `pdf417` and `aztec` are fitted on both axes. Example: a 44-character GS1-128 is 319 modules with its quiet zones; in a box of `90mm` at 203 dpi (719 dots) the module is `719 / 319 = 2` dots (0.25 mm), the symbol 638 dots, and centred it leaves 40 and 41 dots. - - **Place.** The fitted symbol sits in the box by `align` and `valign` (default `left`, `top`), the odd dot to the right and below, and paints its own white ground, quiet zones included, so ink under 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. - - **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.quietModules`, `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`. 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 4000 characters is `VALUE_TOO_LONG`. A value the symbology cannot hold is `GS1_INVALID` for `gs1-128` and `SCENE_INVALID` at `value` or `valuePath` otherwise, with `measured.characters` and never the value; 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. Validation reads no record, so anything bound is not fitted then and warns `FIT_NOT_PROVEN`; a literal value in a stated symbology is fitted when the scene is validated. + - **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 `\` to `\` 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. diff --git a/renderer/schema/zpl-scene-2.schema.json b/renderer/schema/zpl-scene-2.schema.json index 8e51b94..84072e8 100644 --- a/renderer/schema/zpl-scene-2.schema.json +++ b/renderer/schema/zpl-scene-2.schema.json @@ -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,7 +126,7 @@ }, { "$ref": "#/$defs/placement", - "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; a two-dimensional symbol (`qr`, `datamatrix`, `pdf417`, `aztec`) is fitted on both axes. The fitted symbol is placed in the box by `align` and `valign`, and paints its own white ground, quiet zones included. The renderer is the authority: it also refuses a value the symbology cannot hold (GS1_INVALID for `gs1-128`, SCENE_INVALID at the value otherwise, with a count of characters and never the value), a quiet zone under 10 when any symbology the element may draw is linear, and a maximum module under the minimum. A value, symbology or scanner profile bound to the record is not known when a scene is validated: the warning FIT_NOT_PROVEN says so.", + "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, "allOf": [ {"oneOf": [{"required": ["symbology"], "not": {"anyOf": [{"required": ["symbologyPath"]}, {"required": ["allowedSymbologies"]}]}}, {"required": ["symbologyPath", "allowedSymbologies"], "not": {"required": ["symbology"]}}]}, @@ -138,14 +138,17 @@ "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. 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."}, - "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 4000 characters VALUE_TOO_LONG. No record is read when a scene is validated."}, - "minModule": {"$ref": "#/$defs/absolute", "description": "The smallest module, more than nothing, taken up to a whole dot. Default: 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). A stated minimum that is fewer dots than the default is drawn, with the warning BARCODE_MODULE_BELOW_RECOMMENDED every time the scene is validated or rendered."}, + "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 and least: 10 for a linear symbol, 4 for a two-dimensional one."}, + "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: a retail value arrives with its check digit, which is verified."}, + "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."} } diff --git a/renderer/src/main/java/com/kingsrook/qbits/renderer/scene2/BoxElement.java b/renderer/src/main/java/com/kingsrook/qbits/renderer/scene2/BoxElement.java index c908fbe..5fe040a 100644 --- a/renderer/src/main/java/com/kingsrook/qbits/renderer/scene2/BoxElement.java +++ b/renderer/src/main/java/com/kingsrook/qbits/renderer/scene2/BoxElement.java @@ -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 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)); } } diff --git a/renderer/src/main/java/com/kingsrook/qbits/renderer/scene2/Diagnostic.java b/renderer/src/main/java/com/kingsrook/qbits/renderer/scene2/Diagnostic.java index 4795d8a..bfd256e 100644 --- a/renderer/src/main/java/com/kingsrook/qbits/renderer/scene2/Diagnostic.java +++ b/renderer/src/main/java/com/kingsrook/qbits/renderer/scene2/Diagnostic.java @@ -40,8 +40,9 @@ import java.util.regex.Pattern; public final class Diagnostic { private static final Set 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 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 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 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}"); diff --git a/renderer/src/main/java/com/kingsrook/qbits/renderer/scene2/Frame.java b/renderer/src/main/java/com/kingsrook/qbits/renderer/scene2/Frame.java index c3f13d4..f252bc9 100644 --- a/renderer/src/main/java/com/kingsrook/qbits/renderer/scene2/Frame.java +++ b/renderer/src/main/java/com/kingsrook/qbits/renderer/scene2/Frame.java @@ -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. ***************************************************************************/ diff --git a/renderer/src/main/java/com/kingsrook/qbits/renderer/scene2/GroupElement.java b/renderer/src/main/java/com/kingsrook/qbits/renderer/scene2/GroupElement.java index 3db468a..63e2edd 100644 --- a/renderer/src/main/java/com/kingsrook/qbits/renderer/scene2/GroupElement.java +++ b/renderer/src/main/java/com/kingsrook/qbits/renderer/scene2/GroupElement.java @@ -83,6 +83,17 @@ final class GroupElement implements SceneElement + /*************************************************************************** + ** A group puts no ink anywhere; its children answer for theirs. + ***************************************************************************/ + @Override + public List ink(Frame frame, int dpi) + { + return (List.of()); + } + + + /*************************************************************************** ** A group has no ink of its own. ***************************************************************************/ diff --git a/renderer/src/main/java/com/kingsrook/qbits/renderer/scene2/Scene2.java b/renderer/src/main/java/com/kingsrook/qbits/renderer/scene2/Scene2.java index c4b2871..5ee01ec 100644 --- a/renderer/src/main/java/com/kingsrook/qbits/renderer/scene2/Scene2.java +++ b/renderer/src/main/java/com/kingsrook/qbits/renderer/scene2/Scene2.java @@ -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 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 elements, Layout layout, Canvas canvas, boolean paint, RenderInput input, Diagnostics diagnostics, List 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) { diff --git a/renderer/src/main/java/com/kingsrook/qbits/renderer/scene2/SceneElement.java b/renderer/src/main/java/com/kingsrook/qbits/renderer/scene2/SceneElement.java index b4d12c1..9671108 100644 --- a/renderer/src/main/java/com/kingsrook/qbits/renderer/scene2/SceneElement.java +++ b/renderer/src/main/java/com/kingsrook/qbits/renderer/scene2/SceneElement.java @@ -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 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. ***************************************************************************/ diff --git a/renderer/src/main/java/com/kingsrook/qbits/renderer/scene2/barcode/BarcodeElement.java b/renderer/src/main/java/com/kingsrook/qbits/renderer/scene2/barcode/BarcodeElement.java index a96de55..9cbaf2c 100644 --- a/renderer/src/main/java/com/kingsrook/qbits/renderer/scene2/barcode/BarcodeElement.java +++ b/renderer/src/main/java/com/kingsrook/qbits/renderer/scene2/barcode/BarcodeElement.java @@ -26,7 +26,6 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; import com.fasterxml.jackson.databind.JsonNode; -import com.kingsrook.qbits.renderer.BarcodeFit; import com.kingsrook.qbits.renderer.Barcodes; import com.kingsrook.qbits.renderer.RenderInput; import com.kingsrook.qbits.renderer.ScannerSymbology; @@ -38,7 +37,6 @@ 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.Refusal; import com.kingsrook.qbits.renderer.scene2.SceneElement; import uk.org.okapibarcode.backend.OkapiInputException; @@ -46,36 +44,58 @@ 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 - ** symbol as modules; `ModuleFit` chooses the module, a whole number of dots; - ** the symbol, quiet zones included, is placed in the box by `align` and - ** `valign` and painted as a white ground and black bars, so that nothing - ** painted under it can close a space or a quiet zone. + ** 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. + ** 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. ** - ** Nothing bound to the record is known when a scene is validated, so - ** validation reads no record: it fits a barcode whose symbology and value are - ** both literal, and warns `FIT_NOT_PROVEN` for any other. + ** 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 symbologies; + private final List symbologies; private final String symbologyPath; private final String value; private final String valuePath; @@ -83,14 +103,17 @@ final class BarcodeElement implements SceneElement 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: `length` modules from column - ** `column` of row `row`. + ** 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) { @@ -98,6 +121,16 @@ final class BarcodeElement implements SceneElement + /*************************************************************************** + ** 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 runs) + { + } + + + /*************************************************************************** ** A fitted symbol on the label, in dots: the area it takes, quiet zones ** included, and its bars. @@ -119,6 +152,13 @@ final class BarcodeElement implements SceneElement 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"); @@ -128,7 +168,7 @@ final class BarcodeElement implements SceneElement } if(stated != null) { - if(!stated.isTextual() || !BarcodeFit.TYPES.contains(stated.textValue())) + if(Symbology.of(stated.textValue()) == null) { throw source.invalid("symbology"); } @@ -136,7 +176,7 @@ final class BarcodeElement implements SceneElement { throw source.invalid("allowedSymbologies"); } - symbologies = List.of(stated.textValue()); + symbologies = List.of(Symbology.of(stated.textValue())); } else { @@ -144,14 +184,15 @@ final class BarcodeElement implements SceneElement { throw source.invalid("allowedSymbologies"); } - List listed = new ArrayList<>(); + List listed = new ArrayList<>(); for(JsonNode entry : allowed) { - if(!entry.isTextual() || !BarcodeFit.TYPES.contains(entry.textValue()) || listed.contains(entry.textValue())) + Symbology named = Symbology.of(entry.textValue()); + if(named == null || listed.contains(named)) { throw source.invalid("allowedSymbologies"); } - listed.add(entry.textValue()); + listed.add(named); } symbologies = List.copyOf(listed); } @@ -175,15 +216,13 @@ final class BarcodeElement implements SceneElement throw source.invalid(scannerPath == null ? "scannerSymbologyPath" : "aimIdentifierPath"); } - minModule = module(source, "minModule"); - maxModule = module(source, "maxModule"); - JsonNode zone = source.value("quiet"); - int fewest = symbologies.stream().mapToInt(ModuleFit::quiet).max().orElseThrow(); - if(zone != null && (!zone.isIntegralNumber() || !zone.canConvertToInt() || zone.intValue() < fewest || zone.intValue() > ModuleFit.MAXIMUM_QUIET_MODULES)) - { - throw source.invalid("quiet"); - } - quiet = zone == null ? null : Integer.valueOf(zone.intValue()); + 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); } @@ -208,9 +247,10 @@ final class BarcodeElement implements SceneElement /*************************************************************************** - ** A module size, more than nothing, or null when the element states none. + ** A module size or a bar height, more than nothing, or null when the + ** element states none. ***************************************************************************/ - private static Length module(ElementSource source, String name) + private static Length more(ElementSource source, String name) { Length stated = source.length(name, null, ElementSource.ABSOLUTE); if(stated != null && stated.thousandths() <= 0) @@ -222,6 +262,25 @@ final class BarcodeElement implements SceneElement + /*************************************************************************** + ** 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 @@ -269,6 +328,21 @@ final class BarcodeElement implements SceneElement + /*************************************************************************** + ** 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. ***************************************************************************/ @@ -296,64 +370,54 @@ final class BarcodeElement implements SceneElement /*************************************************************************** ** Resolve the symbology and the value, encode, fit and place. The answer is - ** what to draw, or null when something bound to the record is only being - ** validated. + ** 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 bound = symbologyPath != null || valuePath != null || scannerPath != null; - if(bound) + 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(bound && "VALIDATE".equals(input.operation)) + 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); } - String symbology = symbology(frame, input, diagnostics); - int[] limits = limits(List.of(symbology), dpi, frame, diagnostics); - String payload = payload(symbology, frame, input, diagnostics); - int zone = quiet == null ? ModuleFit.quiet(symbology) : quiet; - BufferedImage modules; - try - { - modules = Barcodes.renderForFormat("ZPL", symbology, payload, 1, zone, Wire.JSON.createObjectNode()); - } - catch(IllegalArgumentException | OkapiInputException rejected) - { - ////////////////////////////////////////////////////////////////////////////////////// - // the encoder cannot draw this value in this symbology. What it says may quote the // - // value, so nothing of it is kept: the refusal is the count of characters // - ////////////////////////////////////////////////////////////////////////////////////// - throw unusable("gs1-128".equals(symbology) ? "GS1_INVALID" : "SCENE_INVALID", payload, frame, diagnostics); - } + 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 matrix = ModuleFit.matrix(symbology); boolean upright = rotation % 180 == 0; - ModuleFit.Request request = new ModuleFit.Request(modules.getWidth(), matrix ? modules.getHeight() : 0, upright ? frame.width() : frame.height(), upright ? frame.height() : frame.width(), - limits[0], limits[1]); - ModuleFit.Fit fit = ModuleFit.fit(request); - Map measured = Map.of("modules", request.columns(), "quietModules", zone, "moduleDots", fit.module(), "widthDots", fit.width(), "heightDots", fit.height(), - "moduleMicrometres", Math.toIntExact(((long) fit.module() * MICROMETRES_PER_INCH + dpi / 2) / dpi)); - Map limit = Map.of("widthDots", request.along(), "heightDots", request.across(), "minModuleDots", request.minimum()); - if(!fit.fits()) + 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 drawn = new ArrayList<>(); + for(Run run : bars.runs()) { - throw diagnostics.refuse("BARCODE_DOES_NOT_FIT", id, null, frame, measured, limit); - } - diagnostics.inform("BARCODE_MODULE", id, null, frame, measured, limit); - - Frame area = Layout.align(frame, upright ? fit.width() : fit.height(), upright ? fit.height() : fit.width(), placement.align(), placement.valign()); - List bars = new ArrayList<>(); - for(Run run : runs(modules, matrix)) - { - int x = run.column() * fit.module(); - int y = run.row() * fit.module(); + 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(); - bars.add(switch(rotation) + 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); @@ -361,33 +425,42 @@ final class BarcodeElement implements SceneElement default -> new Frame(area.x() + x, area.y() + y, width, height); }); } - return (new Symbol(area, bars)); + return (new Symbol(area, drawn)); } /*************************************************************************** - ** The smallest and the largest module the scene allows at this resolution, - ** in dots, for the symbologies that may be drawn. A minimum the scene - ** states below the default 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. + ** The smallest module a symbology is drawn at here, in dots: what the + ** scene states, else what the symbology's profile asks. ***************************************************************************/ - private int[] limits(List drawn, int dpi, Frame frame, Diagnostics diagnostics) + private int minimum(Symbology symbology, int dpi) { - int recommended = drawn.stream().mapToInt(symbology -> ModuleFit.recommended(symbology, dpi)).max().orElseThrow(); + 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 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)); } - int maximum = maxModule == null ? Integer.MAX_VALUE : maxModule.module(dpi); - if(maximum < minimum) + if(maxModule != null && maxModule.module(dpi) < minimum) { - throw diagnostics.refuse("SCENE_INVALID", id, pointer("maxModule"), frame, Map.of("maxModuleDots", maximum), Map.of("minModuleDots", minimum)); + throw diagnostics.refuse("SCENE_INVALID", id, pointer("maxModule"), frame, Map.of("maxModuleDots", maxModule.module(dpi)), Map.of("minModuleDots", minimum)); } - return (new int[] {minimum, maximum}); } @@ -396,7 +469,7 @@ final class BarcodeElement implements SceneElement ** The symbology to draw: the one the scene states, or the one the record ** names, which must be one the scene allows. ***************************************************************************/ - private String symbology(Frame frame, RenderInput input, Diagnostics diagnostics) + private Symbology symbology(Frame frame, RenderInput input, Diagnostics diagnostics) { if(symbologyPath == null) { @@ -407,22 +480,21 @@ final class BarcodeElement implements SceneElement { throw diagnostics.refuse("VALUE_MISSING", id, pointer("symbologyPath"), frame, null, null); } - if(!symbologies.contains(named.textValue())) + Symbology symbology = Symbology.of(named.textValue()); + if(symbology == null || !symbologies.contains(symbology)) { throw diagnostics.refuse("SCENE_INVALID", id, pointer("symbologyPath"), frame, null, null); } - return (named.textValue()); + return (symbology); } /*************************************************************************** - ** What the encoder is given: the literal, or the text, number or true/false - ** of the record as JSON writes it, held to the scanner profile when the - ** scene names one. An element string built from `gs1` entries would enter - ** here. + ** 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 payload(String symbology, Frame frame, RenderInput input, Diagnostics diagnostics) + private String text(Frame frame, RenderInput input, Diagnostics diagnostics) { String text = value; if(valuePath != null) @@ -434,13 +506,35 @@ final class BarcodeElement implements SceneElement } text = bound.asText(); } - if(text.length() > MAXIMUM_CHARACTERS) + 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", MAXIMUM_CHARACTERS)); + throw diagnostics.refuse("VALUE_TOO_LONG", id, null, frame, Map.of("characters", text.length()), Map.of("characters", most)); } - if(scannerPath == null) + 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) { - return (text); + 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); @@ -450,7 +544,7 @@ final class BarcodeElement implements SceneElement } try { - return (ScannerSymbology.payload(symbology, text, label.textValue(), identifier.textValue())); + return (ScannerSymbology.payload(symbology.id(), text, label.textValue(), identifier.textValue())); } catch(IllegalArgumentException rejected) { @@ -462,50 +556,127 @@ final class BarcodeElement implements SceneElement { throw diagnostics.refuse("SCENE_INVALID", id, pointer("scannerSymbologyPath"), frame, null, null); } - throw unusable("SCENE_INVALID", text, frame, diagnostics); + throw diagnostics.refuse(code, id, at, frame, Map.of("characters", text.length()), null); } } /*************************************************************************** - ** The refusal of a value this barcode cannot hold, at the property that - ** gives the value, with only how many characters it has. + ** 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 Refusal unusable(String code, String text, Frame frame, Diagnostics diagnostics) + private Bars encode(Symbology symbology, String payload, int characters, Frame frame, Diagnostics diagnostics) { - return (diagnostics.refuse(code, id, pointer(valuePath == null ? "value" : "valuePath"), frame, Map.of("characters", text.length()), null)); - } + 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); + } - - - /*************************************************************************** - ** The ink of a symbol the encoder drew at one dot to the module. 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. - ***************************************************************************/ - private static List runs(BufferedImage modules, boolean matrix) - { + /////////////////////////////////////////////////////////////////////////////////////////// + // 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 runs = new ArrayList<>(); - int first = matrix ? 0 : modules.getHeight() / 2; - int last = matrix ? modules.getHeight() - 1 : first; + 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 <= modules.getWidth(); column++) + for(int column = 0; column <= columns; column++) { - boolean ink = column < modules.getWidth() && (modules.getRGB(column, row) & 0xFFFFFF) == 0; + 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, matrix ? row : 0, column - start)); + runs.add(new Run(start, row - first, column - start)); start = -1; } } } - return (runs); + 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 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 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); } } diff --git a/renderer/src/main/java/com/kingsrook/qbits/renderer/scene2/barcode/BarcodeFactory.java b/renderer/src/main/java/com/kingsrook/qbits/renderer/scene2/barcode/BarcodeFactory.java index 4c6e4cc..1fd4076 100644 --- a/renderer/src/main/java/com/kingsrook/qbits/renderer/scene2/barcode/BarcodeFactory.java +++ b/renderer/src/main/java/com/kingsrook/qbits/renderer/scene2/barcode/BarcodeFactory.java @@ -30,7 +30,8 @@ import com.kingsrook.qbits.renderer.scene2.UnavailableElement; /******************************************************************************* ** Barcodes: a symbol fitted to its box in whole-dot modules, never below its - ** stated minimum, and placed in the box by `align` and `valign`. + ** stated minimum, and placed in the box by `align` and `valign`, in the + ** middle unless the scene says otherwise. ** ** `gs1` (an element string built from application identifiers) and `hri` (the ** human-readable line) are declared and not yet read: an element that states @@ -45,7 +46,8 @@ public final class BarcodeFactory implements ElementFactory @Override public Set 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")); } diff --git a/renderer/src/main/java/com/kingsrook/qbits/renderer/scene2/barcode/ModuleFit.java b/renderer/src/main/java/com/kingsrook/qbits/renderer/scene2/barcode/ModuleFit.java index 87b181a..37e31be 100644 --- a/renderer/src/main/java/com/kingsrook/qbits/renderer/scene2/barcode/ModuleFit.java +++ b/renderer/src/main/java/com/kingsrook/qbits/renderer/scene2/barcode/ModuleFit.java @@ -21,10 +21,6 @@ package com.kingsrook.qbits.renderer.scene2.barcode; -import java.util.Set; -import com.kingsrook.qbits.renderer.scene2.Length; - - /******************************************************************************* ** 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 @@ -32,33 +28,17 @@ import com.kingsrook.qbits.renderer.scene2.Length; ** 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 linear symbol is fitted along its width only; its bars take the whole - ** height of the box. A two-dimensional symbol is fitted on both axes. Integer + ** 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 { - ///////////////////////////////////////////////////////////////////////////////////////////////// - // the smallest module a symbol is drawn at unless the scene states its own `minModule`: 0.25 // - // mm for a linear symbol and 0.375 mm for a two-dimensional one. Taken up to whole dots that // - // is 2 and 3 dots at 203 dpi, 3 and 5 at 300 dpi and 6 and 9 at 600 dpi. They are the // - // proposals of the design, to be confirmed by printing and scanning before a final release // - ///////////////////////////////////////////////////////////////////////////////////////////////// - static final Length LINEAR_MINIMUM_MODULE = new Length(Length.Unit.MM, 250); - static final Length MATRIX_MINIMUM_MODULE = new Length(Length.Unit.MM, 375); - - ///////////////////////////////////////////////////////////////////////////////////////////////// - // quiet zones, in modules on each side: at least ten for a linear symbol and four for a // - // two-dimensional one, the minimums version 1 holds its encoders to, and at most 64 // - ///////////////////////////////////////////////////////////////////////////////////////////////// - static final int LINEAR_QUIET_MODULES = 10; - static final int MATRIX_QUIET_MODULES = 4; - static final int MAXIMUM_QUIET_MODULES = 64; - - private static final Set MATRIX = Set.of("qr", "datamatrix", "aztec", "pdf417"); - - - /*************************************************************************** ** Utility class. ***************************************************************************/ @@ -69,13 +49,33 @@ final class ModuleFit /*************************************************************************** - ** What is fitted: a symbol of `columns` modules across, quiet zones - ** included, and for a two-dimensional symbol `rows` modules down (0 for a - ** linear one), 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. + ** 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 Request(int columns, int rows, int along, int across, int minimum, int maximum) + 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) { } @@ -85,7 +85,8 @@ final class ModuleFit ** 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. + ** 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) { @@ -93,51 +94,31 @@ final class ModuleFit - /*************************************************************************** - ** Whether a symbology is two-dimensional: fitted on both axes, with the - ** larger minimum module and the smaller quiet zone. - ***************************************************************************/ - static boolean matrix(String symbology) - { - return (MATRIX.contains(symbology)); - } - - - - /*************************************************************************** - ** The smallest module of a symbology at a resolution, in dots, when the - ** scene states none: the physical default taken up to a whole dot. - ***************************************************************************/ - static int recommended(String symbology, int dpi) - { - return ((matrix(symbology) ? MATRIX_MINIMUM_MODULE : LINEAR_MINIMUM_MODULE).module(dpi)); - } - - - - /*************************************************************************** - ** The fewest quiet modules a symbology may have on each side. - ***************************************************************************/ - static int quiet(String symbology) - { - return (matrix(symbology) ? MATRIX_QUIET_MODULES : LINEAR_QUIET_MODULES); - } - - - /*************************************************************************** ** 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. + ** 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()); - if(request.rows() > 0) + 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(); - return (new Fit(module, request.columns() * drawn, request.rows() > 0 ? request.rows() * drawn : request.across(), fits)); + int tall = height == null ? request.rows() * drawn : fits ? request.across() : height.at(drawn); + return (new Fit(module, request.columns() * drawn, tall, fits)); } } diff --git a/renderer/src/main/java/com/kingsrook/qbits/renderer/scene2/barcode/Payload.java b/renderer/src/main/java/com/kingsrook/qbits/renderer/scene2/barcode/Payload.java new file mode 100644 index 0000000..042e227 --- /dev/null +++ b/renderer/src/main/java/com/kingsrook/qbits/renderer/scene2/barcode/Payload.java @@ -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 `\` to `\` 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 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 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 measured, Map 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 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); + } +} diff --git a/renderer/src/main/java/com/kingsrook/qbits/renderer/scene2/barcode/Symbology.java b/renderer/src/main/java/com/kingsrook/qbits/renderer/scene2/barcode/Symbology.java new file mode 100644 index 0000000..3a926e4 --- /dev/null +++ b/renderer/src/main/java/com/kingsrook/qbits/renderer/scene2/barcode/Symbology.java @@ -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); + } +} diff --git a/renderer/src/test/java/com/kingsrook/qbits/renderer/MutationCampaignTest.java b/renderer/src/test/java/com/kingsrook/qbits/renderer/MutationCampaignTest.java index d03bebf..331f73a 100644 --- a/renderer/src/test/java/com/kingsrook/qbits/renderer/MutationCampaignTest.java +++ b/renderer/src/test/java/com/kingsrook/qbits/renderer/MutationCampaignTest.java @@ -496,7 +496,7 @@ class MutationCampaignTest Set 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") + "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)) @@ -523,11 +523,15 @@ class MutationCampaignTest ** 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 that are - ** more than nothing in a physical unit or dots; a quiet zone of at least - ** the 10 modules of a linear symbol (4 when every symbology it may draw is - ** two-dimensional) and at most 64; and a quarter-turn rotation. An element - ** that asks for `gs1` or `hri` is never accepted. + ** 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) { @@ -572,12 +576,15 @@ class MutationCampaignTest { assertTrue(!stated.has(path) || (stated.get(path).isTextual() && stated.get(path).textValue().startsWith("/") && stated.get(path).textValue().length() <= 256), () -> about + path + " " + stated.get(path)); } - for(String module : List.of("minModule", "maxModule")) + 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().allMatch(Set.of("qr", "datamatrix", "pdf417", "aztec")::contains) ? 4 : 10; + 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")); } diff --git a/renderer/src/test/java/com/kingsrook/qbits/renderer/scene2/DiagnosticsTest.java b/renderer/src/test/java/com/kingsrook/qbits/renderer/scene2/DiagnosticsTest.java index cbbeb94..1dcff0b 100644 --- a/renderer/src/test/java/com/kingsrook/qbits/renderer/scene2/DiagnosticsTest.java +++ b/renderer/src/test/java/com/kingsrook/qbits/renderer/scene2/DiagnosticsTest.java @@ -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); diff --git a/renderer/src/test/java/com/kingsrook/qbits/renderer/scene2/Scene2Test.java b/renderer/src/test/java/com/kingsrook/qbits/renderer/scene2/Scene2Test.java index dc695c1..10a4e1f 100644 --- a/renderer/src/test/java/com/kingsrook/qbits/renderer/scene2/Scene2Test.java +++ b/renderer/src/test/java/com/kingsrook/qbits/renderer/scene2/Scene2Test.java @@ -508,11 +508,14 @@ class Scene2Test /*************************************************************************** - ** An image is parsed and placed by this renderer and then - ** refused by name, with the box they would have had. The design's own - ** example scene with a logo beside it is the case: validated, the logo is - ** its one error; rendered with an empty record, its barcode is refused - ** first, for the value the record lacks, and its caption for the same. + ** 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 @@ -521,6 +524,7 @@ 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}}}]}"; @@ -532,7 +536,9 @@ class Scene2Test Map answer = RendererMain.process(SceneFixture.request("RENDER", example).put("diagnostics", "COLLECT")); assertEquals("REFUSED", answer.get("outcome")); - assertEquals(List.of(Map.of("code", "FIT_NOT_PROVEN", "severity", "WARNING", "element", "code", "box", List.of(6, 4, 751, 87)), 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)), @@ -577,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> expected = List.of( Map.of("code", "DEVICE_UNIT_USED", "severity", "WARNING", "element", "good", "pointer", "/elements/0/x"), diff --git a/renderer/src/test/java/com/kingsrook/qbits/renderer/scene2/barcode/BarcodeDecodeTest.java b/renderer/src/test/java/com/kingsrook/qbits/renderer/scene2/barcode/BarcodeDecodeTest.java index c0c574a..e6ff140 100644 --- a/renderer/src/test/java/com/kingsrook/qbits/renderer/scene2/barcode/BarcodeDecodeTest.java +++ b/renderer/src/test/java/com/kingsrook/qbits/renderer/scene2/barcode/BarcodeDecodeTest.java @@ -46,23 +46,36 @@ import static org.junit.jupiter.api.Assertions.assertTrue; ** 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. + ** 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 DESIGN = List.of(2, 3, 6); + private static final List RETAIL = List.of(3, 4, 7); + private static final List DISTRIBUTION = List.of(4, 6, 12); + private static final List MATRIX = List.of(3, 5, 9); /*************************************************************************** - ** One symbology: how it is read back, its box, 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. + ** 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 symbology, BarcodeFormat format, String width, String height, IntFunction value, UnaryOperator decoded, int fewest, int typical, int most, int step) + private record Profile(String name, String symbology, BarcodeFormat format, String width, String height, String profile, int quietLeft, int quietRight, List narrowest, IntFunction value, + UnaryOperator decoded, int fewest, int typical, int most, int step) { /************************************************************************ ** The name of the profile in a test report. @@ -70,7 +83,7 @@ class BarcodeDecodeTest @Override public String toString() { - return (symbology); + return (name); } } @@ -122,36 +135,50 @@ class BarcodeDecodeTest /*************************************************************************** ** The thirteen fit-qualified symbologies. The retail symbols have one - ** length; their check digits here were worked out by hand. + ** 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 profiles() { UnaryOperator same = value -> value; return (List.of( - new Profile("code128", BarcodeFormat.CODE_128, "90mm", "12mm", length -> repeated(LETTERS, length), same, 1, 12, 80, 1), - new Profile("code39", BarcodeFormat.CODE_39, "90mm", "12mm", length -> repeated(LETTERS, length), same, 1, 10, 80, 1), - new Profile("code93", BarcodeFormat.CODE_93, "90mm", "12mm", length -> repeated(LETTERS, length), same, 1, 10, 80, 1), - new Profile("itf", BarcodeFormat.ITF, "90mm", "12mm", length -> repeated(DIGITS, length), same, 6, 14, 120, 2), - new Profile("gs1-128", BarcodeFormat.CODE_128, "90mm", "12mm", BarcodeDecodeTest::elementString, BarcodeDecodeTest::scanned, 1, 8, 40, 1), - new Profile("ean13", BarcodeFormat.EAN_13, "40mm", "12mm", length -> "001234567890", value -> value + "5", 12, 12, 12, 1), - new Profile("ean8", BarcodeFormat.EAN_8, "40mm", "12mm", length -> "1234567", value -> value + "0", 7, 7, 7, 1), - new Profile("upca", BarcodeFormat.UPC_A, "40mm", "12mm", length -> "01234567890", value -> value + "5", 11, 11, 11, 1), - new Profile("upce", BarcodeFormat.UPC_E, "40mm", "12mm", length -> "0425261", value -> value + "4", 7, 7, 7, 1), - new Profile("qr", BarcodeFormat.QR_CODE, "40mm", "40mm", length -> repeated(LETTERS, length), same, 1, 40, 2000, 1), - new Profile("datamatrix", BarcodeFormat.DATA_MATRIX, "40mm", "40mm", length -> repeated(LETTERS, length), same, 1, 40, 2000, 1), - new Profile("pdf417", BarcodeFormat.PDF_417, "90mm", "30mm", length -> repeated(LETTERS, length), same, 1, 40, 1000, 1), - new Profile("aztec", BarcodeFormat.AZTEC, "40mm", "40mm", length -> repeated(LETTERS, length), same, 1, 40, 2000, 1))); + 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, centred in its box. + ** 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); - return (element.put("anchor", "center").put("width", profile.width()).put("height", profile.height()).put("align", "center").put("valign", "middle").toString()); + if(profile.profile() != null) + { + element.put("barcodeProfile", profile.profile()); + } + return (element.put("anchor", "center").put("width", profile.width()).put("height", profile.height()).toString()); } @@ -216,8 +243,10 @@ class BarcodeDecodeTest /*************************************************************************** ** 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, with the - ** quiet zones blank and the symbol centred within one dot. + ** 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") @@ -241,7 +270,8 @@ class BarcodeDecodeTest } assertTrue(codes.contains("BARCODE_DOES_NOT_FIT"), profile + " one step past its longest value: " + codes); } - StringBuilder report = new StringBuilder(profile.symbology()); + 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); @@ -253,25 +283,30 @@ class BarcodeDecodeTest 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 quiet = BarcodeFixture.number(chosen, "measured", "quietModules"); + 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 // + // the box holds, since one dot more would not fit or would ask for taller bars // ////////////////////////////////////////////////////////////////////////////////////// - int minimum = matrix ? Map.of(203, 3, 300, 5, 600, 9).get(dpi) : Map.of(203, 2, 300, 3, 600, 6).get(dpi); - assertEquals(matrix ? 4 : 10, quiet, about); + 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()), about + ": a larger module fits"); + 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 centred // - // within one dot, and every bar and space a whole number of modules // + // 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); @@ -279,22 +314,22 @@ class BarcodeDecodeTest int right = box.right() - 1 - ink.right(); int top = ink.top() - box.y(); int bottom = box.bottom() - 1 - ink.bottom(); - assertTrue(left >= quiet * module && right >= quiet * module, about + ": quiet zones " + left + " and " + right); + 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 + quiet * module && (!edged || left == (box.width() - width) / 2 + quiet * module), about + ": left margin " + left); + assertTrue(left >= (box.width() - width) / 2 + quietLeft * module && (!edged || left == (box.width() - width) / 2 + quietLeft * module), about + ": left margin " + left); if(matrix) { - assertTrue(top >= quiet * module && bottom >= quiet * module, about + ": quiet zones " + top + " and " + bottom); - assertTrue(top >= (box.height() - height) / 2 + quiet * module && (!edged || top == (box.height() - height) / 2 + quiet * module), about + ": top margin " + top); + 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 - right) <= 1, about + ": margins " + left + " and " + right); + 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 runs = BarcodeFixture.runs(page[(ink.top() + ink.bottom()) / 2]); assertEquals(module, runs.stream().mapToInt(Integer::intValue).min().orElseThrow(), about); - assertEquals((modules - 2 * quiet) * module, runs.stream().mapToInt(Integer::intValue).sum(), about); + assertEquals(bars * module, runs.stream().mapToInt(Integer::intValue).sum(), about); } for(int y = ink.top(); y <= ink.bottom(); y++) { diff --git a/renderer/src/test/java/com/kingsrook/qbits/renderer/scene2/barcode/BarcodeElementTest.java b/renderer/src/test/java/com/kingsrook/qbits/renderer/scene2/barcode/BarcodeElementTest.java index 6519b6c..0246a29 100644 --- a/renderer/src/test/java/com/kingsrook/qbits/renderer/scene2/barcode/BarcodeElementTest.java +++ b/renderer/src/test/java/com/kingsrook/qbits/renderer/scene2/barcode/BarcodeElementTest.java @@ -47,7 +47,8 @@ import static org.junit.jupiter.api.Assertions.assertTrue; ** A `barcode` element from scene to label: what it reads, what it refuses and ** with which numbers, where the fitted symbol sits in its box, and that the ** label still decodes. Values are obviously fake. The label is the example - ** stock of the design, 4 x 1 in with 3 mm margins. + ** stock of the design, 4 x 1 in with 3 mm margins: at 203 dpi a printable + ** area of 764 by 155 dots. *******************************************************************************/ class BarcodeElementTest { @@ -73,11 +74,36 @@ class BarcodeElementTest /*************************************************************************** - ** The same, 90 mm by 12 mm at the top of the label, centred in its box. + ** The same, 12 mm high at the top of the label. Nothing says where the + ** symbol sits in its box, so it sits in the middle. ***************************************************************************/ private static ObjectNode centred(String symbology, String value, String width) { - return (barcode(symbology, value).put("anchor", "top-center").put("width", width).put("height", "12mm").put("align", "center").put("valign", "middle")); + return (barcode(symbology, value).put("anchor", "top-center").put("width", width).put("height", "12mm")); + } + + + + /*************************************************************************** + ** The same for a GS1-128 held to the design's own 0.25 mm and not to the + ** 0.495 mm of a logistic unit, which no 44-character symbol can have on a + ** label 4 in wide. + ***************************************************************************/ + private static ObjectNode general(String value, String width) + { + return (centred("gs1-128", value, width).put("barcodeProfile", "gs1-general")); + } + + + + /*************************************************************************** + ** A barcode named `code` whose value is read from the record. + ***************************************************************************/ + private static ObjectNode bound(String symbology) + { + ObjectNode element = centred(symbology, "", "90mm"); + element.remove("value"); + return (element.put("valuePath", "/record/value")); } @@ -115,6 +141,27 @@ class BarcodeElementTest + /*************************************************************************** + ** The numbers of a fitted or refused symbol, as a diagnostic carries them. + ***************************************************************************/ + private static Map measured(int modules, int quietLeft, int quietRight, int moduleDots, int moduleMicrometres, int widthDots, int heightDots) + { + return (Map.of("modules", modules, "quietLeftModules", quietLeft, "quietRightModules", quietRight, "moduleDots", moduleDots, "moduleMicrometres", moduleMicrometres, "widthDots", widthDots, "heightDots", heightDots)); + } + + + + /*************************************************************************** + ** The codes of the findings of an answer, in order. + ***************************************************************************/ + @SuppressWarnings("unchecked") + private static List codes(Map answer) + { + return (((List>) answer.get("diagnostics")).stream().map(finding -> finding.get("code")).toList()); + } + + + /*************************************************************************** ** A part of a page, as rows of ink. ***************************************************************************/ @@ -152,18 +199,18 @@ class BarcodeElementTest ** The worked example of the fit rule: a 44-character GS1-128 in a box 90 mm ** wide at 203 dpi. The box is 719 dots; the symbol is 319 modules with its ** quiet zones; 719 / 319 is 2.25, so the module is 2 dots (0.25 mm) and the - ** symbol 638 dots. Centred, 40 dots are left blank on the left and 41 on the - ** right, and with the quiet zones the bars start 60 dots in and end 61 dots - ** short. The bars are the whole height of the box, and the label decodes to - ** the value with FNC1 read as `]C1`. + ** symbol 638 dots. With nothing said about alignment it sits in the middle + ** of its box: 40 dots are left blank on the left and 41 on the right, and + ** with the quiet zones the bars start 60 dots in and end 61 dots short. + ** The bars are the whole height of the box, and the label decodes to the + ** value with FNC1 read as `]C1`. ***************************************************************************/ @Test - void aGs1SymbolIsFittedInWholeDotsAndCentredInItsBox() throws Exception + void aGs1SymbolIsFittedInWholeDotsAndSitsInTheMiddleOfItsBox() throws Exception { - Map answer = BarcodeFixture.answer(request("RENDER", 203, centred("gs1-128", TYPICAL, "90mm").toString(), Map.of())); + Map answer = BarcodeFixture.answer(request("RENDER", 203, general(TYPICAL, "90mm").toString(), Map.of())); assertEquals(List.of(Map.of("code", "BARCODE_MODULE", "severity", "INFO", "element", "code", "box", List.of(22, 0, 719, 95), - "measured", Map.of("modules", 319, "quietModules", 10, "moduleDots", 2, "moduleMicrometres", 250, "widthDots", 638, "heightDots", 95), - "limit", Map.of("widthDots", 719, "heightDots", 95, "minModuleDots", 2))), answer.get("diagnostics")); + "measured", measured(319, 10, 10, 2, 250, 638, 95), "limit", Map.of("widthDots", 719, "heightDots", 95, "minModuleDots", 2))), answer.get("diagnostics")); boolean[][] page = BarcodeFixture.page(answer); assertEquals(new BarcodeFixture.Ink(24 + 22 + 60, 24, 24 + 22 + 719 - 61 - 1, 24 + 94), BarcodeFixture.ink(page)); @@ -175,7 +222,7 @@ class BarcodeElementTest Map> modules = Map.of(300, List.of(3, 254, 957, 1062), 600, List.of(6, 254, 1914, 2125)); for(Map.Entry> expected : modules.entrySet()) { - Map other = BarcodeFixture.answer(request("RENDER", expected.getKey(), centred("gs1-128", TYPICAL, "90mm").toString(), Map.of())); + Map other = BarcodeFixture.answer(request("RENDER", expected.getKey(), general(TYPICAL, "90mm").toString(), Map.of())); Map chosen = BarcodeFixture.finding(other, "BARCODE_MODULE"); assertEquals(expected.getValue(), List.of(BarcodeFixture.number(chosen, "measured", "moduleDots"), BarcodeFixture.number(chosen, "measured", "moduleMicrometres"), BarcodeFixture.number(chosen, "measured", "widthDots"), BarcodeFixture.number(chosen, "limit", "widthDots"))); @@ -186,28 +233,99 @@ class BarcodeElementTest /*************************************************************************** - ** Carried over from the alignment tests of version 1: a centred symbol - ** leaves the same blank width on both sides of its box, within one dot, for - ** a short, a typical and a long value at every resolution, at the largest - ** module that fits. + ** The narrowest bar is that of the profile, and with no profile named that + ** of the strictest rule written for the symbology. For a GS1-128 that is + ** the 0.495 mm of a logistic unit: 4 dots at 203 dpi, at which the + ** 44-character symbol needs 1276 dots and does not fit a label 4 in wide, + ** whose printable width is 751 dots here and 812 with no margins at all. + ** Named `gs1-general` or `none`, it is held to 0.25 mm and drawn at 2 + ** dots; 3 would need 957. ***************************************************************************/ @Test - void aCentredSymbolLeavesEqualMarginsWithinOneDot() throws Exception + void theProfileGivesTheNarrowestBar() throws Exception + { + Map logistic = Map.of("code", "BARCODE_DOES_NOT_FIT", "severity", "ERROR", "element", "code", "box", List.of(6, 0, 751, 95), + "measured", measured(319, 10, 10, 2, 250, 1276, 180), "limit", Map.of("widthDots", 751, "heightDots", 95, "minModuleDots", 4)); + assertEquals(logistic, refused("RENDER", 203, centred("gs1-128", TYPICAL, "94mm"), Map.of())); + assertEquals(logistic, refused("VALIDATE", 203, centred("gs1-128", TYPICAL, "94mm"), Map.of())); + assertEquals(logistic, refused("RENDER", 203, centred("gs1-128", TYPICAL, "94mm").put("barcodeProfile", "gs1-logistics"), Map.of())); + for(String profile : List.of("gs1-general", "none")) + { + Map answer = BarcodeFixture.answer(request("RENDER", 203, centred("gs1-128", TYPICAL, "94mm").put("barcodeProfile", profile).toString(), Map.of())); + assertEquals(List.of(Map.of("code", "BARCODE_MODULE", "severity", "INFO", "element", "code", "box", List.of(6, 0, 751, 95), + "measured", measured(319, 10, 10, 2, 250, 638, 95), "limit", Map.of("widthDots", 751, "heightDots", 95, "minModuleDots", 2))), answer.get("diagnostics"), profile); + assertEquals(SCANNED, BarcodeFixture.decode(BarcodeFixture.page(answer), BarcodeFormat.CODE_128, true).getText(), profile); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // with no margins the label is 812 dots wide: still two dots, since three need 957 // + /////////////////////////////////////////////////////////////////////////////////////////// + String whole = "{\"schema\":\"qqq-zpl-scene/2\",\"design\":{\"width\":\"4in\",\"height\":\"1in\"},\"elements\":[" + barcode("gs1-128", TYPICAL).put("height", "12mm") + "]}"; + Refusal edge = assertThrows(Refusal.class, () -> RendererMain.process(SceneFixture.request("RENDER", SceneFixture.stock(101_600, 25_400, 0, 203), whole))); + assertEquals(Map.of("widthDots", 812, "heightDots", 95, "minModuleDots", 4), edge.diagnostic().wire().get("limit")); + assertEquals(measured(319, 10, 10, 2, 250, 1276, 180), edge.diagnostic().wire().get("measured")); + + /////////////////////////////////////////////////////////////////////////////////////////// + // nor does it fit along the 6 in of 4 x 6 in stock, which is 1218 dots: at 4 dots the // + // symbol is 160 mm. A shorter element string, an SSCC, is what 4 dots have room for // + /////////////////////////////////////////////////////////////////////////////////////////// + String turned = "{\"schema\":\"qqq-zpl-scene/2\",\"design\":{\"width\":\"4in\",\"height\":\"6in\"},\"elements\":[" + barcode("gs1-128", TYPICAL).put("width", "24mm").put("rotation", 90) + "]}"; + Refusal along = assertThrows(Refusal.class, () -> RendererMain.process(SceneFixture.request("RENDER", SceneFixture.stock(101_600, 152_400, 0, 203), turned))); + assertEquals(Map.of("widthDots", 1218, "heightDots", 191, "minModuleDots", 4), along.diagnostic().wire().get("limit")); + String unit = "[00]00012345678901234" + BarcodeFixture.checkDigit("00012345678901234"); + Map logisticUnit = BarcodeFixture.answer(request("RENDER", 203, centred("gs1-128", unit, "94mm").toString(), Map.of())); + assertEquals(measured(176, 10, 10, 4, 500, 704, 95), BarcodeFixture.finding(logisticUnit, "BARCODE_MODULE").get("measured")); + assertEquals("]C100" + unit.substring(4), BarcodeFixture.decode(BarcodeFixture.page(logisticUnit), BarcodeFormat.CODE_128, true).getText()); + + /////////////////////////////////////////////////////////////////////////////////////////// + // the retail symbols take the 0.264 mm of general retail unless told otherwise, and an // + // Interleaved 2 of 5 the 0.495 mm of general distribution // + /////////////////////////////////////////////////////////////////////////////////////////// + Map> minimums = new LinkedHashMap<>(); + minimums.put("", List.of(3, 4)); + minimums.put("none", List.of(2, 2)); + minimums.put("gs1-general", List.of(3, 2)); + minimums.put("gs1-logistics", List.of(4, 4)); + for(Map.Entry> minimum : minimums.entrySet()) + { + ObjectNode retail = centred("ean13", "0012345678905", "90mm").put("minHeight", "5mm"); + ObjectNode pairs = centred("itf", "00123456789012", "90mm"); + if(!minimum.getKey().isEmpty()) + { + retail.put("barcodeProfile", minimum.getKey()); + pairs.put("barcodeProfile", minimum.getKey()); + } + assertEquals(minimum.getValue().get(0), BarcodeFixture.number(BarcodeFixture.finding(BarcodeFixture.answer(request("VALIDATE", 203, retail.toString(), Map.of())), "BARCODE_MODULE"), "limit", "minModuleDots"), minimum.getKey()); + assertEquals(minimum.getValue().get(1), BarcodeFixture.number(BarcodeFixture.finding(BarcodeFixture.answer(request("VALIDATE", 203, pairs.toString(), Map.of())), "BARCODE_MODULE"), "limit", "minModuleDots"), minimum.getKey()); + } + } + + + + /*************************************************************************** + ** Carried over from the alignment tests of version 1: a symbol in the + ** middle of its box leaves the same blank width on both sides, within one + ** dot, for a short, a typical and a long value at every resolution, at the + ** largest module at which it fits and its bars are tall enough. + ***************************************************************************/ + @Test + void aSymbolInTheMiddleLeavesEqualMarginsWithinOneDot() throws Exception { for(int dpi : Scene2.RESOLUTIONS) { for(String value : List.of(SHORT, TYPICAL, LONG)) { - Map answer = BarcodeFixture.answer(request("RENDER", dpi, centred("gs1-128", value, "100%").toString(), Map.of())); + Map answer = BarcodeFixture.answer(request("RENDER", dpi, general(value, "100%").put("height", "14mm").toString(), Map.of())); Map chosen = BarcodeFixture.finding(answer, "BARCODE_MODULE"); Frame box = BarcodeFixture.box(chosen, dpi); BarcodeFixture.Ink ink = BarcodeFixture.ink(BarcodeFixture.page(answer)); int left = ink.left() - box.x(); int right = box.right() - 1 - ink.right(); int module = BarcodeFixture.number(chosen, "measured", "moduleDots"); + int modules = BarcodeFixture.number(chosen, "measured", "modules"); String about = value.length() + " characters at " + dpi + " dpi: " + left + " and " + right; assertTrue(Math.abs(left - right) <= 1 && left >= 10 * module, about); - assertTrue(BarcodeFixture.number(chosen, "measured", "modules") * (module + 1) > box.width(), about); + assertTrue(modules * (module + 1) > box.width() || ((modules - 20) * (module + 1) * 15 + 99) / 100 > box.height(), about); assertEquals(box.height(), ink.height(), about); } } @@ -216,8 +334,9 @@ class BarcodeElementTest /*************************************************************************** - ** Carried over as well: with no `align` the symbol starts at the origin of - ** its box, as every element does; `right` and `bottom` put it against the + ** A symbol sits in the middle of its box unless the scene says otherwise: + ** what the box has to spare is then quiet zone on both sides. `left` and + ** `top` put it at the origin of its box, `right` and `bottom` against the ** far edges; `center` and `middle` split what is left over, the odd dot to ** the right and below. A two-dimensional symbol shows both axes: a 29 ** module QR code in a 20 mm by 12 mm box at 203 dpi is 3 dots to the @@ -227,62 +346,110 @@ class BarcodeElementTest void alignmentPlacesTheSymbolInItsBox() throws Exception { Map> corners = new LinkedHashMap<>(); - corners.put("", List.of(12, 12)); + corners.put("", List.of(36 + 12, 4 + 12)); corners.put("left top", List.of(12, 12)); corners.put("center middle", List.of(36 + 12, 4 + 12)); corners.put("right bottom", List.of(72 + 12, 8 + 12)); corners.put("right top", List.of(72 + 12, 12)); corners.put("left bottom", List.of(12, 8 + 12)); + corners.put("left", List.of(12, 4 + 12)); + corners.put("- bottom", List.of(36 + 12, 8 + 12)); for(Map.Entry> corner : corners.entrySet()) { ObjectNode element = barcode("qr", "TEST-0001").put("x", "10mm").put("y", "2mm").put("width", "20mm").put("height", "12mm"); - if(!corner.getKey().isEmpty()) + String[] words = corner.getKey().split(" ", -1); + if(!words[0].isEmpty() && !"-".equals(words[0])) { - element.put("align", corner.getKey().split(" ", -1)[0]).put("valign", corner.getKey().split(" ", -1)[1]); + element.put("align", words[0]); + } + if(words.length > 1) + { + element.put("valign", words[1]); } Map answer = BarcodeFixture.answer(request("RENDER", 203, element.toString(), Map.of())); Map chosen = BarcodeFixture.finding(answer, "BARCODE_MODULE"); assertEquals(List.of(80, 16, 159, 95), chosen.get("box")); - assertEquals(Map.of("modules", 29, "quietModules", 4, "moduleDots", 3, "moduleMicrometres", 375, "widthDots", 87, "heightDots", 87), chosen.get("measured")); + assertEquals(measured(29, 4, 4, 3, 375, 87, 87), chosen.get("measured")); BarcodeFixture.Ink ink = BarcodeFixture.ink(BarcodeFixture.page(answer)); assertEquals(List.of(24 + 80 + corner.getValue().get(0), 24 + 16 + corner.getValue().get(1), 63, 63), List.of(ink.left(), ink.top(), ink.width(), ink.height()), corner.getKey()); assertEquals("TEST-0001", BarcodeFixture.decode(BarcodeFixture.page(answer), BarcodeFormat.QR_CODE, false).getText()); } ObjectNode linear = barcode("code128", "TEST-0001").put("x", "10mm").put("y", "2mm").put("width", "60mm").put("height", "12mm"); - for(String align : List.of("left", "center", "right")) + for(String align : List.of("", "left", "center", "right")) { - Map answer = BarcodeFixture.answer(request("RENDER", 300, linear.put("align", align).put("valign", "bottom").toString(), Map.of())); + if(!align.isEmpty()) + { + linear.put("align", align).put("valign", "bottom"); + } + Map answer = BarcodeFixture.answer(request("RENDER", 300, linear.toString(), Map.of())); Map chosen = BarcodeFixture.finding(answer, "BARCODE_MODULE"); Frame box = BarcodeFixture.box(chosen, 300); int spare = box.width() - BarcodeFixture.number(chosen, "measured", "widthDots"); int quiet = 10 * BarcodeFixture.number(chosen, "measured", "moduleDots"); BarcodeFixture.Ink ink = BarcodeFixture.ink(BarcodeFixture.page(answer)); - assertEquals(box.x() + quiet + Map.of("left", 0, "center", spare / 2, "right", spare).get(align), ink.left(), align); + assertTrue(spare > 1, align); + assertEquals(box.x() + quiet + Map.of("", spare / 2, "left", 0, "center", spare / 2, "right", spare).get(align), ink.left(), align); assertEquals(List.of(box.y(), box.bottom() - 1), List.of(ink.top(), ink.bottom()), align); } } + /*************************************************************************** + ** Each symbology has its own quiet zones, and a symbol in the middle of + ** its box is its bars and both of them: an EAN-13 has 11 blank modules + ** before its first bar and 7 after its last, so its bars sit two modules + ** right of the middle. A `quiet` the scene states is that many on both + ** sides and may not be under the larger of the two. + ***************************************************************************/ + @Test + void theQuietZonesAreThoseOfTheSymbology() throws Exception + { + Map> zones = Map.of("ean13", List.of(11, 7, 95), "ean8", List.of(7, 7, 67), "upca", List.of(9, 9, 95), "upce", List.of(9, 7, 51), "code39", List.of(10, 10, 51)); + Map values = Map.of("ean13", "0012345678905", "ean8", "12345670", "upca", "012345678905", "upce", "04252614", "code39", "T1"); + for(Map.Entry> zone : zones.entrySet()) + { + ObjectNode element = barcode(zone.getKey(), values.get(zone.getKey())).put("width", "60mm").put("height", "12mm").put("minHeight", "5mm").put("maxModule", "3dot").put("barcodeProfile", "none"); + Map answer = BarcodeFixture.answer(request("RENDER", 203, element.toString(), Map.of())); + Map chosen = BarcodeFixture.finding(answer, "BARCODE_MODULE"); + int left = zone.getValue().get(0); + int right = zone.getValue().get(1); + int bars = zone.getValue().get(2); + assertEquals(List.of(left + bars + right, left, right, 3), List.of(BarcodeFixture.number(chosen, "measured", "modules"), BarcodeFixture.number(chosen, "measured", "quietLeftModules"), + BarcodeFixture.number(chosen, "measured", "quietRightModules"), BarcodeFixture.number(chosen, "measured", "moduleDots")), zone.getKey()); + BarcodeFixture.Ink ink = BarcodeFixture.ink(BarcodeFixture.page(answer)); + int spare = 479 - (left + bars + right) * 3; + assertEquals(List.of(24 + spare / 2 + left * 3, bars * 3), List.of(ink.left(), ink.width()), zone.getKey()); + assertEquals(invalid("quiet"), refused("VALIDATE", 203, element.deepCopy().put("quiet", Math.max(left, right) - 1), Map.of()), zone.getKey()); + Map wider = BarcodeFixture.finding(BarcodeFixture.answer(request("RENDER", 203, element.deepCopy().put("quiet", 12).toString(), Map.of())), "BARCODE_MODULE"); + assertEquals(List.of(bars + 24, 12, 12), List.of(BarcodeFixture.number(wider, "measured", "modules"), BarcodeFixture.number(wider, "measured", "quietLeftModules"), BarcodeFixture.number(wider, "measured", "quietRightModules")), zone.getKey()); + } + } + + + /*************************************************************************** ** `rotation` turns the symbol 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 picture is the upright one turned, dot for dot. Every turn decodes - ** once the page is turned back. + ** once the page is turned back. An EAN-13 shows that the quiet zones turn + ** with the symbol: the wider one is before the first bar whichever way + ** that now points. ** The label here is 4 x 2 in, tall enough for a symbol on its side. ***************************************************************************/ @Test void rotationTurnsTheSymbolClockwiseInItsBox() throws Exception { - for(String[] symbol : List.of(new String[] {"code128", "TEST-0001", "CODE_128"}, new String[] {"datamatrix", "TEST-0001-0002", "DATA_MATRIX"}, new String[] {"pdf417", "TEST-0001", "PDF_417"})) + for(String[] symbol : List.of(new String[] {"code128", "TEST-0001", "CODE_128"}, new String[] {"datamatrix", "TEST-0001-0002", "DATA_MATRIX"}, new String[] {"pdf417", "TEST-0001", "PDF_417"}, + new String[] {"ean13", "0012345678905", "EAN_13"})) { boolean[][] expected = null; for(int rotation : List.of(0, 90, 180, 270)) { boolean upright = rotation % 180 == 0; ObjectNode element = barcode(symbol[0], symbol[1]).put("x", "30mm").put("y", "1mm").put("width", upright ? "40mm" : "12mm").put("height", upright ? "12mm" : "40mm").put("rotation", rotation); - element.put("minModule", "0.2mm").put("align", "center").put("valign", "middle"); + element.put("minModule", "0.2mm").put("minHeight", "5mm"); Map answer = BarcodeFixture.answer(BarcodeFixture.request("RENDER", 300, element.toString(), Map.of())); Map chosen = BarcodeFixture.finding(answer, "BARCODE_MODULE"); boolean[][] page = BarcodeFixture.page(answer); @@ -294,6 +461,19 @@ class BarcodeElementTest assertTrue(ink.left() >= box.x() && ink.right() < box.right() && ink.top() >= box.y() && ink.bottom() < box.bottom(), about); expected = expected == null ? crop(page, ink) : turned(expected); assertArrayEquals(expected, crop(page, ink), about); + if("ean13".equals(symbol[0])) + { + int module = BarcodeFixture.number(chosen, "measured", "moduleDots"); + int spare = (472 - 113 * module) / 2; + int before = switch(rotation) + { + case 90 -> ink.top() - box.y(); + case 180 -> box.right() - 1 - ink.right(); + case 270 -> box.bottom() - 1 - ink.bottom(); + default -> ink.left() - box.x(); + }; + assertTrue(before == spare + 11 * module || before == spare + 11 * module + 1, about + ": " + before + " dots before the first bar"); + } boolean[][] righted = crop(page, new BarcodeFixture.Ink(box.x(), box.y(), box.right() - 1, box.bottom() - 1)); for(int turn = rotation; turn % 360 != 0; turn += 90) { @@ -315,14 +495,13 @@ class BarcodeElementTest @Test void aValueThatDoesNotFitAtTheMinimumModuleIsRefusedWithItsNumbers() throws Exception { - ObjectNode narrow = centred("gs1-128", TYPICAL, "60mm"); + ObjectNode narrow = general(TYPICAL, "60mm"); Map expected = Map.of("code", "BARCODE_DOES_NOT_FIT", "severity", "ERROR", "element", "code", "box", List.of(142, 0, 479, 95), - "measured", Map.of("modules", 319, "quietModules", 10, "moduleDots", 1, "moduleMicrometres", 125, "widthDots", 638, "heightDots", 95), - "limit", Map.of("widthDots", 479, "heightDots", 95, "minModuleDots", 2)); + "measured", measured(319, 10, 10, 1, 125, 638, 90), "limit", Map.of("widthDots", 479, "heightDots", 95, "minModuleDots", 2)); assertEquals(expected, refused("RENDER", 203, narrow, Map.of())); assertEquals(expected, refused("VALIDATE", 203, narrow, Map.of())); Map at300 = refused("RENDER", 300, narrow, Map.of()); - assertEquals(Map.of("modules", 319, "quietModules", 10, "moduleDots", 2, "moduleMicrometres", 169, "widthDots", 957, "heightDots", 141), at300.get("measured")); + assertEquals(measured(319, 10, 10, 2, 169, 957, 135), at300.get("measured")); assertEquals(Map.of("widthDots", 708, "heightDots", 141, "minModuleDots", 3), at300.get("limit")); Map collected = RendererMain.process(request("RENDER", 203, narrow + ",{\"id\":\"rule\",\"type\":\"box\",\"anchor\":\"bottom-left\",\"width\":\"10mm\",\"height\":\"1mm\",\"fill\":true}", Map.of()) @@ -336,32 +515,114 @@ class BarcodeElementTest // dots at 3 dots, and a box 5 mm high has 39; not one dot fits a box of 2 mm // /////////////////////////////////////////////////////////////////////////////////////////// Map flat = refused("RENDER", 203, barcode("qr", "TEST-0001").put("width", "40mm").put("height", "5mm"), Map.of()); - assertEquals(Map.of("modules", 29, "quietModules", 4, "moduleDots", 1, "moduleMicrometres", 125, "widthDots", 87, "heightDots", 87), flat.get("measured")); + assertEquals(measured(29, 4, 4, 1, 125, 87, 87), flat.get("measured")); assertEquals(Map.of("widthDots", 319, "heightDots", 39, "minModuleDots", 3), flat.get("limit")); Map flatter = refused("RENDER", 203, barcode("qr", "TEST-0001").put("width", "40mm").put("height", "2mm"), Map.of()); assertEquals(0, BarcodeFixture.number(flatter, "measured", "moduleDots")); assertEquals(0, BarcodeFixture.number(flatter, "measured", "moduleMicrometres")); + + /////////////////////////////////////////////////////////////////////////////////////////// + // a symbol of more modules than the encoder draws fits no box: 400 characters of Code // + // 39 are 5225 modules // + /////////////////////////////////////////////////////////////////////////////////////////// + assertEquals(Map.of("code", "BARCODE_DOES_NOT_FIT", "severity", "ERROR", "element", "code", "box", List.of(0, 0, 764, 155), "measured", Map.of("characters", 400), "limit", Map.of("modules", 4096)), + refused("VALIDATE", 203, barcode("code39", "A".repeat(400)), Map.of())); + } + + + + /*************************************************************************** + ** The bars of a linear symbol take the whole height of its box, and that + ** must be the height the symbology asks of bars as wide as these. The 598 + ** dots of bars of the 44-character GS1-128 at 2 dots ask for 90 dots, 15% + ** of their width: a box 11 mm high has 87 and the symbol is refused, with + ** the height it needs. A box under 5 mm is refused whatever it holds. The + ** author who accepts shorter bars states `minHeight`: the symbol is drawn + ** and the label carries a warning every time. An EAN-13 must be 69.24 + ** modules tall, more than a label 1 in high has at the 3 dots of general + ** retail, so on this stock it is always drawn with that warning or not at + ** all. A module the width would allow is not taken where it would make the + ** symbol too wide for its height. + ***************************************************************************/ + @Test + void barsShorterThanTheSymbologyAsksAreDrawnOnlyWhenTheAuthorStatesItAndAlwaysWithAWarning() throws Exception + { + ObjectNode low = general(TYPICAL, "90mm").put("height", "11mm"); + Map expected = Map.of("code", "BARCODE_DOES_NOT_FIT", "severity", "ERROR", "element", "code", "box", List.of(22, 0, 719, 87), + "measured", measured(319, 10, 10, 1, 125, 638, 90), "limit", Map.of("widthDots", 719, "heightDots", 87, "minModuleDots", 2)); + assertEquals(expected, refused("RENDER", 203, low, Map.of())); + assertEquals(expected, refused("VALIDATE", 203, low, Map.of())); + assertEquals(List.of("BARCODE_MODULE"), codes(BarcodeFixture.answer(request("RENDER", 203, low.deepCopy().put("height", "11.3mm").toString(), Map.of())))); + + Map warning = Map.of("code", "BARCODE_HEIGHT_BELOW_RECOMMENDED", "severity", "WARNING", "element", "code", "box", List.of(22, 0, 719, 87), "measured", Map.of("heightDots", 87), "limit", Map.of("heightDots", 90)); + for(String operation : List.of("VALIDATE", "RENDER")) + { + Map stated = BarcodeFixture.answer(request(operation, 203, low.deepCopy().put("minHeight", "10mm").toString(), Map.of())); + assertEquals(List.of("BARCODE_HEIGHT_BELOW_RECOMMENDED", "BARCODE_MODULE"), codes(stated), operation); + assertEquals(warning, BarcodeFixture.finding(stated, "BARCODE_HEIGHT_BELOW_RECOMMENDED"), operation); + assertEquals(measured(319, 10, 10, 2, 250, 638, 87), BarcodeFixture.finding(stated, "BARCODE_MODULE").get("measured"), operation); + } + assertEquals(SCANNED, BarcodeFixture.decode(BarcodeFixture.page(BarcodeFixture.answer(request("RENDER", 203, low.deepCopy().put("minHeight", "10mm").toString(), Map.of()))), BarcodeFormat.CODE_128, true).getText()); + + /////////////////////////////////////////////////////////////////////////////////////////// + // a stated height is still a floor: 11 mm is 88 dots, one more than the box has; and a // + // stated height the symbology would have asked for anyway brings no warning // + /////////////////////////////////////////////////////////////////////////////////////////// + Map floor = refused("RENDER", 203, low.deepCopy().put("minHeight", "11mm"), Map.of()); + assertEquals(List.of("BARCODE_DOES_NOT_FIT", 0, 88), List.of(floor.get("code"), BarcodeFixture.number(floor, "measured", "moduleDots"), BarcodeFixture.number(floor, "measured", "heightDots"))); + assertEquals(List.of("BARCODE_MODULE"), codes(BarcodeFixture.answer(request("RENDER", 203, general(TYPICAL, "90mm").put("minHeight", "11.5mm").toString(), Map.of())))); + assertEquals(invalid("minHeight"), refused("VALIDATE", 203, low.deepCopy().put("minHeight", "0mm"), Map.of())); + assertEquals(invalid("minHeight"), refused("VALIDATE", 203, low.deepCopy().put("minHeight", "50%"), Map.of())); + + /////////////////////////////////////////////////////////////////////////////////////////// + // 5 mm is 40 dots at 203 dpi: a box of 39 holds no linear symbol, however narrow // + /////////////////////////////////////////////////////////////////////////////////////////// + Map thin = refused("VALIDATE", 203, centred("code128", "T", "90mm").put("height", "4.9mm"), Map.of()); + assertEquals(List.of("BARCODE_DOES_NOT_FIT", 0, 40, 39), List.of(thin.get("code"), BarcodeFixture.number(thin, "measured", "moduleDots"), BarcodeFixture.number(thin, "measured", "heightDots"), BarcodeFixture.number(thin, "limit", "heightDots"))); + assertEquals(List.of("BARCODE_MODULE"), codes(BarcodeFixture.answer(request("VALIDATE", 203, centred("code128", "T", "90mm").put("height", "5.1mm").toString(), Map.of())))); + + /////////////////////////////////////////////////////////////////////////////////////////// + // a short value in a wide box: nine characters of Code 128 are 123 modules of bars. The // + // width has room for 5 dots, at which the bars would be 615 wide and ask for 93 dots; // + // in a box of 87 the module is 4 // + /////////////////////////////////////////////////////////////////////////////////////////// + assertEquals(5, BarcodeFixture.number(BarcodeFixture.finding(BarcodeFixture.answer(request("RENDER", 203, centred("code128", "TEST-0001", "90mm").toString(), Map.of())), "BARCODE_MODULE"), "measured", "moduleDots")); + Map held = BarcodeFixture.answer(request("RENDER", 203, centred("code128", "TEST-0001", "90mm").put("height", "11mm").toString(), Map.of())); + assertEquals(measured(143, 10, 10, 4, 500, 572, 87), BarcodeFixture.finding(held, "BARCODE_MODULE").get("measured")); + assertEquals("TEST-0001", BarcodeFixture.decode(BarcodeFixture.page(held), BarcodeFormat.CODE_128, false).getText()); + + /////////////////////////////////////////////////////////////////////////////////////////// + // an EAN-13 at 3 dots asks for 208 dots, and the printable area is 155 high // + /////////////////////////////////////////////////////////////////////////////////////////// + ObjectNode retail = barcode("ean13", "0012345678905"); + Map tall = refused("VALIDATE", 203, retail, Map.of()); + assertEquals(Map.of("code", "BARCODE_DOES_NOT_FIT", "severity", "ERROR", "element", "code", "box", List.of(0, 0, 764, 155), "measured", measured(113, 11, 7, 2, 250, 339, 208), + "limit", Map.of("widthDots", 764, "heightDots", 155, "minModuleDots", 3)), tall); + Map truncated = BarcodeFixture.answer(request("RENDER", 203, retail.put("minHeight", "12mm").toString(), Map.of())); + assertEquals(Map.of("code", "BARCODE_HEIGHT_BELOW_RECOMMENDED", "severity", "WARNING", "element", "code", "box", List.of(0, 0, 764, 155), "measured", Map.of("heightDots", 155), "limit", Map.of("heightDots", 416)), + BarcodeFixture.finding(truncated, "BARCODE_HEIGHT_BELOW_RECOMMENDED")); + assertEquals("0012345678905", BarcodeFixture.decode(BarcodeFixture.page(truncated), BarcodeFormat.EAN_13, false).getText()); } /*************************************************************************** ** An author who accepts a smaller module states it: the symbol is then - ** drawn below the default and the label carries a warning every time, - ** validated or rendered. A minimum is never passed: a larger stated one - ** refuses a symbol the default would have drawn. + ** drawn below what its profile asks and the label carries a warning every + ** time, validated or rendered. A minimum is never passed: a larger stated + ** one refuses a symbol the default would have drawn. ***************************************************************************/ @Test void aSmallerModuleIsDrawnOnlyWhenTheAuthorStatesItAndAlwaysWithAWarning() throws Exception { - String scene = "{\"schema\":\"qqq-zpl-scene/2\",\"design\":{\"width\":\"4in\",\"height\":\"1in\"},\"elements\":[" + centred("gs1-128", TYPICAL, "60mm") + String scene = "{\"schema\":\"qqq-zpl-scene/2\",\"design\":{\"width\":\"4in\",\"height\":\"1in\"},\"elements\":[" + general(TYPICAL, "60mm") + "],\"overrides\":[{\"when\":{\"dpi\":[203]},\"set\":{\"code\":{\"minModule\":\"1dot\"}}}]}"; Map warning = Map.of("code", "BARCODE_MODULE_BELOW_RECOMMENDED", "severity", "WARNING", "element", "code", "box", List.of(142, 0, 479, 95), "measured", Map.of("minModuleDots", 1), "limit", Map.of("minModuleDots", 2)); Map rendered = BarcodeFixture.answer(SceneFixture.request("RENDER", scene)); assertEquals(warning, BarcodeFixture.finding(rendered, "BARCODE_MODULE_BELOW_RECOMMENDED")); Map chosen = BarcodeFixture.finding(rendered, "BARCODE_MODULE"); - assertEquals(Map.of("modules", 319, "quietModules", 10, "moduleDots", 1, "moduleMicrometres", 125, "widthDots", 319, "heightDots", 95), chosen.get("measured")); + assertEquals(measured(319, 10, 10, 1, 125, 319, 95), chosen.get("measured")); assertEquals(Map.of("widthDots", 479, "heightDots", 95, "minModuleDots", 1), chosen.get("limit")); assertEquals(SCANNED, BarcodeFixture.decode(BarcodeFixture.page(rendered), BarcodeFormat.CODE_128, true).getText()); assertEquals(rendered.get("diagnostics"), BarcodeFixture.answer(SceneFixture.request("VALIDATE", scene)).get("diagnostics")); @@ -372,31 +633,29 @@ class BarcodeElementTest // minimum is compared in dots: 0.2 mm is the default 2 dots at 203 dpi and 5 dots, // // under the default 6, at 600 dpi // /////////////////////////////////////////////////////////////////////////////////////////// - Map stated = BarcodeFixture.answer(request("VALIDATE", 203, centred("gs1-128", TYPICAL, "60mm").put("minModule", "1dot").toString(), Map.of())); + Map stated = BarcodeFixture.answer(request("VALIDATE", 203, general(TYPICAL, "60mm").put("minModule", "1dot").toString(), Map.of())); assertEquals(Map.of("code", "DEVICE_UNIT_USED", "severity", "WARNING", "element", "code", "pointer", "/elements/0/minModule"), BarcodeFixture.finding(stated, "DEVICE_UNIT_USED")); assertEquals(warning, BarcodeFixture.finding(stated, "BARCODE_MODULE_BELOW_RECOMMENDED")); - ObjectNode physical = centred("gs1-128", TYPICAL, "90mm").put("minModule", "0.2mm"); + ObjectNode physical = general(TYPICAL, "90mm").put("minModule", "0.2mm"); assertEquals(List.of("BARCODE_MODULE"), codes(BarcodeFixture.answer(request("RENDER", 203, physical.toString(), Map.of())))); Map fine = BarcodeFixture.answer(request("RENDER", 600, physical.toString(), Map.of())); assertEquals(List.of("BARCODE_MODULE_BELOW_RECOMMENDED", "BARCODE_MODULE"), codes(fine)); assertEquals(Map.of("minModuleDots", 5), BarcodeFixture.finding(fine, "BARCODE_MODULE_BELOW_RECOMMENDED").get("measured")); assertEquals(6, BarcodeFixture.number(BarcodeFixture.finding(fine, "BARCODE_MODULE"), "measured", "moduleDots")); - Map larger = refused("RENDER", 203, centred("gs1-128", TYPICAL, "90mm").put("minModule", "0.5mm"), Map.of()); + Map larger = refused("RENDER", 203, general(TYPICAL, "90mm").put("minModule", "0.5mm"), Map.of()); assertEquals("BARCODE_DOES_NOT_FIT", larger.get("code")); assertEquals(Map.of("widthDots", 719, "heightDots", 95, "minModuleDots", 4), larger.get("limit")); assertEquals(319 * 4, BarcodeFixture.number(larger, "measured", "widthDots")); - } - - - /*************************************************************************** - ** The codes of the findings of an answer, in order. - ***************************************************************************/ - @SuppressWarnings("unchecked") - private static List codes(Map answer) - { - return (((List>) answer.get("diagnostics")).stream().map(finding -> finding.get("code")).toList()); + /////////////////////////////////////////////////////////////////////////////////////////// + // with no profile a GS1-128 is held to the 4 dots of a logistic unit: the 2 dots that // + // fit this label are then the author's own choice, and warned of // + /////////////////////////////////////////////////////////////////////////////////////////// + Map chose = BarcodeFixture.answer(request("RENDER", 203, centred("gs1-128", TYPICAL, "90mm").put("minModule", "0.25mm").toString(), Map.of())); + assertEquals(List.of("BARCODE_MODULE_BELOW_RECOMMENDED", "BARCODE_MODULE"), codes(chose)); + assertEquals(List.of(Map.of("minModuleDots", 2), Map.of("minModuleDots", 4)), List.of(BarcodeFixture.finding(chose, "BARCODE_MODULE_BELOW_RECOMMENDED").get("measured"), BarcodeFixture.finding(chose, "BARCODE_MODULE_BELOW_RECOMMENDED").get("limit"))); + assertEquals(measured(319, 10, 10, 2, 250, 638, 95), BarcodeFixture.finding(chose, "BARCODE_MODULE").get("measured")); } @@ -464,13 +723,26 @@ class BarcodeElementTest mistakes.put("{\"symbology\":\"code128\",\"value\":\"TEST-0001\",\"minModule\":\"1%\"}", "minModule"); mistakes.put("{\"symbology\":\"code128\",\"value\":\"TEST-0001\",\"minModule\":0.25}", "minModule"); mistakes.put("{\"symbology\":\"code128\",\"value\":\"TEST-0001\",\"maxModule\":\"0pt\"}", "maxModule"); + mistakes.put("{\"symbology\":\"code128\",\"value\":\"TEST-0001\",\"minHeight\":\"0dot\"}", "minHeight"); + mistakes.put("{\"symbology\":\"code128\",\"value\":\"TEST-0001\",\"minHeight\":5}", "minHeight"); mistakes.put("{\"symbology\":\"code128\",\"value\":\"TEST-0001\",\"quiet\":9}", "quiet"); mistakes.put("{\"symbology\":\"code128\",\"value\":\"TEST-0001\",\"quiet\":65}", "quiet"); mistakes.put("{\"symbology\":\"code128\",\"value\":\"TEST-0001\",\"quiet\":\"10\"}", "quiet"); mistakes.put("{\"symbology\":\"code128\",\"value\":\"TEST-0001\",\"quiet\":10.5}", "quiet"); mistakes.put("{\"symbology\":\"code128\",\"value\":\"TEST-0001\",\"quiet\":4294967306}", "quiet"); mistakes.put("{\"symbology\":\"qr\",\"value\":\"TEST-0001\",\"quiet\":3}", "quiet"); + mistakes.put("{\"symbology\":\"ean13\",\"value\":\"0012345678905\",\"quiet\":10}", "quiet"); mistakes.put("{\"symbologyPath\":\"/record/type\",\"allowedSymbologies\":[\"qr\",\"code128\"],\"value\":\"TEST-0001\",\"quiet\":4}", "quiet"); + mistakes.put("{\"symbologyPath\":\"/record/type\",\"allowedSymbologies\":[\"ean8\",\"upca\"],\"value\":\"TEST-0001\",\"quiet\":8}", "quiet"); + mistakes.put("{\"symbology\":\"code128\",\"value\":\"TEST-0001\",\"maxLength\":0}", "maxLength"); + mistakes.put("{\"symbology\":\"code128\",\"value\":\"TEST-0001\",\"maxLength\":4001}", "maxLength"); + mistakes.put("{\"symbology\":\"code128\",\"value\":\"TEST-0001\",\"maxLength\":\"12\"}", "maxLength"); + mistakes.put("{\"symbology\":\"code128\",\"value\":\"TEST-0001\",\"maxLength\":12.5}", "maxLength"); + mistakes.put("{\"symbology\":\"code128\",\"value\":\"TEST-0001\",\"barcodeProfile\":\"gs1\"}", "barcodeProfile"); + mistakes.put("{\"symbology\":\"code128\",\"value\":\"TEST-0001\",\"barcodeProfile\":\"GS1-GENERAL\"}", "barcodeProfile"); + mistakes.put("{\"symbology\":\"code128\",\"value\":\"TEST-0001\",\"barcodeProfile\":1}", "barcodeProfile"); + mistakes.put("{\"symbology\":\"code128\",\"value\":\"TEST-0001\",\"align\":\"middle\"}", "align"); + mistakes.put("{\"symbology\":\"code128\",\"value\":\"TEST-0001\",\"valign\":\"center\"}", "valign"); mistakes.put("{\"symbology\":\"code128\",\"value\":\"TEST-0001\",\"rotation\":45}", "rotation"); for(Map.Entry mistake : mistakes.entrySet()) { @@ -482,13 +754,15 @@ class BarcodeElementTest refused("VALIDATE", 203, barcode("code128", "TEST-0001").put("scale", 2), Map.of())); /////////////////////////////////////////////////////////////////////////////////////////// - // the widest quiet zone, and a quiet zone of four where every symbology allowed is two- // - // dimensional, are what the schema says // + // the widest quiet zone, a quiet zone of four where every symbology allowed is two- // + // dimensional, and one of seven for an EAN-8, are what the schema says // /////////////////////////////////////////////////////////////////////////////////////////// assertEquals(64, BarcodeFixture.number(BarcodeFixture.finding(BarcodeFixture.answer(request("RENDER", 203, barcode("code128", "TEST-0001").put("quiet", 64).toString(), Map.of())), "BARCODE_MODULE"), - "measured", "quietModules")); + "measured", "quietLeftModules")); assertEquals(5, BarcodeFixture.number(BarcodeFixture.finding(BarcodeFixture.answer(request("RENDER", 203, barcode("qr", "TEST-0001").put("quiet", 5).toString(), Map.of())), "BARCODE_MODULE"), - "measured", "quietModules")); + "measured", "quietRightModules")); + assertEquals(7, BarcodeFixture.number(BarcodeFixture.finding(BarcodeFixture.answer(request("RENDER", 203, barcode("ean8", "12345670").put("quiet", 7).put("minHeight", "5mm").toString(), Map.of())), "BARCODE_MODULE"), + "measured", "quietLeftModules")); } @@ -517,14 +791,13 @@ class BarcodeElementTest ** A value bound to the record: text is encoded as it is, a number or ** true/false as JSON writes it; anything else, and nothing at all, is ** `VALUE_MISSING` at the property that binds it. Validation reads no - ** record, so it cannot fit a bound value and says so every time. + ** record: with no length declared it cannot fit a bound value, and says + ** so every time. ***************************************************************************/ @Test void aBoundValueIsReadFromTheRecordWhenRenderingAndNotProvenWhenValidating() throws Exception { - ObjectNode element = centred("code128", "", "90mm"); - element.remove("value"); - element.put("valuePath", "/record/value"); + ObjectNode element = bound("code128"); Map unproven = Map.of("code", "FIT_NOT_PROVEN", "severity", "WARNING", "element", "code", "box", List.of(22, 0, 719, 95)); assertEquals(List.of(unproven), BarcodeFixture.answer(request("VALIDATE", 203, element.toString(), Map.of())).get("diagnostics")); for(Map.Entry value : Map.of("TEST-0001", "TEST-0001", 12345, "12345", true, "true").entrySet()) @@ -550,9 +823,81 @@ class BarcodeElementTest + /*************************************************************************** + ** A bound barcode that declares `maxLength` is proven when the scene is + ** validated: the widest symbol a value of that length can need is fitted, + ** and the scene is refused if it does not fit. Twelve characters of Code + ** 128 are at most 167 modules of bars; sixty are 695, too many for 90 mm. + ** When printing, a longer value is `VALUE_TOO_LONG` before any encoding. + ** A symbology of one width, an EAN or a UPC, needs no length. A + ** two-dimensional symbol is fitted for that many bytes, which refuses a + ** box they cannot fit, and is still not proven, since another value of + ** that length can be the larger symbol. + ***************************************************************************/ + @Test + void aDeclaredLengthProvesTheFitOfABoundValue() throws Exception + { + assertEquals(List.of(), BarcodeFixture.answer(request("VALIDATE", 203, bound("code128").put("maxLength", 12).toString(), Map.of())).get("diagnostics")); + Map drawn = BarcodeFixture.answer(request("RENDER", 203, bound("code128").put("maxLength", 12).toString(), Map.of("value", "abcdefghijkl"))); + assertEquals(List.of("BARCODE_MODULE"), codes(drawn)); + assertEquals(167 + 20, BarcodeFixture.number(BarcodeFixture.finding(drawn, "BARCODE_MODULE"), "measured", "modules")); + assertEquals(Map.of("code", "VALUE_TOO_LONG", "severity", "ERROR", "element", "code", "box", List.of(22, 0, 719, 95), "measured", Map.of("characters", 13), "limit", Map.of("characters", 12)), + refused("RENDER", 203, bound("code128").put("maxLength", 12), Map.of("value", "abcdefghijklm"))); + assertEquals(Map.of("code", "VALUE_TOO_LONG", "severity", "ERROR", "element", "code", "box", List.of(22, 0, 719, 95), "measured", Map.of("characters", 9), "limit", Map.of("characters", 8)), + refused("VALIDATE", 203, centred("code128", "TEST-0001", "90mm").put("maxLength", 8), Map.of())); + + Map wide = Map.of("code", "BARCODE_DOES_NOT_FIT", "severity", "ERROR", "element", "code", "box", List.of(22, 0, 719, 95), + "measured", measured(715, 10, 10, 0, 0, 1430, 209), "limit", Map.of("widthDots", 719, "heightDots", 95, "minModuleDots", 2)); + assertEquals(wide, refused("VALIDATE", 203, bound("code128").put("maxLength", 60), Map.of())); + assertEquals(List.of("BARCODE_MODULE"), codes(BarcodeFixture.answer(request("RENDER", 203, bound("code128").put("maxLength", 60).toString(), Map.of("value", "0".repeat(40)))))); + + /////////////////////////////////////////////////////////////////////////////////////////// + // the widest symbol of each linear symbology, and the widths that have no length // + /////////////////////////////////////////////////////////////////////////////////////////// + for(String symbology : List.of("code39", "code93", "itf", "gs1-128")) + { + assertEquals(List.of(), BarcodeFixture.answer(request("VALIDATE", 203, bound(symbology).put("maxLength", 6).put("barcodeProfile", "none").toString(), Map.of())).get("diagnostics"), symbology); + assertEquals("BARCODE_DOES_NOT_FIT", refused("VALIDATE", 203, bound(symbology).put("maxLength", 80).put("barcodeProfile", "none"), Map.of()).get("code"), symbology); + assertEquals(List.of("FIT_NOT_PROVEN"), codes(BarcodeFixture.answer(request("VALIDATE", 203, bound(symbology).toString(), Map.of()))), symbology); + } + for(String symbology : List.of("ean13", "ean8", "upca", "upce")) + { + assertEquals(List.of("BARCODE_HEIGHT_BELOW_RECOMMENDED"), codes(BarcodeFixture.answer(request("VALIDATE", 203, bound(symbology).put("minHeight", "10mm").toString(), Map.of()))), symbology); + assertEquals("BARCODE_DOES_NOT_FIT", refused("VALIDATE", 203, bound(symbology), Map.of()).get("code"), symbology); + assertEquals("BARCODE_DOES_NOT_FIT", refused("VALIDATE", 203, bound(symbology).put("minHeight", "10mm").put("width", "10mm"), Map.of()).get("code"), symbology); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // forty bytes of QR are 29 modules and with its quiet zones 37: at 3 dots, 111 of the // + // 155 the printable area is high // + /////////////////////////////////////////////////////////////////////////////////////////// + ObjectNode square = barcode("qr", ""); + square.remove("value"); + square.put("valuePath", "/record/value").put("width", "20mm"); + assertEquals(List.of("FIT_NOT_PROVEN"), codes(BarcodeFixture.answer(request("VALIDATE", 203, square.deepCopy().put("maxLength", 40).toString(), Map.of())))); + assertEquals(List.of("FIT_NOT_PROVEN"), codes(BarcodeFixture.answer(request("VALIDATE", 203, square.toString(), Map.of())))); + Map large = refused("VALIDATE", 203, square.deepCopy().put("maxLength", 200), Map.of()); + assertEquals(List.of("BARCODE_DOES_NOT_FIT", 61, 183), List.of(large.get("code"), BarcodeFixture.number(large, "measured", "modules"), BarcodeFixture.number(large, "measured", "widthDots"))); + assertEquals(Map.of("code", "BARCODE_DOES_NOT_FIT", "severity", "ERROR", "element", "code", "pointer", "/elements/0/maxLength", "box", List.of(0, 0, 159, 155), "measured", Map.of("characters", 4000)), + refused("VALIDATE", 203, square.deepCopy().put("maxLength", 4000), Map.of())); + for(String symbology : List.of("datamatrix", "pdf417", "aztec")) + { + ObjectNode shaped = square.deepCopy().put("symbology", symbology).put("width", "60mm"); + assertEquals(List.of("FIT_NOT_PROVEN"), codes(BarcodeFixture.answer(request("VALIDATE", 203, shaped.deepCopy().put("maxLength", 20).toString(), Map.of()))), symbology); + assertEquals(List.of("FIT_NOT_PROVEN"), codes(BarcodeFixture.answer(request("VALIDATE", 203, shaped.toString(), Map.of()))), symbology); + assertEquals("BARCODE_DOES_NOT_FIT", refused("VALIDATE", 203, shaped.deepCopy().put("maxLength", 1200), Map.of()).get("code"), symbology); + assertEquals(List.of("FIT_NOT_PROVEN", "BARCODE_MODULE"), codes(BarcodeFixture.answer(request("RENDER", 203, shaped.deepCopy().put("maxLength", 20).toString(), Map.of("value", "TEST-0001")))), symbology); + } + } + + + /*************************************************************************** ** A symbology read from the record must be one the scene allows. The ** symbol is then fitted by the rules of the symbology the record names. + ** Validated, a literal value is fitted in every symbology the record may + ** name, so nothing is left unproven; one of them that cannot hold the + ** value is the scene's mistake. ***************************************************************************/ @Test void aBoundSymbologyIsHeldToTheListTheSceneAllows() throws Exception @@ -560,26 +905,39 @@ class BarcodeElementTest ObjectNode element = Wire.JSON.createObjectNode().put("id", "code").put("type", "barcode").put("symbologyPath", "/record/type").put("value", "TEST-0001").put("width", "40mm").put("height", "15mm"); element.putArray("allowedSymbologies").add("code128").add("qr"); Map linear = BarcodeFixture.answer(request("RENDER", 203, element.toString(), Map.of("type", "code128"))); - assertEquals(List.of(10, 2), List.of(BarcodeFixture.number(BarcodeFixture.finding(linear, "BARCODE_MODULE"), "measured", "quietModules"), BarcodeFixture.number(BarcodeFixture.finding(linear, "BARCODE_MODULE"), "limit", "minModuleDots"))); + assertEquals(List.of(10, 2), List.of(BarcodeFixture.number(BarcodeFixture.finding(linear, "BARCODE_MODULE"), "measured", "quietLeftModules"), BarcodeFixture.number(BarcodeFixture.finding(linear, "BARCODE_MODULE"), "limit", "minModuleDots"))); assertEquals("TEST-0001", BarcodeFixture.decode(BarcodeFixture.page(linear), BarcodeFormat.CODE_128, false).getText()); Map matrix = BarcodeFixture.answer(request("RENDER", 203, element.toString(), Map.of("type", "qr"))); - assertEquals(List.of(4, 3), List.of(BarcodeFixture.number(BarcodeFixture.finding(matrix, "BARCODE_MODULE"), "measured", "quietModules"), BarcodeFixture.number(BarcodeFixture.finding(matrix, "BARCODE_MODULE"), "limit", "minModuleDots"))); + assertEquals(List.of(4, 3), List.of(BarcodeFixture.number(BarcodeFixture.finding(matrix, "BARCODE_MODULE"), "measured", "quietLeftModules"), BarcodeFixture.number(BarcodeFixture.finding(matrix, "BARCODE_MODULE"), "limit", "minModuleDots"))); assertEquals("TEST-0001", BarcodeFixture.decode(BarcodeFixture.page(matrix), BarcodeFormat.QR_CODE, false).getText()); List box = List.of(0, 0, 319, 119); assertEquals(Map.of("code", "SCENE_INVALID", "severity", "ERROR", "element", "code", "pointer", "/elements/0/symbologyPath", "box", box), refused("RENDER", 203, element, Map.of("type", "datamatrix"))); + assertEquals(Map.of("code", "SCENE_INVALID", "severity", "ERROR", "element", "code", "pointer", "/elements/0/symbologyPath", "box", box), refused("RENDER", 203, element, Map.of("type", "codabar"))); assertEquals(Map.of("code", "VALUE_MISSING", "severity", "ERROR", "element", "code", "pointer", "/elements/0/symbologyPath", "box", box), refused("RENDER", 203, element, Map.of())); assertEquals(Map.of("code", "VALUE_MISSING", "severity", "ERROR", "element", "code", "pointer", "/elements/0/symbologyPath", "box", box), refused("RENDER", 203, element, Map.of("type", 128))); /////////////////////////////////////////////////////////////////////////////////////////// - // validated, the symbology is not known: the fit is not proven, and a stated minimum is // - // held against the largest default of the symbologies allowed // + // validated, the symbology is not known: the literal is fitted in each one allowed, and // + // a stated minimum is held against the largest default among them // /////////////////////////////////////////////////////////////////////////////////////////// - assertEquals(List.of("FIT_NOT_PROVEN"), codes(BarcodeFixture.answer(request("VALIDATE", 203, element.toString(), Map.of())))); + assertEquals(List.of(), codes(BarcodeFixture.answer(request("VALIDATE", 203, element.toString(), Map.of())))); Map small = BarcodeFixture.answer(request("VALIDATE", 203, element.put("minModule", "0.25mm").toString(), Map.of())); - assertEquals(List.of("FIT_NOT_PROVEN", "BARCODE_MODULE_BELOW_RECOMMENDED"), codes(small)); + assertEquals(List.of("BARCODE_MODULE_BELOW_RECOMMENDED"), codes(small)); assertEquals(Map.of("minModuleDots", 3), BarcodeFixture.finding(small, "BARCODE_MODULE_BELOW_RECOMMENDED").get("limit")); - assertEquals(List.of("FIT_NOT_PROVEN", "BARCODE_MODULE"), codes(BarcodeFixture.answer(request("RENDER", 203, element.toString(), Map.of("type", "code128"))))); + assertEquals(List.of("BARCODE_MODULE"), codes(BarcodeFixture.answer(request("RENDER", 203, element.toString(), Map.of("type", "code128"))))); + element.remove("minModule"); + + Map narrow = refused("VALIDATE", 203, element.deepCopy().put("width", "12mm"), Map.of()); + assertEquals(List.of("BARCODE_DOES_NOT_FIT", 143), List.of(narrow.get("code"), BarcodeFixture.number(narrow, "measured", "modules"))); + Map flat = refused("VALIDATE", 203, element.deepCopy().put("height", "8mm"), Map.of()); + assertEquals(List.of("BARCODE_DOES_NOT_FIT", 29), List.of(flat.get("code"), BarcodeFixture.number(flat, "measured", "modules"))); + ObjectNode retail = element.deepCopy(); + retail.putArray("allowedSymbologies").add("code128").add("ean13"); + assertEquals(Map.of("code", "VALUE_NOT_ENCODABLE", "severity", "ERROR", "element", "code", "pointer", "/elements/0/value", "box", box, "measured", Map.of("characters", 9), "limit", Map.of("characters", 13)), + refused("VALIDATE", 203, retail, Map.of())); + assertEquals(Map.of("code", "VALUE_TOO_LONG", "severity", "ERROR", "element", "code", "box", box, "measured", Map.of("characters", 9), "limit", Map.of("characters", 4)), + refused("VALIDATE", 203, element.deepCopy().put("maxLength", 4), Map.of())); } @@ -599,16 +957,30 @@ class BarcodeElementTest assertEquals("001234", BarcodeFixture.decode(BarcodeFixture.page(answer), BarcodeFormat.CODE_128, false).getText()); assertEquals(List.of("FIT_NOT_PROVEN"), codes(BarcodeFixture.answer(request("VALIDATE", 203, element.toString(), Map.of())))); - ObjectNode retail = element.deepCopy().put("symbology", "ean13"); + ObjectNode retail = element.deepCopy().put("symbology", "ean13").put("minHeight", "10mm"); Map checked = BarcodeFixture.answer(request("RENDER", 203, retail.toString(), Map.of("value", "0012345678905", "label", "EAN13", "aim", "]E0"))); assertEquals("0012345678905", BarcodeFixture.decode(BarcodeFixture.page(checked), BarcodeFormat.EAN_13, false).getText()); + /////////////////////////////////////////////////////////////////////////////////////////// + // a literal with a profile is fitted when the scene is validated, where no record says // + // what was scanned, and held to the profile when it is rendered // + /////////////////////////////////////////////////////////////////////////////////////////// + ObjectNode literal = element.deepCopy(); + literal.remove("valuePath"); + literal.put("value", "001234"); + assertEquals(List.of("BARCODE_MODULE"), codes(BarcodeFixture.answer(request("VALIDATE", 203, literal.toString(), Map.of())))); + assertEquals(List.of("BARCODE_MODULE"), codes(BarcodeFixture.answer(request("RENDER", 203, literal.toString(), Map.of("label", "CODE128", "aim", "]C0"))))); + List box = List.of(0, 0, 479, 95); Map profile = Map.of("code", "SCENE_INVALID", "severity", "ERROR", "element", "code", "pointer", "/elements/0/scannerSymbologyPath", "box", box); assertEquals(profile, refused("RENDER", 203, element, Map.of("value", "001234", "label", "CODE128", "aim", "]C1"))); assertEquals(profile, refused("RENDER", 203, element, Map.of("value", "001234", "label", "QRCODE", "aim", "]Q1"))); - Map unusable = Map.of("code", "SCENE_INVALID", "severity", "ERROR", "element", "code", "pointer", "/elements/0/valuePath", "box", box, "measured", Map.of("characters", 13)); - assertEquals(unusable, refused("RENDER", 203, retail, Map.of("value", "0012345678901", "label", "EAN13", "aim", "]E0"))); + assertEquals(profile, refused("RENDER", 203, literal, Map.of("label", "QRCODE", "aim", "]Q1"))); + Map mistyped = Map.of("code", "VALUE_NOT_ENCODABLE", "severity", "ERROR", "element", "code", "pointer", "/elements/0/valuePath", "box", box, + "measured", Map.of("characters", 13, "checkDigitMatches", 0), "limit", Map.of("characters", 13, "checkDigitMatches", 1)); + assertEquals(mistyped, refused("RENDER", 203, retail, Map.of("value", "0012345678901", "label", "EAN13", "aim", "]E0"))); + Map unusable = Map.of("code", "VALUE_NOT_ENCODABLE", "severity", "ERROR", "element", "code", "pointer", "/elements/0/valuePath", "box", box, "measured", Map.of("characters", 7)); + assertEquals(unusable, refused("RENDER", 203, element, Map.of("value", "0012\t34", "label", "CODE128", "aim", "]C0"))); assertEquals(Map.of("code", "VALUE_MISSING", "severity", "ERROR", "element", "code", "pointer", "/elements/0/scannerSymbologyPath", "box", box), refused("RENDER", 203, element, Map.of("value", "001234", "aim", "]C0"))); assertEquals(Map.of("code", "VALUE_MISSING", "severity", "ERROR", "element", "code", "pointer", "/elements/0/aimIdentifierPath", "box", box), @@ -626,21 +998,28 @@ class BarcodeElementTest void aValueTheSymbologyCannotHoldIsRefusedWithoutTheValue() throws Exception { String secret = "TEST-SECRET-0001"; - for(String[] unusable : List.of(new String[] {"itf", "SCENE_INVALID"}, new String[] {"ean13", "SCENE_INVALID"}, new String[] {"gs1-128", "GS1_INVALID"})) + Map> refusals = new LinkedHashMap<>(); + refusals.put("itf", Map.of("code", "VALUE_NOT_ENCODABLE", "measured", Map.of("characters", 16))); + refusals.put("code39", Map.of("code", "VALUE_NOT_ENCODABLE", "measured", Map.of("characters", 16))); + refusals.put("ean13", Map.of("code", "VALUE_NOT_ENCODABLE", "measured", Map.of("characters", 16), "limit", Map.of("characters", 13))); + refusals.put("gs1-128", Map.of("code", "GS1_INVALID", "measured", Map.of("characters", 16))); + for(Map.Entry> unusable : refusals.entrySet()) { - Refusal literal = assertThrows(Refusal.class, () -> RendererMain.process(request("VALIDATE", 203, barcode(unusable[0], secret).toString(), Map.of()))); - assertEquals(Map.of("code", unusable[1], "severity", "ERROR", "element", "code", "pointer", "/elements/0/value", "box", List.of(0, 0, 764, 155), "measured", Map.of("characters", 16)), - literal.diagnostic().wire()); - ObjectNode bound = barcode(unusable[0], ""); + String value = "code39".equals(unusable.getKey()) ? secret.toLowerCase(java.util.Locale.ROOT) : secret; + Map expected = new LinkedHashMap<>(unusable.getValue()); + expected.putAll(Map.of("severity", "ERROR", "element", "code", "pointer", "/elements/0/value", "box", List.of(0, 0, 764, 155))); + Refusal literal = assertThrows(Refusal.class, () -> RendererMain.process(request("VALIDATE", 203, barcode(unusable.getKey(), value).toString(), Map.of()))); + assertEquals(expected, literal.diagnostic().wire()); + ObjectNode bound = barcode(unusable.getKey(), ""); bound.remove("value"); - Refusal read = assertThrows(Refusal.class, () -> RendererMain.process(request("RENDER", 203, bound.put("valuePath", "/record/value").toString(), Map.of("value", secret)))); + Refusal read = assertThrows(Refusal.class, () -> RendererMain.process(request("RENDER", 203, bound.put("valuePath", "/record/value").toString(), Map.of("value", value)))); assertEquals("/elements/0/valuePath", read.diagnostic().pointer()); for(Refusal refusal : List.of(literal, read)) { - assertEquals(unusable[1], refusal.getMessage()); + assertEquals(unusable.getValue().get("code"), refusal.getMessage()); assertNull(refusal.getCause()); assertEquals(0, refusal.getSuppressed().length); - assertFalse(refusal.toString().contains("SECRET") || refusal.diagnostic().wire().toString().contains("SECRET")); + assertFalse(refusal.toString().toUpperCase(java.util.Locale.ROOT).contains("SECRET") || refusal.diagnostic().wire().toString().toUpperCase(java.util.Locale.ROOT).contains("SECRET")); } } String overridden = "{\"schema\":\"qqq-zpl-scene/2\",\"design\":{\"width\":\"4in\",\"height\":\"1in\"},\"elements\":[" + barcode("code128", "TEST-0001") @@ -650,22 +1029,106 @@ class BarcodeElementTest + /*************************************************************************** + ** The values a symbol would not read back as, each refused before the + ** encoder sees it, literal or from the record, with numbers only. An + ** escape of the encoder in a Code 128, Data Matrix, Aztec or QR value, or + ** in any other, is `VALUE_NOT_ENCODABLE` with a count of characters (for a + ** `gs1-128`, `GS1_INVALID`): without the refusal the first of these would + ** be drawn as a GS1-128 of the record's choosing, and decodes as one. An + ** odd number of Interleaved 2 of 5 digits is never padded. A retail number + ** short of its check digit is never completed, and one with a wrong check + ** digit never drawn. A GS1 key with a wrong check digit is `GS1_INVALID` + ** with its application identifier. + ***************************************************************************/ + @Test + void aValueThatWouldNotReadBackAsItselfIsRefused() throws Exception + { + List box = List.of(0, 0, 764, 155); + for(String symbology : BarcodeFit.TYPES) + { + for(String escape : List.of("\\", "\\", "\\", "\\", "\\<")) + { + for(String value : List.of(escape + "0112345678901231", "SECRET" + escape + "A", "0012345678" + escape)) + { + String code = "gs1-128".equals(symbology) ? "GS1_INVALID" : "VALUE_NOT_ENCODABLE"; + Map expected = Map.of("code", code, "severity", "ERROR", "element", "code", "pointer", "/elements/0/value", "box", box, "measured", Map.of("characters", value.length())); + Refusal literal = assertThrows(Refusal.class, () -> RendererMain.process(request("VALIDATE", 203, barcode(symbology, value).put("minHeight", "5mm").toString(), Map.of())), symbology + " " + escape); + assertEquals(expected, literal.diagnostic().wire(), symbology + " " + escape); + ObjectNode bound = barcode(symbology, "").put("minHeight", "5mm"); + bound.remove("value"); + Refusal read = assertThrows(Refusal.class, () -> RendererMain.process(request("RENDER", 203, bound.put("valuePath", "/record/value").toString(), Map.of("value", value))), symbology + " " + escape); + assertEquals("/elements/0/valuePath", read.diagnostic().pointer()); + assertEquals(Map.of("characters", value.length()), read.diagnostic().wire().get("measured")); + assertFalse(literal.toString().contains("SECRET") || read.toString().contains("SECRET") || read.diagnostic().wire().toString().contains("SECRET") || read.diagnostic().wire().toString().contains("FNC")); + } + } + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // what the first of those values would have been: the encoder of version 1, given it // + // as it is, draws a symbol that a scanner reads as GS1-128 // + /////////////////////////////////////////////////////////////////////////////////////////// + java.awt.image.BufferedImage injected = com.kingsrook.qbits.renderer.Barcodes.renderForFormat("ZPL", "code128", "\\0112345678901231", 3, 10, Wire.JSON.createObjectNode()); + boolean[][] picture = new boolean[injected.getHeight()][injected.getWidth()]; + for(int y = 0; y < picture.length; y++) + { + for(int x = 0; x < picture[y].length; x++) + { + picture[y][x] = (injected.getRGB(x, y) & 0xFFFFFF) == 0; + } + } + assertEquals("]C10112345678901231", BarcodeFixture.decode(picture, BarcodeFormat.CODE_128, true).getText()); + + for(String odd : List.of("0", "001", "0012345", "001234567890123")) + { + Map expected = Map.of("code", "VALUE_NOT_ENCODABLE", "severity", "ERROR", "element", "code", "pointer", "/elements/0/value", "box", box, + "measured", Map.of("characters", odd.length()), "limit", Map.of("charactersMultipleOf", 2)); + assertEquals(expected, refused("VALIDATE", 203, barcode("itf", odd), Map.of()), odd); + assertEquals(List.of("BARCODE_MODULE"), codes(BarcodeFixture.answer(request("VALIDATE", 203, barcode("itf", odd + "0").toString(), Map.of()))), odd); + } + + Map numbers = Map.of("ean13", "0012345678905", "ean8", "12345670", "upca", "012345678905", "upce", "04252614"); + for(Map.Entry number : numbers.entrySet()) + { + String whole = number.getValue(); + ObjectNode element = barcode(number.getKey(), whole).put("minHeight", "5mm"); + assertEquals(List.of("BARCODE_HEIGHT_BELOW_RECOMMENDED", "BARCODE_MODULE"), codes(BarcodeFixture.answer(request("VALIDATE", 203, element.toString(), Map.of()))), number.getKey()); + assertEquals(Map.of("code", "VALUE_NOT_ENCODABLE", "severity", "ERROR", "element", "code", "pointer", "/elements/0/value", "box", box, "measured", Map.of("characters", whole.length() - 1), "limit", Map.of("characters", whole.length())), + refused("VALIDATE", 203, element.deepCopy().put("value", whole.substring(0, whole.length() - 1)), Map.of()), number.getKey()); + assertEquals(Map.of("code", "VALUE_NOT_ENCODABLE", "severity", "ERROR", "element", "code", "pointer", "/elements/0/value", "box", box, "measured", Map.of("characters", whole.length(), "checkDigitMatches", 0), + "limit", Map.of("characters", whole.length(), "checkDigitMatches", 1)), refused("RENDER", 203, element.deepCopy().put("value", whole.substring(0, whole.length() - 1) + "9"), Map.of()), number.getKey()); + } + + Map key = Map.of("code", "GS1_INVALID", "severity", "ERROR", "element", "code", "pointer", "/elements/0/value", "box", List.of(22, 0, 719, 95), + "measured", Map.of("characters", 44, "applicationIdentifier", 1, "checkDigitMatches", 0), "limit", Map.of("checkDigitMatches", 1)); + assertEquals(key, refused("VALIDATE", 203, general(TYPICAL.replace("678905", "678904"), "90mm"), Map.of())); + assertEquals(key, refused("RENDER", 203, general(TYPICAL.replace("678905", "678904"), "90mm"), Map.of())); + Map sscc = refused("RENDER", 203, general("[00]000123456789012345", "90mm"), Map.of()); + assertEquals(List.of("GS1_INVALID", Map.of("characters", 22, "applicationIdentifier", 0, "checkDigitMatches", 0)), List.of(sscc.get("code"), sscc.get("measured"))); + assertEquals(List.of("BARCODE_MODULE"), codes(BarcodeFixture.answer(request("RENDER", 203, general("[00]00012345678901234" + BarcodeFixture.checkDigit("00012345678901234"), "90mm").toString(), Map.of())))); + } + + + /*************************************************************************** ** Whatever the value, every fit-qualified symbology ends as a label or as a ** refusal with a diagnostic, bound or literal: an encoder's own exception ** never leaves the element. The values are the awkward ones: characters no ** symbology here has, control characters, half of a surrogate pair, escape ** sequences the encoders read, broken element strings, the longest value - ** and one of every length a retail symbol could take. + ** and one of every length a retail symbol could take. A value that holds + ** the beginning of an escape, and an odd number of digits of Interleaved 2 + ** of 5, never ends as a label. ***************************************************************************/ @Test void everyValueEndsAsALabelOrARefusal() throws Exception { String accented = Character.toString(0xE9); List values = new ArrayList<>(List.of(" ", "\u0000", "\t\r\n", "\u007f", accented, Character.toString(0xFF), Character.toString(0x100), Character.toString(0x2603), String.valueOf((char) 0xD83D), Character.toString(0x1F600), String.valueOf((char) 0xFFFF), "~", "~1", "\\", "\\", "\\0100012345678905", - "\\", "\\<", "\\x", "\\d065", "\\u00e9", "\\eci", "^ECI000003", "[", "]", "[]", "[0", "[01]", "[01]1", "[01]00012345678905[", "[01]0001234567890A", "[01]00012345678906", "[99]", "[9999]TEST", "[01][01]", "(01)00012345678905", - "[01]00012345678905[10]", "[21]" + "A".repeat(21), "[00]123456789012345678", "*", "*TEST*", "test", "Test-0001", "+", "%", "$", "/", ".", "-", "TEST 0001", "A".repeat(4000), "0".repeat(4000), "01".repeat(600), - accented.repeat(200), "[01]00012345678905".repeat(40))); + "\\", "\\", "\\", "\\A", "\\<", "A\\34", "\\x", "\\d065", "\\u00e9", "\\eci", "^ECI000003", "[", "]", "[]", "[0", "[01]", "[01]1", "[01]00012345678905[", "[01]0001234567890A", "[01]00012345678906", "[99]", "[9999]TEST", "[01][01]", "(01)00012345678905", + "[01]00012345678905[10]", "[21]" + "A".repeat(21), "[00]123456789012345678", "[01]00012345678905[21]\\", "*", "*TEST*", "test", "Test-0001", "+", "%", "$", "/", ".", "-", "TEST 0001", "A".repeat(4000), "0".repeat(4000), "01".repeat(600), + accented.repeat(200), "[01]00012345678905".repeat(40), "0012345678905", "12345670", "012345678905", "04252614", "0012345678905+12")); for(int digits = 1; digits <= 15; digits++) { values.add("0123456789012345".substring(0, digits)); @@ -673,44 +1136,52 @@ class BarcodeElementTest } int labels = 0; int refusals = 0; + int escapes = 0; for(String symbology : BarcodeFit.TYPES) { for(String value : values) { - ObjectNode literal = barcode(symbology, value).put("minModule", "1dot"); + boolean never = value.contains("\\<") || ("itf".equals(symbology) && value.length() % 2 != 0); + escapes += never ? 1 : 0; + ObjectNode literal = barcode(symbology, value).put("minModule", "1dot").put("minHeight", "1dot"); ObjectNode bound = barcode(symbology, ""); bound.remove("value"); - bound.put("valuePath", "/record/value").put("minModule", "1dot"); + bound.put("valuePath", "/record/value").put("minModule", "1dot").put("minHeight", "1dot"); for(ObjectNode request : List.of(request("RENDER", 600, literal.toString(), Map.of()), request("VALIDATE", 203, literal.toString(), Map.of()), request("RENDER", 300, bound.toString(), Map.of("value", value)))) { try { assertEquals("OK", RendererMain.process(request).get("outcome")); + assertFalse(never, symbology + " drew a value of " + value.length() + " characters that does not read back as itself"); labels++; } catch(Refusal refusal) { refusals++; - assertTrue(List.of("BARCODE_DOES_NOT_FIT", "SCENE_INVALID", "GS1_INVALID").contains(refusal.diagnostic().code()), symbology + " refused a value of " + value.length() + " characters as " + refusal.diagnostic().code()); + assertTrue(List.of("BARCODE_DOES_NOT_FIT", "VALUE_NOT_ENCODABLE", "GS1_INVALID").contains(refusal.diagnostic().code()), symbology + " refused a value of " + value.length() + " characters as " + refusal.diagnostic().code()); + assertTrue(!never || List.of("VALUE_NOT_ENCODABLE", "GS1_INVALID").contains(refusal.diagnostic().code()), symbology + " refused an escape as " + refusal.diagnostic().code()); assertEquals("code", refusal.diagnostic().element()); } } } } - assertTrue(labels > 500 && refusals > 500, labels + " labels, " + refusals + " refusals"); + assertTrue(labels > 500 && refusals > 500 && escapes > 100, labels + " labels, " + refusals + " refusals, " + escapes + " values that must not be drawn"); } /*************************************************************************** - ** A symbol is its own white ground: over a filled box its quiet zones and - ** spaces are white, so it decodes, and the box is untouched beside it. + ** A symbol is its own white ground: over a filled box painted before it + ** its quiet zones and spaces are white, so it decodes, and the box is + ** untouched beside it. ***************************************************************************/ @Test void theSymbolClearsItsOwnGroundQuietZonesIncluded() throws Exception { String ground = "{\"id\":\"ground\",\"type\":\"box\",\"width\":\"94mm\",\"height\":\"14mm\",\"fill\":true},"; - Map answer = BarcodeFixture.answer(request("RENDER", 203, ground + centred("gs1-128", TYPICAL, "90mm"), Map.of())); + Map answer = BarcodeFixture.answer(request("RENDER", 203, ground + general(TYPICAL, "90mm"), Map.of())); + assertEquals(List.of("BARCODE_MODULE"), codes(answer)); + assertEquals(codes(answer), codes(BarcodeFixture.answer(request("VALIDATE", 203, ground + general(TYPICAL, "90mm"), Map.of())))); boolean[][] page = BarcodeFixture.page(answer); int left = 24 + 22 + 40; for(int y = 24; y < 24 + 95; y++) @@ -732,4 +1203,93 @@ class BarcodeElementTest } assertEquals(SCANNED, BarcodeFixture.decode(page, BarcodeFormat.CODE_128, true).getText()); } + + + + /*************************************************************************** + ** Nothing is painted over a barcode. An element painted after it 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: with the pointer of the barcode, the edges of the ink and the + ** edges of what the barcode keeps. The symbol here is the worked example, + ** 2 mm down the label: in a box at 22, 16 of 719 by 95 dots it keeps the + ** 638 dots from 62. A filled box, a box that only clips a quiet zone, the + ** bar of an outline, a panel of reverse text that would turn the symbol + ** white on black, plain text, another barcode and the child of a group are + ** all refused; ink in the spare of the box beside the symbol, an outline + ** around the label and a group that only holds elements elsewhere are not. + ***************************************************************************/ + @Test + void nothingIsPaintedOverABarcodeOrItsQuietZones() throws Exception + { + String code = general(TYPICAL, "90mm").put("y", "2mm") + ","; + Map kept = Map.of("leftDots", 62, "topDots", 16, "rightDots", 700, "bottomDots", 111); + Map> intruders = new LinkedHashMap<>(); + intruders.put("{\"id\":\"over\",\"type\":\"box\",\"x\":\"40mm\",\"y\":\"5mm\",\"width\":\"5mm\",\"height\":\"2mm\",\"fill\":true}", List.of(320, 40, 359, 55)); + intruders.put("{\"id\":\"over\",\"type\":\"box\",\"x\":\"8mm\",\"y\":\"13mm\",\"width\":\"2mm\",\"height\":\"4mm\",\"fill\":true}", List.of(64, 104, 79, 135)); + intruders.put("{\"id\":\"over\",\"type\":\"box\",\"x\":\"87.4mm\",\"y\":\"2mm\",\"width\":\"1mm\",\"height\":\"1mm\",\"thickness\":\"1dot\"}", List.of(699, 16, 706, 17)); + intruders.put("{\"id\":\"over\",\"type\":\"text\",\"value\":\"TEST\",\"font\":\"qqq-sans-bold\",\"size\":\"9pt\",\"x\":\"30mm\",\"y\":\"4mm\",\"width\":\"30mm\",\"height\":\"8mm\",\"reverse\":true}", List.of(240, 32, 479, 95)); + intruders.put("{\"id\":\"over\",\"type\":\"text\",\"value\":\"TEST\",\"font\":\"qqq-sans-bold\",\"size\":\"9pt\",\"x\":\"30mm\",\"y\":\"12mm\",\"width\":\"30mm\",\"height\":\"6mm\"}", List.of(240, 96, 479, 143)); + intruders.put("{\"id\":\"over\",\"type\":\"barcode\",\"symbology\":\"qr\",\"value\":\"TEST-0001\",\"x\":\"80mm\",\"y\":\"7mm\",\"width\":\"12mm\",\"height\":\"12mm\"}", List.of(639, 56, 734, 151)); + for(Map.Entry> intruder : intruders.entrySet()) + { + List edges = intruder.getValue(); + for(String operation : List.of("VALIDATE", "RENDER")) + { + Map refusal = refused(operation, 203, code + intruder.getKey(), Map.of()); + assertEquals(List.of("QUIET_ZONE_INTRUDED", "ERROR", "over", "/elements/0", kept), List.of(refusal.get("code"), refusal.get("severity"), refusal.get("element"), refusal.get("pointer"), refusal.get("limit")), intruder.getKey()); + assertEquals(Map.of("leftDots", edges.get(0), "topDots", edges.get(1), "rightDots", edges.get(2), "bottomDots", edges.get(3)), refusal.get("measured"), intruder.getKey()); + } + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // the child of a later group is the element refused; and collecting keeps the barcode, // + // refuses only what would cover it and goes on with the rest // + /////////////////////////////////////////////////////////////////////////////////////////// + String group = "{\"id\":\"panel\",\"type\":\"group\",\"y\":\"2mm\",\"width\":\"94mm\",\"height\":\"16mm\",\"children\":[{\"id\":\"fine\",\"type\":\"box\",\"y\":\"13mm\",\"width\":\"94mm\",\"height\":\"1pt\",\"thickness\":\"1pt\"}," + + "{\"id\":\"inside\",\"type\":\"box\",\"x\":\"20mm\",\"width\":\"1mm\",\"height\":\"8mm\",\"fill\":true}]}"; + Map child = refused("VALIDATE", 203, code + group, Map.of()); + assertEquals(List.of("QUIET_ZONE_INTRUDED", "inside", "/elements/0", List.of(160, 16, 7, 63)), List.of(child.get("code"), child.get("element"), child.get("pointer"), child.get("box"))); + Map collected = RendererMain.process(request("RENDER", 203, code + group, Map.of()).put("diagnostics", "COLLECT")); + assertEquals(List.of("REFUSED", List.of("BARCODE_MODULE", "QUIET_ZONE_INTRUDED")), List.of(collected.get("outcome"), codes(collected))); + + /////////////////////////////////////////////////////////////////////////////////////////// + // not refused: ink in the 40 dots the box has to spare on either side of the symbol, an // + // outline around the whole label, a line under the box, and a group that covers the // + // barcode and holds only a line elsewhere. All of them are painted after the barcode // + /////////////////////////////////////////////////////////////////////////////////////////// + String beside = "{\"id\":\"beside\",\"type\":\"box\",\"x\":\"3mm\",\"y\":\"2mm\",\"width\":\"4.5mm\",\"height\":\"12mm\",\"fill\":true}," + + "{\"id\":\"after\",\"type\":\"box\",\"anchor\":\"top-right\",\"x\":\"3mm\",\"y\":\"2mm\",\"width\":\"4.5mm\",\"height\":\"12mm\",\"fill\":true}," + + "{\"id\":\"frame\",\"type\":\"box\",\"thickness\":\"1pt\"}," + + "{\"id\":\"rule\",\"type\":\"box\",\"y\":\"14.5mm\",\"width\":\"94mm\",\"height\":\"1pt\",\"thickness\":\"1pt\"}," + + "{\"id\":\"panel\",\"type\":\"group\",\"children\":[{\"id\":\"low\",\"type\":\"box\",\"anchor\":\"bottom-left\",\"y\":\"1mm\",\"width\":\"50mm\",\"height\":\"1pt\",\"thickness\":\"1pt\"}]}"; + Map clear = BarcodeFixture.answer(request("RENDER", 203, code + beside, Map.of())); + assertEquals(List.of("BARCODE_MODULE"), codes(clear)); + assertEquals(codes(clear), codes(BarcodeFixture.answer(request("VALIDATE", 203, code + beside, Map.of())))); + boolean[][] page = BarcodeFixture.page(clear); + assertTrue(page[24 + 60][24 + 30] && page[24 + 60][24 + 735] && !page[24 + 60][24 + 62] && !page[24 + 60][24 + 699], "ink beside the symbol and none in its quiet zones"); + assertTrue(page[24 + 1][24 + 300] && page[24 + 117][24 + 300], "the outline of the label and the line under the box"); + for(int y = 0; y < page.length; y++) + { + for(int x = 0; x < page[y].length; x++) + { + page[y][x] &= x >= 24 + 62 && x < 24 + 700 && y >= 24 + 16 && y < 24 + 111; + } + } + assertEquals(SCANNED, BarcodeFixture.decode(page, BarcodeFormat.CODE_128, true).getText()); + + /////////////////////////////////////////////////////////////////////////////////////////// + // a barcode whose value comes from the record keeps its whole box when the scene is // + // validated, since the symbol may be anywhere in it, and the symbol alone when the // + // record is read: a value two digits longer is a symbol 22 dots wider, which the same // + // ink then enters // + /////////////////////////////////////////////////////////////////////////////////////////// + ObjectNode read = bound("gs1-128").put("barcodeProfile", "gs1-general").put("y", "2mm"); + String spare = read + ",{\"id\":\"beside\",\"type\":\"box\",\"x\":\"3mm\",\"y\":\"2mm\",\"width\":\"4.5mm\",\"height\":\"12mm\",\"fill\":true}"; + Map whole = refused("VALIDATE", 203, spare, Map.of()); + assertEquals(List.of("QUIET_ZONE_INTRUDED", "beside", Map.of("leftDots", 22, "topDots", 16, "rightDots", 741, "bottomDots", 111)), List.of(whole.get("code"), whole.get("element"), whole.get("limit"))); + assertEquals(List.of("FIT_NOT_PROVEN", "BARCODE_MODULE"), codes(BarcodeFixture.answer(request("RENDER", 203, spare, Map.of("value", TYPICAL))))); + Map wider = refused("RENDER", 203, spare, Map.of("value", LONG.substring(0, 46))); + assertEquals(List.of("QUIET_ZONE_INTRUDED", Map.of("leftDots", 51, "topDots", 16, "rightDots", 711, "bottomDots", 111)), List.of(wider.get("code"), wider.get("limit"))); + } } diff --git a/renderer/src/test/java/com/kingsrook/qbits/renderer/scene2/barcode/BarcodeFixture.java b/renderer/src/test/java/com/kingsrook/qbits/renderer/scene2/barcode/BarcodeFixture.java index 42a2b07..177b3b2 100644 --- a/renderer/src/test/java/com/kingsrook/qbits/renderer/scene2/barcode/BarcodeFixture.java +++ b/renderer/src/test/java/com/kingsrook/qbits/renderer/scene2/barcode/BarcodeFixture.java @@ -110,6 +110,26 @@ final class BarcodeFixture + /*************************************************************************** + ** 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. diff --git a/renderer/src/test/java/com/kingsrook/qbits/renderer/scene2/barcode/BarcodePropertyTest.java b/renderer/src/test/java/com/kingsrook/qbits/renderer/scene2/barcode/BarcodePropertyTest.java index 08ba760..9cfcfb1 100644 --- a/renderer/src/test/java/com/kingsrook/qbits/renderer/scene2/barcode/BarcodePropertyTest.java +++ b/renderer/src/test/java/com/kingsrook/qbits/renderer/scene2/barcode/BarcodePropertyTest.java @@ -41,8 +41,8 @@ import static org.junit.jupiter.api.Assertions.assertTrue; /******************************************************************************* ** Properties of a fitted barcode, over seeded symbologies, values, boxes, - ** resolutions, alignments and rotations, each read from the label a printer - ** would be sent. No clock and a fixed seed, so a failure replays. + ** 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 { @@ -75,12 +75,14 @@ class BarcodePropertyTest ** 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 the default 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, and centred leaves - ** equal margins within one dot; a linear symbol takes the whole of the - ** box across its bars; and the label decodes to the value; + ** 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. ***************************************************************************/ @@ -91,7 +93,8 @@ class BarcodePropertyTest int drawn = 0; int refused = 0; int below = 0; - for(int iteration = 0; iteration < 240; iteration++) + 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); @@ -108,18 +111,37 @@ class BarcodePropertyTest int rotation = random.nextInt(4) * 90; boolean upright = rotation % 180 == 0; boolean stated = random.nextInt(4) == 0; - String align = List.of("left", "center", "right").get(random.nextInt(3)); - String valign = List.of("top", "middle", "bottom").get(random.nextInt(3)); + 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, upright ? 43_000 : 94_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("align", align).put("valign", valign).put("rotation", rotation); + 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"; - int recommended = matrix ? Map.of(203, 3, 300, 5, 600, 9).get(dpi) : Map.of(203, 2, 300, 3, 600, 6).get(dpi); + Symbology rules = Symbology.of(symbology); + int recommended = rules.narrowest(Symbology.Profile.of(profile), dpi); int quiet = matrix ? 4 : 10; Map answer; @@ -137,7 +159,7 @@ class BarcodePropertyTest 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") - || (matrix && BarcodeFixture.number(wire, "measured", "heightDots") > BarcodeFixture.number(wire, "limit", "heightDots")), about + ": " + wire); + || 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; } @@ -165,11 +187,19 @@ class BarcodePropertyTest 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, "center", spareX / 2, "right", spareX).get(align), area.x() - box.x(), about); - assertEquals(Map.of("top", 0, "middle", spareY / 2, "bottom", spareY).get(valign), area.y() - box.y(), about); - assertTrue(!"center".equals(align) || Math.abs((area.x() - box.x()) - (box.right() - area.right())) <= 1, about); - assertTrue(!"middle".equals(valign) || Math.abs((area.y() - box.y()) - (box.bottom() - area.bottom())) <= 1, about); + 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])) @@ -203,6 +233,6 @@ class BarcodePropertyTest throw new AssertionError(about + ": module " + module + ", the label does not decode", unread); } } - assertTrue(drawn > 80 && refused > 40 && below > 5, drawn + " drawn, " + refused + " refused, " + below + " below the default module"); + assertTrue(drawn > 80 && refused > 40 && below > 5 && low > 2, drawn + " drawn, " + refused + " refused, " + below + " below the module asked, " + low + " with bars shorter than asked"); } } diff --git a/renderer/src/test/java/com/kingsrook/qbits/renderer/scene2/barcode/ModuleFitTest.java b/renderer/src/test/java/com/kingsrook/qbits/renderer/scene2/barcode/ModuleFitTest.java index 838496f..7f2b49f 100644 --- a/renderer/src/test/java/com/kingsrook/qbits/renderer/scene2/barcode/ModuleFitTest.java +++ b/renderer/src/test/java/com/kingsrook/qbits/renderer/scene2/barcode/ModuleFitTest.java @@ -24,7 +24,6 @@ package com.kingsrook.qbits.renderer.scene2.barcode; import java.util.ArrayList; import java.util.List; import java.util.SplittableRandom; -import com.kingsrook.qbits.renderer.BarcodeFit; import com.kingsrook.qbits.renderer.scene2.Length; import com.kingsrook.qbits.renderer.scene2.Scene2; import org.junit.jupiter.api.Test; @@ -35,35 +34,22 @@ 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. + ** 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; /*************************************************************************** - ** The defaults are the design's: 0.25 mm for a linear symbol and 0.375 mm - ** for a two-dimensional one, which taken up to whole dots is 2 and 3 dots - ** at 203 dpi, 3 and 5 at 300 dpi and 6 and 9 at 600 dpi; quiet zones are at - ** least 10 and 4 modules. Every fit-qualified symbology is one or the other. + ** A linear symbol with no rule for the height of its bars. ***************************************************************************/ - @Test - void theDefaultsAreTheMinimumsOfTheDesign() + private static ModuleFit.Request linear(int columns, int along, int across, int minimum, int maximum) { - assertEquals(250, ModuleFit.LINEAR_MINIMUM_MODULE.micrometres()); - assertEquals(375, ModuleFit.MATRIX_MINIMUM_MODULE.micrometres()); - assertEquals(List.of(2, 3, 6), Scene2.RESOLUTIONS.stream().map(dpi -> ModuleFit.recommended("gs1-128", dpi)).toList()); - assertEquals(List.of(3, 5, 9), Scene2.RESOLUTIONS.stream().map(dpi -> ModuleFit.recommended("qr", dpi)).toList()); - List matrix = BarcodeFit.TYPES.stream().filter(ModuleFit::matrix).toList(); - assertEquals(List.of("qr", "datamatrix", "pdf417", "aztec"), matrix); - for(String symbology : BarcodeFit.TYPES) - { - assertEquals(matrix.contains(symbology) ? 4 : 10, ModuleFit.quiet(symbology), symbology); - assertEquals(matrix.contains(symbology) ? 3 : 2, ModuleFit.recommended(symbology, 203), symbology); - } - assertEquals(64, ModuleFit.MAXIMUM_QUIET_MODULES); + return (new ModuleFit.Request(columns, 0, along, across, minimum, maximum, new ModuleFit.Height(columns, 0, 0, 1))); } @@ -78,12 +64,13 @@ class ModuleFitTest @Test void aLinearSymbolTakesTheLargestModuleThatFitsItsWidth() { - assertEquals(new ModuleFit.Fit(2, 638, 87, true), ModuleFit.fit(new ModuleFit.Request(319, 0, 719, 87, 2, Integer.MAX_VALUE))); - assertEquals(new ModuleFit.Fit(2, 638, 87, true), ModuleFit.fit(new ModuleFit.Request(319, 0, 638, 87, 2, Integer.MAX_VALUE))); - assertEquals(new ModuleFit.Fit(1, 638, 87, false), ModuleFit.fit(new ModuleFit.Request(319, 0, 637, 87, 2, Integer.MAX_VALUE))); - assertEquals(new ModuleFit.Fit(0, 638, 87, false), ModuleFit.fit(new ModuleFit.Request(319, 0, 318, 87, 2, Integer.MAX_VALUE))); - assertEquals(new ModuleFit.Fit(1, 319, 87, true), ModuleFit.fit(new ModuleFit.Request(319, 0, 637, 87, 1, Integer.MAX_VALUE))); - assertEquals(new ModuleFit.Fit(7, 2233, 1, true), ModuleFit.fit(new ModuleFit.Request(319, 0, 2400, 1, 6, Integer.MAX_VALUE))); + 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))); } @@ -95,11 +82,46 @@ class ModuleFitTest @Test void aMatrixSymbolIsHeldOnBothAxesAndAMaximumCaps() { - assertEquals(new ModuleFit.Fit(5, 145, 145, true), ModuleFit.fit(new ModuleFit.Request(29, 29, 319, 147, 3, Integer.MAX_VALUE))); - assertEquals(new ModuleFit.Fit(11, 319, 319, true), ModuleFit.fit(new ModuleFit.Request(29, 29, 319, 400, 3, Integer.MAX_VALUE))); - assertEquals(new ModuleFit.Fit(4, 116, 116, true), ModuleFit.fit(new ModuleFit.Request(29, 29, 319, 400, 3, 4))); - assertEquals(new ModuleFit.Fit(2, 87, 87, false), ModuleFit.fit(new ModuleFit.Request(29, 29, 319, 86, 3, Integer.MAX_VALUE))); - assertEquals(new ModuleFit.Fit(3, 957, 87, true), ModuleFit.fit(new ModuleFit.Request(319, 0, 2400, 87, 2, 3))); + 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))); } @@ -107,9 +129,10 @@ class ModuleFitTest /*************************************************************************** ** 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 and one dot more would not fit; when it does not, - ** the dots it needs at the minimum are more than the box has, on the axis - ** that refused it. + ** 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() @@ -117,16 +140,19 @@ class ModuleFitTest 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 columns = random.nextInt(21, 600); + 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) : Integer.MAX_VALUE; - ModuleFit.Request request = new ModuleFit.Request(columns, rows, along, across, minimum, maximum); + 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()) @@ -136,30 +162,33 @@ class ModuleFitTest 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(fit.module() == maximum || columns * (fit.module() + 1) > along || (matrix && rows * (fit.module() + 1) > 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); - assertTrue(fit.width() > along || (matrix && fit.height() > across), 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, fitted + " fitted, " + refused + " refused"); + 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 default minimum: - ** 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. + ** 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() @@ -171,7 +200,7 @@ class ModuleFitTest { boolean matrix = random.nextBoolean(); int columns = random.nextInt(21, 500); - Length minimum = matrix ? ModuleFit.MATRIX_MINIMUM_MODULE : ModuleFit.LINEAR_MINIMUM_MODULE; + 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; @@ -180,7 +209,7 @@ class ModuleFitTest for(int dpi : Scene2.RESOLUTIONS) { int along = box.size(dpi, 0); - ModuleFit.Fit fit = ModuleFit.fit(new ModuleFit.Request(columns, matrix ? columns : 0, along, along, minimum.module(dpi), Integer.MAX_VALUE)); + 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); diff --git a/renderer/src/test/java/com/kingsrook/qbits/renderer/scene2/barcode/PayloadTest.java b/renderer/src/test/java/com/kingsrook/qbits/renderer/scene2/barcode/PayloadTest.java new file mode 100644 index 0000000..87fc5ee --- /dev/null +++ b/renderer/src/test/java/com/kingsrook/qbits/renderer/scene2/barcode/PayloadTest.java @@ -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 `\` to `\` 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>", "", 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 numbers = Map.of(Symbology.EAN13, "0012345678905", Symbology.EAN8, "12345670", Symbology.UPCA, "012345678905", Symbology.UPCE, "04252614"); + for(Map.Entry 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> 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> 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()); + } +} diff --git a/renderer/src/test/java/com/kingsrook/qbits/renderer/scene2/barcode/SymbologyTest.java b/renderer/src/test/java/com/kingsrook/qbits/renderer/scene2/barcode/SymbologyTest.java new file mode 100644 index 0000000..b75d008 --- /dev/null +++ b/renderer/src/test/java/com/kingsrook/qbits/renderer/scene2/barcode/SymbologyTest.java @@ -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 LINEAR = List.of("code128", "code39", "code93", "itf", "gs1-128", "ean13", "ean8", "upca", "upce"); + private static final List 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 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 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> 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 design = List.of(2, 3, 6); + List retail = List.of(3, 4, 7); + List 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)))); + } +} diff --git a/renderer/src/test/resources/scene2/cases/barcode-gs1.json b/renderer/src/test/resources/scene2/cases/barcode-gs1.json index 613de30..fb2ff5e 100644 --- a/renderer/src/test/resources/scene2/cases/barcode-gs1.json +++ b/renderer/src/test/resources/scene2/cases/barcode-gs1.json @@ -4,7 +4,7 @@ "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", "align": "center", "valign": "middle"}, + {"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"} ] } diff --git a/renderer/src/test/resources/scene2/cases/barcode-linear.json b/renderer/src/test/resources/scene2/cases/barcode-linear.json index fdd817c..94a4a65 100644 --- a/renderer/src/test/resources/scene2/cases/barcode-linear.json +++ b/renderer/src/test/resources/scene2/cases/barcode-linear.json @@ -5,9 +5,9 @@ "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", "align": "center", "valign": "middle"}, + {"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", "align": "center", "valign": "middle", "rotation": 90} + {"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"}}} diff --git a/renderer/src/test/resources/scene2/cases/barcode-matrix.json b/renderer/src/test/resources/scene2/cases/barcode-matrix.json index 160929c..5fb2662 100644 --- a/renderer/src/test/resources/scene2/cases/barcode-matrix.json +++ b/renderer/src/test/resources/scene2/cases/barcode-matrix.json @@ -4,7 +4,7 @@ "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", "align": "center", "valign": "middle"}, + {"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} ] diff --git a/renderer/src/test/resources/scene2/cases/barcode-retail.json b/renderer/src/test/resources/scene2/cases/barcode-retail.json new file mode 100644 index 0000000..7ab6b1b --- /dev/null +++ b/renderer/src/test/resources/scene2/cases/barcode-retail.json @@ -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"} + ] + } +} diff --git a/renderer/src/test/resources/scene2/golden/barcode-retail-203.png b/renderer/src/test/resources/scene2/golden/barcode-retail-203.png new file mode 100644 index 0000000000000000000000000000000000000000..ec143563c86b2f6cdc20548766c45cec66e0f9a1 GIT binary patch literal 272 zcmeAS@N?(olHy`uVBq!ia0y~yVAcV$Pcs4uh9xmt(}C1=PZ!6KinzCDH|8=q3bb9^ z%AwcHwr#@c6(@NMWE~>6BxXu~NR?LD;M*cF{m&)Ai1*y-CZA8em93rr{G?W5lY!$5 z1s?%54&fHgD(lOBd*#~9zDFG^lQp}(QTF@vmv^2;|9mt1=bx>z^7`?i{7>H>bl;!T z9`$X%d+9EHgVTuy5lx7*Y}U_T0wI!wdor zfs&#(4@J2wxXK!Lf2r$`8;vw z+s9Vt5AW#zZFBzgck{@2TxR08ae!p6>EjCqn4{`vZsoK8__5}%eB^tKu!1RqhU7<1 Xv7+Ns&%eK61d1Y0S3j3^P6d&OO@VnpC;M@E68-C7x{FrHZr)b9@fpdCH zm+d}mW8Z6d|K1t?$3?L-->=_3)$rKW%SU%@s;iPaE`R&f-G>je?(TmxOa6Y@+0*sM z&Tjj=qv(&xv4=Zeo9Bw_2)91VdH z5(57^A{qxJJZRrrYF~F@%Y3bCm6z*QMLhTYy)tR8`P=35dXA