Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f5acf93e3d
|
||
|
|
d3378825d7
|
No files matched your search
@@ -16,3 +16,6 @@
|
||||
# Stored renderer outputs are compared byte for byte: git must never rewrite them.
|
||||
renderer/src/test/resources/compat-v1/** -text
|
||||
renderer/src/test/resources/scene2/golden/** -text
|
||||
|
||||
# Bundled fonts and their licence texts are shipped exactly as released and are hashed: git must never rewrite them.
|
||||
renderer/src/main/resources/fonts/** -text
|
||||
@@ -346,6 +346,64 @@ Legibility on thermal heads:
|
||||
- Guidance for the authoring guide: at 203 dpi use a bold face below 10 pt, keep text at or above 6 pt, prefer the
|
||||
condensed bold over shrinking.
|
||||
|
||||
**Decided while building text (P2).** The code is `scene2/text`; the renderer README states the rules for authors.
|
||||
|
||||
- **The set as shipped.** `qqq-sans` and `qqq-sans-bold` are IBM Plex Sans Regular and Bold (release
|
||||
`@ibm/plex-sans@1.1.0`), `qqq-condensed-bold` is IBM Plex Sans Condensed Bold (`@ibm/plex-sans-condensed@2.0.0`),
|
||||
`qqq-mono` and `qqq-mono-bold` are JetBrains Mono Regular and Bold (`v2.304`). Every licence was confirmed as SIL
|
||||
OFL 1.1 from the release archive's own licence file. A scene names a face with `font`; the name carries family
|
||||
and weight, and there is no separate weight or style property (the set has no italic). Only bundled faces are
|
||||
drawn: a supplied `font/ttf` asset named by a version 2 text is `FONT_UNKNOWN` until the asset package (P8).
|
||||
- **One glyph for each character, left to right.** No kerning, ligatures, mark placement or bidirectional
|
||||
reordering, which is what version 1 raster text does too and what an identifier wants (JetBrains Mono would
|
||||
otherwise join `->` and `!=`). A character that cannot be drawn that way (a control or format character, a
|
||||
combining mark, a script that needs shaping, anything outside the Basic Multilingual Plane, or a character the
|
||||
face lacks) is `FONT_MISSING_GLYPH` with the count; the value prints whole or not at all.
|
||||
- **Measurement is integer arithmetic.** The faces have 1000 units to the em. Advances and outline boxes are
|
||||
read once at a reference size and scaled as `units x millipoints x dpi / 72 000 000`, so the same value is the
|
||||
same width on every machine. Each character sits at a whole-dot pen position. A line's box is its advance,
|
||||
widened to the ink of its characters plus two dots on each inked side: the rasteriser fits an outline to the
|
||||
dot grid within 1.4 dots of its exact scale (measured over every character of the five faces), and a render
|
||||
whose ink would leave the measured box fails rather than clip or print outside it.
|
||||
- **Wrapping.** At each size a line is broken between words while the text then fits, and at any character when
|
||||
it does not. So a larger size is preferred to whole words, a value with no space wraps (the example's GS1 line
|
||||
has none), and a length is provable: n characters need no more lines than n of the widest.
|
||||
- **Truncation.** At the floor size, as many lines as the box holds; the last ends with the ellipsis after as
|
||||
many of its characters as fit with it, down to the ellipsis alone. A cut text is also reported as shrunk.
|
||||
- **The proof of `maxLength`** lays out that many copies of a worst character: the face's greatest advance,
|
||||
reaching as far left, right, above and below as the furthest-reaching character of the face (in IBM Plex Sans
|
||||
Bold the per mille sign is 1.382 em wide, the fraction slash reaches 0.245 em past its advance on each side, and
|
||||
a capital A with ring and acute rises to 1.15 em against an ascent of 1.025). It is therefore sound for any
|
||||
value, and stricter than a typical value needs. The example scene of section 2.1 does not pass it: two lines at
|
||||
the 7 pt floor need 59 dots at 203 dpi (53 from rounding the ascent, the line pitch and the descent up to whole
|
||||
dots, 6 for the accent and the descender) and the 6.5 mm caption box has 51. A box of exactly `lines x line
|
||||
height` is not proven; 7.5 mm is. The proof is for the render target of the request, so a host that prints
|
||||
one revision at several resolutions validates at each (P5 or P6b).
|
||||
- **Validation reads no record.** A bound value is laid out only when rendering; `FIT_NOT_PROVEN` is raised for
|
||||
a bound element without `maxLength` in both operations. Legibility is judged at the stated size when a bound
|
||||
element is validated and at the size chosen when it is rendered.
|
||||
- **Bounds.** A size from 1 pt to 1000 pt, `shrinkTo` from 1 pt up to the size, `lineHeight` from 0.5 to 5 with
|
||||
three decimals, 64 lines, 1024 characters. Anything inside `fit` that is wrong is reported at `fit`, since a
|
||||
pointer is made only of declared property names of an element.
|
||||
- **The members of `fit` are not schema properties.** The published schema states `maxLines`, `shrinkTo` and
|
||||
`overflow` in the description of `fit` and does not declare them under `properties`. The host admits in a
|
||||
pointer exactly the names the schema declares (`SceneFacts.VOCABULARY`, held to the schema by a test in the
|
||||
bridge), and the renderer admits the same set (`ElementFactory.VOCABULARY`); declaring three names that no
|
||||
pointer carries would have widened the host's list for nothing. If a pointer should one day say
|
||||
`/elements/1/fit/maxLines`, the three names are added to the schema and to both lists in one change.
|
||||
- **Rotation and reverse.** A text turned a quarter is fitted along the box's height, and `align` and `valign`
|
||||
follow the direction of reading. Reverse text fills its own box black and clears the letters; it counts twice
|
||||
against the painted-area cap.
|
||||
- **`RESERVED_FONT_NAME`** is raised by a text element (with the count of supplied assets under `qqq-`), not for
|
||||
the scene as a whole: a scene with no text has no face to shadow.
|
||||
- **Determinism (section 10.2).** No tolerance was needed. The goldens are generated in the CI image (Linux,
|
||||
Temurin 21.0.9) and are the same bytes on Windows (Temurin 21.0.12), at 203, 300 and 600 dpi, for the glyph
|
||||
bitmaps (up to 100 dots to the em) and for the outline path above that; they are compared exactly on both.
|
||||
One case depends on more than the scene: a line whose strip is over 8 million dots (type of several hundred
|
||||
points) is drawn in bands of rows, and the rasteriser may shade a few edge dots at a band's border differently
|
||||
than in one piece (fewer than 1 in 10 000 ink dots). Where the bands fall is a function of the line's box, so
|
||||
the result is still the same on every machine.
|
||||
|
||||
## 4. Barcodes
|
||||
|
||||
### 4.1 Symbologies
|
||||
|
||||
@@ -28,16 +28,25 @@ 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` and `group` elements with only their declared properties
|
||||
(the other types are refused until their packages exist), groups at most four deep, every length a number
|
||||
and a unit, and overrides only for the three resolutions and for declared ids. About one version 2 input in
|
||||
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
|
||||
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
|
||||
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). 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 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. The
|
||||
(`CANVAS_BOUNDS`, `CANVAS_LIMIT`, `LENGTH_NOT_ABSOLUTE`, `LENGTH_NOT_PHYSICAL`), which no scene may reach; so is
|
||||
the text package's `IllegalStateException` for ink outside a measured line (`TEXT_INK_OUTSIDE_MEASURE`), and
|
||||
any arithmetic or number-format failure. The
|
||||
one exception is a source the routing sends to the frozen version 1 parser, whose own refusal is a plain
|
||||
`IllegalArgumentException`; such sources are validated only. The campaign also asserts that it rendered labels.
|
||||
- **Printer gate.** An accepted input contains only the commands the renderer may emit and no tilde command.
|
||||
|
||||
@@ -26,3 +26,12 @@ The encoder and prime-field ECC helper retain the MIT license. See
|
||||
META-INF/licenses/bwipp-code93i-MIT.txt and Code93i.java. Pinned corresponding
|
||||
upstream source, helper, tests and provenance are retained under
|
||||
src/test/resources/barcodes/code93i.
|
||||
|
||||
Bundled fonts (2026-11-12). This distribution contains, unmodified and as data,
|
||||
IBM Plex Sans Regular and Bold and IBM Plex Sans Condensed Bold, Copyright (c)
|
||||
2017 IBM Corp. with Reserved Font Name "Plex", and JetBrains Mono Regular and
|
||||
Bold, Copyright 2020 The JetBrains Mono Project Authors
|
||||
(https://github.com/JetBrains/JetBrainsMono). They are licensed under the SIL
|
||||
Open Font License, Version 1.1, separately from the renderer's Apache license,
|
||||
which does not replace it. Each font is in /fonts of the renderer JAR beside
|
||||
the full license text and a provenance note; /fonts/SHA256SUMS lists the hashes.
|
||||
@@ -124,7 +124,12 @@ A source with schema `qqq-zpl-scene/2` is a layout in physical units that render
|
||||
- **Coordinates** start at the top left of the printable area, the stock less its margins. In dots the printable area is `floor((width - left - right) x dpi)` wide and `floor((height - top - bottom) x dpi)` high, which is how a box size is rounded, and it starts at `ceil(left x dpi)`, `ceil(top x dpi)`, moved back by one dot where it would otherwise pass the page edge. So a box of exactly the printable width and height fits at 203, 300 and 600 dpi alike. No ink is ever a whole dot inside a margin; the right and bottom edges may pass theirs by a fraction of a dot (less than 0.125 mm at 203 dpi). `design` states the stock the layout was drawn for; the request's stock must match it within 0.5 mm on each side (`STOCK_MISMATCH`).
|
||||
- **Every element** has a unique `id` (`[A-Za-z][A-Za-z0-9_-]{0,31}`), a `type`, an `anchor` (one of nine points, default `top-left`: the same point of the box is placed on that point of its container), `x` and `y` (distances inward from the anchored edge; on a centred axis a shift that may be negative), `width` and `height` (omitted: the rest of the container), and `align` and `valign` (where content sits in the box).
|
||||
- **`box`** has `thickness` and optional `fill`; a line is a box whose width or height is its thickness. **`group`** has `children`, placed relative to its box; groups nest four deep. A scene holds at most 1000 elements, and its elements may paint at most sixteen times its printable area in all, an overlap counted each time: a filled box counts its area, an outline its four bars, a group nothing. The element that would pass that is refused as `SCENE_INVALID` at its pointer, with its box and `measured.paintedPercent` against `limit.paintedPercent` of 1600. A real label paints less than twice its area; the limit holds the worst scene to sixteen fills of the canvas where 1000 were possible.
|
||||
- **`text`, `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.
|
||||
- **`text`** draws one value in a face bundled with the renderer: `qqq-sans`, `qqq-sans-bold` (IBM Plex Sans), `qqq-condensed-bold` (IBM Plex Sans Condensed) and `qqq-mono`, `qqq-mono-bold` (JetBrains Mono). There is no printer font in version 2 and no installed font is used; any other `font` is `FONT_UNKNOWN`, and a request that supplies an asset whose name begins `qqq-` is refused as `RESERVED_FONT_NAME`. `font` and `size` may come from the scene's `defaults`. The value is `value` or, read from the record when rendering, `valuePath`; at most 1024 characters, each of which the face must draw as one glyph from left to right (no control or format characters, combining marks, shaping or right-to-left scripts: `FONT_MISSING_GLYPH`, with the count).
|
||||
- **Size and measurement.** `size` is the em as a physical length from `1pt` to `1000pt`, not rounded to dots. A text is measured before it is drawn, by integer arithmetic on the face's own numbers, so the measurement is the same on every machine. A line's box is its pen advance widened to hold the ink of every character in it, plus two dots on each inked side for the rasteriser. The block of lines is placed in the element's box by `align` and `valign`, and each line in the block by `align`. `lineHeight` is the distance between baselines as a multiple of the size (default: the face's own). `rotation` turns the text clockwise in a box given as it appears on the label; `reverse` prints white letters on the box filled black.
|
||||
- **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`).
|
||||
- **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, 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`. 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`. 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). 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.
|
||||
- **`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.
|
||||
|
||||
@@ -58,3 +58,32 @@ the Java file preserves the full license and identifies the dated port.
|
||||
Unmodified source, tests, hashes and maintenance policy are retained in
|
||||
`src/test/resources/barcodes/coop2of5`. No interpreter or additional runtime
|
||||
dependency is introduced. See [the capability contract](BARCODES.md#coop-2-of-5).
|
||||
|
||||
## Bundled fonts for version 2 label text (2026-11-12)
|
||||
|
||||
Version 2 label scenes draw text in five TrueType faces shipped inside the renderer JAR under `/fonts`. They are
|
||||
data, not code: unmodified files taken from each project's official release archive, each beside its licence text
|
||||
and a provenance note, with their SHA-256 in `/fonts/SHA256SUMS`. A test (`FaceTest`) fails the build when a
|
||||
licence, a provenance note or a hash is missing or a file's bytes change.
|
||||
|
||||
| Scene name | Family and style | File | Release | SHA-256 | Licence |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| `qqq-sans` | IBM Plex Sans Regular | `ibm-plex-sans/IBMPlexSans-Regular.ttf` | [`@ibm/plex-sans@1.1.0`](https://github.com/IBM/plex/releases/download/%40ibm/plex-sans%401.1.0/ibm-plex-sans.zip), font version 3.005 | `975dcda37d80f038dcd143c22e33ca2d97a0cc5a929aace1c749153b0fe1afa5` | SIL OFL 1.1 |
|
||||
| `qqq-sans-bold` | IBM Plex Sans Bold | `ibm-plex-sans/IBMPlexSans-Bold.ttf` | the same archive, font version 3.005 | `9e6c74a889a700d707613d24548fe4ffa6bc59559a0689d2cf9e133bdcdafb2f` | SIL OFL 1.1 |
|
||||
| `qqq-condensed-bold` | IBM Plex Sans Condensed Bold | `ibm-plex-sans-condensed/IBMPlexSansCondensed-Bold.ttf` | [`@ibm/plex-sans-condensed@2.0.0`](https://github.com/IBM/plex/releases/download/%40ibm/plex-sans-condensed%402.0.0/ibm-plex-sans-condensed.zip), font version 3.000 | `723e245902b45a49f8cf602d15891bae08c31309dce7c56d209d10cd471b88c7` | SIL OFL 1.1 |
|
||||
| `qqq-mono` | JetBrains Mono Regular | `jetbrains-mono/JetBrainsMono-Regular.ttf` | [`v2.304`](https://github.com/JetBrains/JetBrainsMono/releases/download/v2.304/JetBrainsMono-2.304.zip), font version 2.304 | `a0bf60ef0f83c5ed4d7a75d45838548b1f6873372dfac88f71804491898d138f` | SIL OFL 1.1 |
|
||||
| `qqq-mono-bold` | JetBrains Mono Bold | `jetbrains-mono/JetBrainsMono-Bold.ttf` | the same archive, font version 2.304 | `5590990c82e097397517f275f430af4546e1c45cff408bde4255dad142479dcb` | SIL OFL 1.1 |
|
||||
|
||||
Each licence was confirmed from the release archive itself and not from memory: the archive's own licence file
|
||||
(`LICENSE.txt` for the two IBM Plex archives, `OFL.txt` for JetBrains Mono) states the SIL Open Font License,
|
||||
Version 1.1 and carries its full text, the licence field of every font's name table says the same, and every
|
||||
font's embedding bits are 0 (installable embedding). The licence files are shipped byte for byte. IBM Plex
|
||||
declares the Reserved Font Name "Plex"; the files are neither modified nor renamed, and the scene names
|
||||
(`qqq-sans` and so on) are names of the renderer's faces, not of derived fonts.
|
||||
|
||||
The SIL Open Font License 1.1 is not on the allowlist of `K22-DEP-02`. The maintainer approved one recorded
|
||||
exception for unmodified OFL 1.1 fonts shipped as data on 2026-11-12. The register is kept in the standards
|
||||
repository (`exceptions/register.md`), not here; the entry was proposed with the pull request that added the
|
||||
fonts. The OFL permits bundling and redistribution with software provided the copyright and
|
||||
licence notices accompany the fonts, the fonts are not sold by themselves and a Reserved Font Name is not used
|
||||
for a modified version; it does not apply to documents made with the fonts.
|
||||
@@ -15,7 +15,7 @@
|
||||
"properties": {"width": {"$ref": "#/$defs/physical"}, "height": {"$ref": "#/$defs/physical"}}
|
||||
},
|
||||
"defaults": {
|
||||
"description": "Defaults for text elements.",
|
||||
"description": "The face and size of a text element that states none.",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {"font": {"type": "string", "pattern": "^[A-Za-z0-9_-][A-Za-z0-9_.-]{0,99}$"}, "size": {"$ref": "#/$defs/physical"}}
|
||||
@@ -98,9 +98,25 @@
|
||||
},
|
||||
{
|
||||
"$ref": "#/$defs/placement",
|
||||
"description": "Text. Parsed and placed by renderer 0.2.0 and refused as RENDERER_TOO_OLD until the text package; the values of these properties are not yet read.",
|
||||
"description": "Text in a face bundled with the renderer, measured before it is drawn and placed in its box by `align` and `valign`. `font` and `size` may come from `defaults`. The renderer is the authority: it also enforces a size from 1pt to 1000pt, a `shrinkTo` from 1pt up to the size, at most three decimals in `lineHeight`, at most 1024 characters, only characters the face draws as one glyph each from left to right (FONT_MISSING_GLYPH), and no supplied asset whose name begins `qqq-` (RESERVED_FONT_NAME).",
|
||||
"unevaluatedProperties": false,
|
||||
"properties": {"type": {"const": "text"}, "value": {}, "valuePath": {}, "font": {}, "size": {}, "lineHeight": {}, "fit": {}, "maxLength": {}, "threshold": {}, "reverse": {}, "rotation": {"enum": [0, 90, 180, 270]}}
|
||||
"oneOf": [{"required": ["value"]}, {"required": ["valuePath"]}],
|
||||
"properties": {
|
||||
"type": {"const": "text"},
|
||||
"value": {"type": "string", "maxLength": 1024, "description": "The text itself."},
|
||||
"valuePath": {"type": "string", "pattern": "^/", "maxLength": 256, "description": "A JSON pointer into the record. Text is printed as it is, a number or true/false as JSON writes it; anything else is VALUE_MISSING. No record is read when a scene is validated."},
|
||||
"font": {"enum": ["qqq-sans", "qqq-sans-bold", "qqq-condensed-bold", "qqq-mono", "qqq-mono-bold"], "description": "A bundled face: IBM Plex Sans Regular and Bold, IBM Plex Sans Condensed Bold, JetBrains Mono Regular and Bold. Any other name is FONT_UNKNOWN."},
|
||||
"size": {"$ref": "#/$defs/physical", "description": "The em size. Type scales continuously: the size is not rounded to dots."},
|
||||
"lineHeight": {"type": "number", "minimum": 0.5, "maximum": 5, "description": "The distance between baselines as a multiple of the size. Default: the face's own ascent, descent and line gap."},
|
||||
"fit": {
|
||||
"type": "object",
|
||||
"description": "What happens when the value does not fit the box. In order: lay out at `size` within `maxLines`; else the largest size, in steps of 0.25pt down to `shrinkTo`, that fits; else `overflow`. At each size a line is broken between words while that fits and at any character when it does not. Its members, which the renderer enforces and refuses any other: `maxLines`, a whole number from 1 to 64, default 1; `shrinkTo`, a physical length from 1pt up to `size`, the smallest size the text may be reduced to, default 70% of `size` and not below 5pt (or `size` itself when that is smaller); `overflow`, `refuse` (the default, TEXT_DOES_NOT_FIT) or `truncate` (cut at a character, end with an ellipsis, warn TEXT_TRUNCATED). The members are described here and not declared as schema properties because a diagnostic points at `fit` itself: the names this schema declares are exactly the names a diagnostic's pointer may hold."
|
||||
},
|
||||
"maxLength": {"type": "integer", "minimum": 1, "maximum": 1024, "description": "The longest value, in characters. Validation proves that this many of the face's widest character fit, so a length refusal happens when the scene is published; a longer value is VALUE_TOO_LONG when printing. Without it a bound element warns FIT_NOT_PROVEN."},
|
||||
"threshold": {"type": "integer", "minimum": 1, "maximum": 254, "default": 128, "description": "Glyphs are drawn antialiased; a dot is ink when its grey is darker than this. Higher prints heavier."},
|
||||
"reverse": {"type": "boolean", "default": false, "description": "White letters on the element's box filled black."},
|
||||
"rotation": {"enum": [0, 90, 180, 270], "description": "Clockwise. The box is given as it appears on the label; a text turned a quarter is fitted along the box's height."}
|
||||
}
|
||||
},
|
||||
{
|
||||
"$ref": "#/$defs/placement",
|
||||
|
||||
@@ -209,7 +209,18 @@ public final class ElementSource
|
||||
***************************************************************************/
|
||||
public Refusal invalid(String name)
|
||||
{
|
||||
return (diagnostics.refuse("SCENE_INVALID", id, pointer(name), null, null, null));
|
||||
return (refuse("SCENE_INVALID", name));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** The refusal, by its own code, for a property that is well formed but names
|
||||
** something this renderer does not have, such as a font.
|
||||
***************************************************************************/
|
||||
public Refusal refuse(String code, String name)
|
||||
{
|
||||
return (diagnostics.refuse(code, id, pointer(name), null, null, null));
|
||||
}
|
||||
|
||||
|
||||
@@ -230,8 +241,26 @@ public final class ElementSource
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Where a property is stated: in the override that set it, else in the
|
||||
** element.
|
||||
** Give the element the scene's `defaults` for the properties its type reads
|
||||
** and it does not state. A default is reported where the scene states it.
|
||||
***************************************************************************/
|
||||
void inherit(JsonNode defaults, Set<String> properties)
|
||||
{
|
||||
for(Map.Entry<String, JsonNode> property : defaults.properties())
|
||||
{
|
||||
if(properties.contains(property.getKey()) && !values.containsKey(property.getKey()))
|
||||
{
|
||||
values.put(property.getKey(), property.getValue());
|
||||
pointers.put(property.getKey(), "/defaults/" + property.getKey());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Where a property is stated: in the override that set it, in the scene's
|
||||
** defaults, else in the element.
|
||||
***************************************************************************/
|
||||
private String pointer(String name)
|
||||
{
|
||||
|
||||
@@ -278,6 +278,7 @@ final class SceneParser
|
||||
{
|
||||
ElementFactory factory = ElementFactory.FACTORIES.get(source.type());
|
||||
source.only(factory.properties());
|
||||
source.inherit(root.path("defaults"), factory.properties());
|
||||
built.add(factory.create(source));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,345 @@
|
||||
/*
|
||||
* 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.text;
|
||||
|
||||
|
||||
import java.awt.Font;
|
||||
import java.awt.FontFormatException;
|
||||
import java.awt.font.FontRenderContext;
|
||||
import java.awt.font.GlyphVector;
|
||||
import java.awt.font.LineMetrics;
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The faces bundled with the renderer, by the name a scene writes. Each is an
|
||||
** unmodified TrueType file under the SIL Open Font License 1.1, shipped in the
|
||||
** jar beside its licence, provenance and hash (`/fonts`). No installed font is
|
||||
** ever used and there is no fallback between faces.
|
||||
**
|
||||
** Every number here is in thousandths of the em, read once from the face at a
|
||||
** reference size of 1000, where the bundled faces (1000 units to the em) give
|
||||
** whole numbers. Layout multiplies those integers by the size and never asks
|
||||
** the font for a measurement at the size it draws, so a measurement is the
|
||||
** same on every machine.
|
||||
**
|
||||
** The four numbers after the widest character are the most any character of
|
||||
** the face reaches past its own advance on the left and on the right, above
|
||||
** the baseline and below it. They bound every character, so a value that was
|
||||
** never seen can be proven to fit; a test recomputes them from the font file.
|
||||
*******************************************************************************/
|
||||
public final class Face
|
||||
{
|
||||
public static final Face SANS = new Face("qqq-sans", "ibm-plex-sans/IBMPlexSans-Regular.ttf", false, 0x2030, 203, 203, 1119, 245);
|
||||
public static final Face SANS_BOLD = new Face("qqq-sans-bold", "ibm-plex-sans/IBMPlexSans-Bold.ttf", true, 0x2030, 245, 245, 1150, 275);
|
||||
public static final Face CONDENSED_BOLD = new Face("qqq-condensed-bold", "ibm-plex-sans-condensed/IBMPlexSansCondensed-Bold.ttf", true, 0x2030, 241, 241, 1150, 275);
|
||||
public static final Face MONO = new Face("qqq-mono", "jetbrains-mono/JetBrainsMono-Regular.ttf", false, 0x21, 165, 165, 1120, 400);
|
||||
public static final Face MONO_BOLD = new Face("qqq-mono-bold", "jetbrains-mono/JetBrainsMono-Bold.ttf", true, 0x21, 165, 165, 1120, 400);
|
||||
|
||||
static final FontRenderContext CONTEXT = new FontRenderContext(null, true, true);
|
||||
static final int REFERENCE = 1000;
|
||||
|
||||
private static final List<Face> ALL = List.of(SANS, SANS_BOLD, CONDENSED_BOLD, MONO, MONO_BOLD);
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// characters that cannot be drawn one glyph each from left to right: controls and formats //
|
||||
// (which would print nothing), marks (which need placing on the letter before) and the like //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
private static final Set<Integer> UNDRAWN = Set.of((int) Character.CONTROL, (int) Character.FORMAT, (int) Character.SURROGATE, (int) Character.PRIVATE_USE, (int) Character.UNASSIGNED,
|
||||
(int) Character.LINE_SEPARATOR, (int) Character.PARAGRAPH_SEPARATOR, (int) Character.NON_SPACING_MARK, (int) Character.ENCLOSING_MARK, (int) Character.COMBINING_SPACING_MARK);
|
||||
|
||||
private final String scene;
|
||||
private final String file;
|
||||
private final boolean bold;
|
||||
private final int widest;
|
||||
private final int left;
|
||||
private final int right;
|
||||
private final int top;
|
||||
private final int bottom;
|
||||
private final Map<Integer, Glyph> glyphs = new ConcurrentHashMap<>();
|
||||
private Font font;
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** One character of a face: how far the pen moves, and the box its outline
|
||||
** fills, from the pen (`left`, `right`) and from the baseline (`top` above,
|
||||
** `bottom` below), rounded outward. A space has no ink; only U+0020 is a
|
||||
** `space`, the one place a line may be broken between words.
|
||||
***************************************************************************/
|
||||
public record Glyph(int advance, int left, int right, int top, int bottom, boolean ink, boolean space)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** A face, where its file is, and the bounds a test holds it to.
|
||||
***************************************************************************/
|
||||
private Face(String scene, String file, boolean bold, int widest, int left, int right, int top, int bottom)
|
||||
{
|
||||
this.scene = scene;
|
||||
this.file = file;
|
||||
this.bold = bold;
|
||||
this.widest = widest;
|
||||
this.left = left;
|
||||
this.right = right;
|
||||
this.top = top;
|
||||
this.bottom = bottom;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** The bundled set: a proportional sans in regular and bold, a condensed bold
|
||||
** for long values on narrow labels, and a monospace in regular and bold.
|
||||
***************************************************************************/
|
||||
public static List<Face> all()
|
||||
{
|
||||
return (ALL);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** The face a scene names, or null.
|
||||
***************************************************************************/
|
||||
public static Face of(String scene)
|
||||
{
|
||||
for(Face face : ALL)
|
||||
{
|
||||
if(face.scene.equals(scene))
|
||||
{
|
||||
return (face);
|
||||
}
|
||||
}
|
||||
return (null);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** The name a scene writes.
|
||||
***************************************************************************/
|
||||
public String scene()
|
||||
{
|
||||
return (scene);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** The font file, under `/fonts` in the jar.
|
||||
***************************************************************************/
|
||||
public String file()
|
||||
{
|
||||
return (file);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Whether the strokes are bold. A regular face prints light when small.
|
||||
***************************************************************************/
|
||||
public boolean bold()
|
||||
{
|
||||
return (bold);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** The character with the greatest advance, the lowest when several share it.
|
||||
***************************************************************************/
|
||||
public int widest()
|
||||
{
|
||||
return (widest);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** A character no real one exceeds in any direction: the greatest advance and
|
||||
** the greatest reach on each side. A line of these is at least as wide and
|
||||
** as tall as a line of the same number of any characters of the face.
|
||||
***************************************************************************/
|
||||
public Glyph worst()
|
||||
{
|
||||
int advance = glyph(widest).advance();
|
||||
return (new Glyph(advance, -left, advance + right, top, bottom, true, false));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** The face at a size in dots, for drawing only.
|
||||
***************************************************************************/
|
||||
Font font(float size)
|
||||
{
|
||||
return (reference().deriveFont(size));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Height above the baseline that a line reserves.
|
||||
***************************************************************************/
|
||||
public int ascent()
|
||||
{
|
||||
return (Math.round(metrics().getAscent()));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Depth below the baseline that a line reserves.
|
||||
***************************************************************************/
|
||||
public int descent()
|
||||
{
|
||||
return (Math.round(metrics().getDescent()));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Extra space the face asks for between lines.
|
||||
***************************************************************************/
|
||||
public int leading()
|
||||
{
|
||||
return (Math.round(metrics().getLeading()));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** The height of a capital letter, which is what legibility is judged by.
|
||||
***************************************************************************/
|
||||
public int capHeight()
|
||||
{
|
||||
return (glyph('H').top());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Whether the face can draw a character as one glyph, left to right.
|
||||
***************************************************************************/
|
||||
public boolean draws(int codePoint)
|
||||
{
|
||||
return (codePoint <= Character.MAX_VALUE && !UNDRAWN.contains(Character.getType(codePoint)) && reference().canDisplay(codePoint)
|
||||
&& !Font.textRequiresLayout(new char[] {(char) codePoint}, 0, 1));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** How many characters of a text the face cannot draw.
|
||||
***************************************************************************/
|
||||
public int missing(String text)
|
||||
{
|
||||
return (Math.toIntExact(text.codePoints().filter(codePoint -> !draws(codePoint)).count()));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** The measurements of one character the face draws.
|
||||
***************************************************************************/
|
||||
public Glyph glyph(int codePoint)
|
||||
{
|
||||
return (glyphs.computeIfAbsent(codePoint, this::measure));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** The measurements of a text the face draws, one for each character.
|
||||
***************************************************************************/
|
||||
public Glyph[] glyphs(String text)
|
||||
{
|
||||
return (text.codePoints().mapToObj(this::glyph).toArray(Glyph[]::new));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Read one character from the face at the reference size.
|
||||
***************************************************************************/
|
||||
private Glyph measure(int codePoint)
|
||||
{
|
||||
GlyphVector vector = reference().createGlyphVector(CONTEXT, new char[] {(char) codePoint});
|
||||
int advance = Math.round(vector.getGlyphMetrics(0).getAdvanceX());
|
||||
Rectangle2D bounds = vector.getGlyphOutline(0).getBounds2D();
|
||||
if(bounds.isEmpty())
|
||||
{
|
||||
return (new Glyph(advance, 0, 0, 0, 0, false, codePoint == ' '));
|
||||
}
|
||||
return (new Glyph(advance, (int) Math.floor(bounds.getMinX()), (int) Math.ceil(bounds.getMaxX()), (int) Math.ceil(-bounds.getMinY()), (int) Math.ceil(bounds.getMaxY()), true, false));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** The line metrics of the face at the reference size.
|
||||
***************************************************************************/
|
||||
private LineMetrics metrics()
|
||||
{
|
||||
return (reference().getLineMetrics("H", CONTEXT));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** The face at the reference size, read from the jar the first time.
|
||||
***************************************************************************/
|
||||
private synchronized Font reference()
|
||||
{
|
||||
if(font == null)
|
||||
{
|
||||
font = load("/fonts/" + file);
|
||||
}
|
||||
return (font);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Read a TrueType file from the jar. A file that is not a font is a broken
|
||||
** build, not a mistake in a scene, so it is an illegal state.
|
||||
***************************************************************************/
|
||||
static Font load(String resource)
|
||||
{
|
||||
try(InputStream stream = Face.class.getResourceAsStream(resource))
|
||||
{
|
||||
return (Font.createFont(Font.TRUETYPE_FONT, stream).deriveFont((float) REFERENCE));
|
||||
}
|
||||
catch(IOException | FontFormatException broken)
|
||||
{
|
||||
throw new IllegalStateException("BUNDLED_FONT_UNREADABLE", broken);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,409 @@
|
||||
/*
|
||||
* 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.text;
|
||||
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
|
||||
import com.kingsrook.qbits.renderer.RenderInput;
|
||||
import com.kingsrook.qbits.renderer.scene2.Canvas;
|
||||
import com.kingsrook.qbits.renderer.scene2.Diagnostics;
|
||||
import com.kingsrook.qbits.renderer.scene2.ElementSource;
|
||||
import com.kingsrook.qbits.renderer.scene2.Frame;
|
||||
import com.kingsrook.qbits.renderer.scene2.Layout;
|
||||
import com.kingsrook.qbits.renderer.scene2.Length;
|
||||
import com.kingsrook.qbits.renderer.scene2.Placement;
|
||||
import com.kingsrook.qbits.renderer.scene2.SceneElement;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A `text` element: one value in one bundled face, measured before it is
|
||||
** drawn, placed in its box by `align` and `valign`, and fitted by the rule the
|
||||
** element states (`TextFit`). The value is a literal (`value`) or is read from
|
||||
** the record by a JSON pointer (`valuePath`).
|
||||
**
|
||||
** Every refusal names the element and carries numbers only: a width, a line
|
||||
** count, a size, a count of characters. The value itself never enters a
|
||||
** diagnostic or an exception.
|
||||
**
|
||||
** A bound value is not known when a scene is validated, so validation reads no
|
||||
** record. With `maxLength` it proves instead that the longest value fits: that
|
||||
** many of the face's widest character, each taken to reach as far in every
|
||||
** direction as the furthest-reaching character of the face. Without
|
||||
** `maxLength` it warns `FIT_NOT_PROVEN`. When rendering, a value longer than
|
||||
** `maxLength` is refused before any layout.
|
||||
*******************************************************************************/
|
||||
final class TextElement implements SceneElement
|
||||
{
|
||||
static final int MAXIMUM_CHARACTERS = 1024;
|
||||
static final int MAXIMUM_LINES = 64;
|
||||
static final int MINIMUM_MILLIPOINTS = 1000;
|
||||
static final int MAXIMUM_MILLIPOINTS = 1_000_000;
|
||||
static final int DEFAULT_FLOOR_PERCENT = 70;
|
||||
static final int DEFAULT_FLOOR_MILLIPOINTS = 5000;
|
||||
static final int DEFAULT_THRESHOLD = 128;
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// capital heights in dots below which thermal print is hard to read: any face under 12 (about //
|
||||
// 6 pt at 203 dpi), a regular weight under 20 (about 10 pt at 203 dpi). Starting values, to //
|
||||
// be confirmed on the printed specimen //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
static final int SMALL_CAPITAL_DOTS = 12;
|
||||
static final int LIGHT_CAPITAL_DOTS = 20;
|
||||
|
||||
private static final Set<String> FIT = Set.of("maxLines", "shrinkTo", "overflow");
|
||||
|
||||
private final String id;
|
||||
private final Placement placement;
|
||||
private final int rotation;
|
||||
private final boolean reverse;
|
||||
private final Face face;
|
||||
private final int size;
|
||||
private final int floor;
|
||||
private final Integer lineHeight;
|
||||
private final int maxLines;
|
||||
private final boolean truncate;
|
||||
private final Integer maxLength;
|
||||
private final int threshold;
|
||||
private final String value;
|
||||
private final String path;
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Read the element. A property that is not what the schema says is
|
||||
** `SCENE_INVALID` at that property; a face this renderer does not bundle is
|
||||
** `FONT_UNKNOWN`.
|
||||
***************************************************************************/
|
||||
TextElement(ElementSource source)
|
||||
{
|
||||
id = source.id();
|
||||
placement = source.placement();
|
||||
rotation = source.rotation();
|
||||
reverse = source.flag("reverse", false);
|
||||
JsonNode font = source.value("font");
|
||||
if(font == null || !font.isTextual())
|
||||
{
|
||||
throw source.invalid("font");
|
||||
}
|
||||
face = Face.of(font.textValue());
|
||||
if(face == null)
|
||||
{
|
||||
throw source.refuse("FONT_UNKNOWN", "font");
|
||||
}
|
||||
Length stated = source.length("size", null, ElementSource.PHYSICAL);
|
||||
size = stated == null ? 0 : milliPoints(stated);
|
||||
if(size < MINIMUM_MILLIPOINTS || size > MAXIMUM_MILLIPOINTS)
|
||||
{
|
||||
throw source.invalid("size");
|
||||
}
|
||||
lineHeight = lineHeight(source);
|
||||
threshold = integer(source, "threshold", source.value("threshold"), 1, 254, DEFAULT_THRESHOLD);
|
||||
maxLength = source.value("maxLength") == null ? null : Integer.valueOf(integer(source, "maxLength", source.value("maxLength"), 1, MAXIMUM_CHARACTERS, 0));
|
||||
JsonNode fit = source.value("fit") == null ? JsonNodeFactory.instance.objectNode() : source.value("fit");
|
||||
if(!fit.isObject() || fit.properties().stream().anyMatch(property -> !FIT.contains(property.getKey())))
|
||||
{
|
||||
throw source.invalid("fit");
|
||||
}
|
||||
maxLines = integer(source, "fit", fit.get("maxLines"), 1, MAXIMUM_LINES, 1);
|
||||
floor = floor(source, fit.get("shrinkTo"));
|
||||
String overflow = fit.has("overflow") ? fit.get("overflow").textValue() : "refuse";
|
||||
if(!"refuse".equals(overflow) && !"truncate".equals(overflow))
|
||||
{
|
||||
throw source.invalid("fit");
|
||||
}
|
||||
truncate = "truncate".equals(overflow);
|
||||
JsonNode literal = source.value("value");
|
||||
JsonNode pointer = source.value("valuePath");
|
||||
if((literal == null) == (pointer == null))
|
||||
{
|
||||
throw source.invalid("value");
|
||||
}
|
||||
if(literal != null && (!literal.isTextual() || literal.textValue().codePointCount(0, literal.textValue().length()) > MAXIMUM_CHARACTERS))
|
||||
{
|
||||
throw source.invalid("value");
|
||||
}
|
||||
if(pointer != null && (!pointer.isTextual() || pointer.textValue().length() > 256 || !pointer.textValue().startsWith("/")))
|
||||
{
|
||||
throw source.invalid("valuePath");
|
||||
}
|
||||
value = literal == null ? null : literal.textValue();
|
||||
path = pointer == null ? null : pointer.textValue();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** A type size in thousandths of a point. A point is a 72nd of an inch and an
|
||||
** inch is 25400 micrometres, so a micrometre is 360/127 of a millipoint.
|
||||
***************************************************************************/
|
||||
private static int milliPoints(Length length)
|
||||
{
|
||||
long thousandths = length.thousandths();
|
||||
return (Math.toIntExact(switch(length.unit())
|
||||
{
|
||||
case IN -> thousandths * 72;
|
||||
case MM -> Math.floorDiv(thousandths * 360 + 63, 127);
|
||||
default -> thousandths;
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** A whole number in a range, or the fallback when the scene states none.
|
||||
***************************************************************************/
|
||||
private static int integer(ElementSource source, String name, JsonNode stated, int minimum, int maximum, int fallback)
|
||||
{
|
||||
if(stated == null)
|
||||
{
|
||||
return (fallback);
|
||||
}
|
||||
if(!stated.isIntegralNumber() || !stated.canConvertToInt() || stated.intValue() < minimum || stated.intValue() > maximum)
|
||||
{
|
||||
throw source.invalid(name);
|
||||
}
|
||||
return (stated.intValue());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** The distance between baselines as a multiple of the size, in thousandths:
|
||||
** a number from 0.5 to 5 with at most three decimals. Null is the face's own.
|
||||
** A number too large to be a number at all (`1e400`) is refused like any
|
||||
** other, not left to fail as arithmetic.
|
||||
***************************************************************************/
|
||||
private static Integer lineHeight(ElementSource source)
|
||||
{
|
||||
JsonNode stated = source.value("lineHeight");
|
||||
if(stated == null)
|
||||
{
|
||||
return (null);
|
||||
}
|
||||
BigDecimal thousandths = stated.isNumber() && Double.isFinite(stated.doubleValue()) ? stated.decimalValue().movePointRight(3) : BigDecimal.ZERO;
|
||||
if(thousandths.stripTrailingZeros().scale() > 0 || thousandths.compareTo(BigDecimal.valueOf(500)) < 0 || thousandths.compareTo(BigDecimal.valueOf(5000)) > 0)
|
||||
{
|
||||
throw source.invalid("lineHeight");
|
||||
}
|
||||
return (thousandths.intValueExact());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** The smallest size the text may be reduced to. Stated, it is a physical
|
||||
** length from one point up to the size. Otherwise it is 70% of the size and
|
||||
** not below 5 pt, or the size itself when that is smaller still.
|
||||
***************************************************************************/
|
||||
private int floor(ElementSource source, JsonNode stated)
|
||||
{
|
||||
if(stated == null)
|
||||
{
|
||||
return (Math.min(size, Math.max(size * DEFAULT_FLOOR_PERCENT / 100, DEFAULT_FLOOR_MILLIPOINTS)));
|
||||
}
|
||||
try
|
||||
{
|
||||
Length length = Length.parse(stated.textValue());
|
||||
if(length.physical() && milliPoints(length) >= MINIMUM_MILLIPOINTS && milliPoints(length) <= size)
|
||||
{
|
||||
return (milliPoints(length));
|
||||
}
|
||||
}
|
||||
catch(IllegalArgumentException expected)
|
||||
{
|
||||
// Refused below, at the property; the cause holds nothing more.
|
||||
}
|
||||
throw source.invalid("fit");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** The id the scene declares for the text.
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public String id()
|
||||
{
|
||||
return (id);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** The element's box, by the standing rules.
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public Frame place(Layout layout)
|
||||
{
|
||||
return (layout.place(placement));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Everything that can refuse the text, 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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Reverse text paints its box and then clears its letters, so it is counted
|
||||
** twice against what one scene may paint.
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public long paintedDots(Frame frame, int dpi)
|
||||
{
|
||||
return ((reverse ? 2L : 1L) * frame.width() * frame.height());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Draw the fitted text. Reverse text is white letters on its own box filled
|
||||
** black.
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public void paint(Canvas canvas, Frame frame, RenderInput input, Diagnostics diagnostics)
|
||||
{
|
||||
TextRaster.Drawing drawing = compose(frame, input, diagnostics);
|
||||
if(reverse)
|
||||
{
|
||||
canvas.fill(frame, true);
|
||||
}
|
||||
TextRaster.paint(canvas, frame, drawing, TextRaster.BAND_DOTS);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Resolve the value, hold it to its length and to the face, and fit it. The
|
||||
** answer is what to draw, or null when a bound value is only being
|
||||
** validated.
|
||||
***************************************************************************/
|
||||
private TextRaster.Drawing compose(Frame frame, RenderInput input, Diagnostics diagnostics)
|
||||
{
|
||||
int reserved = input.reservedAssetNames().size();
|
||||
if(reserved > 0)
|
||||
{
|
||||
throw diagnostics.refuse("RESERVED_FONT_NAME", id, null, frame, Map.of("reservedAssets", reserved), Map.of("reservedAssets", 0));
|
||||
}
|
||||
int dpi = input.targetDpi == null ? input.stock.dpi() : input.targetDpi;
|
||||
boolean upright = rotation % 180 == 0;
|
||||
TextFit.Request request = new TextFit.Request(face, upright ? frame.width() : frame.height(), upright ? frame.height() : frame.width(), dpi, size, floor, lineHeight, maxLines, truncate);
|
||||
if(path != null && maxLength == null)
|
||||
{
|
||||
diagnostics.warn("FIT_NOT_PROVEN", id, null, frame, null, null);
|
||||
}
|
||||
String text = value;
|
||||
if(path != null && "VALIDATE".equals(input.operation))
|
||||
{
|
||||
if(maxLength != null)
|
||||
{
|
||||
Face.Glyph[] worst = new Face.Glyph[maxLength];
|
||||
Arrays.fill(worst, face.worst());
|
||||
fitted(request, worst, frame, diagnostics);
|
||||
}
|
||||
legibility(size, dpi, frame, diagnostics);
|
||||
return (null);
|
||||
}
|
||||
if(path != null)
|
||||
{
|
||||
JsonNode bound = input.context.at(path);
|
||||
if(!bound.isTextual() && !bound.isNumber() && !bound.isBoolean())
|
||||
{
|
||||
throw diagnostics.refuse("VALUE_MISSING", id, null, frame, null, null);
|
||||
}
|
||||
text = bound.asText();
|
||||
}
|
||||
int characters = text.codePointCount(0, text.length());
|
||||
int longest = maxLength == null ? MAXIMUM_CHARACTERS : maxLength;
|
||||
if(characters > longest)
|
||||
{
|
||||
throw diagnostics.refuse("VALUE_TOO_LONG", id, null, frame, Map.of("characters", characters), Map.of("characters", longest));
|
||||
}
|
||||
int missing = face.missing(text);
|
||||
if(missing > 0)
|
||||
{
|
||||
throw diagnostics.refuse("FONT_MISSING_GLYPH", id, null, frame, Map.of("missingGlyphs", missing, "characters", characters), Map.of("missingGlyphs", 0));
|
||||
}
|
||||
TextFit.Block block = fitted(request, face.glyphs(text), frame, diagnostics);
|
||||
if(block.milliPoints() < size)
|
||||
{
|
||||
diagnostics.inform("TEXT_SHRUNK", id, null, frame, Map.of("sizeCentipoints", block.milliPoints() / 10), Map.of("sizeCentipoints", size / 10, "floorCentipoints", floor / 10));
|
||||
}
|
||||
if(block.lines().size() > 1)
|
||||
{
|
||||
diagnostics.inform("TEXT_WRAPPED", id, null, frame, Map.of("lines", block.lines().size()), Map.of("lines", maxLines));
|
||||
}
|
||||
if(block.truncated())
|
||||
{
|
||||
diagnostics.warn("TEXT_TRUNCATED", id, null, frame, Map.of("characters", block.characters(), "shownCharacters", block.shown(), "lines", block.lines().size()), Map.of("lines", maxLines));
|
||||
}
|
||||
legibility(block.milliPoints(), dpi, frame, diagnostics);
|
||||
return (new TextRaster.Drawing(face, text, block, placement.align(), placement.valign(), rotation, threshold, !reverse));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Fit, or refuse with what was measured at the floor size against the box.
|
||||
***************************************************************************/
|
||||
private TextFit.Block fitted(TextFit.Request request, Face.Glyph[] glyphs, Frame frame, Diagnostics diagnostics)
|
||||
{
|
||||
TextFit.Block block = TextFit.fit(request, glyphs, face.glyph(TextFit.MARK));
|
||||
if(!block.fits())
|
||||
{
|
||||
throw diagnostics.refuse("TEXT_DOES_NOT_FIT", id, null, frame,
|
||||
Map.of("widthDots", block.width(), "heightDots", block.height(), "lines", block.lines().size(), "sizeCentipoints", block.milliPoints() / 10, "characters", block.characters()),
|
||||
Map.of("widthDots", request.width(), "heightDots", request.height(), "lines", maxLines));
|
||||
}
|
||||
return (block);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Warn when the type is small for the printer: any face whose capitals are
|
||||
** under 12 dots, and a regular weight whose capitals are under 20.
|
||||
***************************************************************************/
|
||||
private void legibility(int milliPoints, int dpi, Frame frame, Diagnostics diagnostics)
|
||||
{
|
||||
int capitals = new TextFit.Scale(milliPoints, dpi).floor(face.capHeight());
|
||||
if(capitals < SMALL_CAPITAL_DOTS)
|
||||
{
|
||||
diagnostics.warn("TEXT_SMALL_FOR_DPI", id, null, frame, Map.of("capHeightDots", capitals, "sizeCentipoints", milliPoints / 10), Map.of("capHeightDots", SMALL_CAPITAL_DOTS));
|
||||
}
|
||||
if(!face.bold() && capitals < LIGHT_CAPITAL_DOTS)
|
||||
{
|
||||
diagnostics.warn("TEXT_LIGHT_FACE", id, null, frame, Map.of("capHeightDots", capitals, "sizeCentipoints", milliPoints / 10), Map.of("capHeightDots", LIGHT_CAPITAL_DOTS));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,22 +25,18 @@ import java.util.Set;
|
||||
import com.kingsrook.qbits.renderer.scene2.ElementFactory;
|
||||
import com.kingsrook.qbits.renderer.scene2.ElementSource;
|
||||
import com.kingsrook.qbits.renderer.scene2.SceneElement;
|
||||
import com.kingsrook.qbits.renderer.scene2.UnavailableElement;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Text: a TrueType face rasterised by the renderer, with size, fit and legibility
|
||||
** rules.
|
||||
**
|
||||
** This is the placeholder of the scene 2 core: a `text` element is parsed and
|
||||
** placed, then refused as `RENDERER_TOO_OLD`. The package that draws it
|
||||
** replaces this class and nothing else.
|
||||
** Text: a TrueType face bundled with the renderer, rasterised by it, with
|
||||
** size, fit and legibility rules. Version 2 has no printer font: a face the
|
||||
** renderer cannot measure can be neither fitted nor previewed truthfully.
|
||||
*******************************************************************************/
|
||||
public final class TextFactory implements ElementFactory
|
||||
{
|
||||
/***************************************************************************
|
||||
** The property names the design gives a `text` element. Their values are
|
||||
** not read yet.
|
||||
** The properties of a `text` element beyond the common ones. `font` and
|
||||
** `size` may come from the scene's `defaults`.
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public Set<String> properties()
|
||||
@@ -51,11 +47,11 @@ public final class TextFactory implements ElementFactory
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** The element, placed and then refused by name.
|
||||
** Build one text element.
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public SceneElement create(ElementSource source)
|
||||
{
|
||||
return (new UnavailableElement(source));
|
||||
return (new TextElement(source));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,405 @@
|
||||
/*
|
||||
* 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.text;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import com.kingsrook.qbits.renderer.scene2.text.Face.Glyph;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Measurement and fit of one text in one box, in whole dots, by integer
|
||||
** arithmetic on the face's own numbers. Nothing is drawn here, and what is
|
||||
** drawn later is drawn from this answer: the pen position of every character
|
||||
** and the box of every line are decided once.
|
||||
**
|
||||
** The order is fixed. Lay the text out at the nominal size within the allowed
|
||||
** lines. If it does not fit, take the largest size, in steps of a quarter of
|
||||
** a point down to the floor, at which it does. If none does, the text is
|
||||
** refused, or, only when the element asked for it, cut at a character and
|
||||
** ended with an ellipsis at the floor size.
|
||||
**
|
||||
** At each size a line is first broken between words only. If that does not
|
||||
** fit, it is broken at any character. So a value with no spaces wraps, a
|
||||
** sentence keeps its words while it can, and a larger size is preferred to
|
||||
** whole words. Breaking at any character is also what makes a length provable:
|
||||
** a text of n characters needs no more lines than n of the face's widest.
|
||||
**
|
||||
** A line's box is its pen advance, widened to hold the ink of every character
|
||||
** in it (a `j` that starts left of the pen, an accent above the ascent) plus
|
||||
** `SLOP` dots on each inked side. The drawn glyph is the font's outline fitted
|
||||
** to the dot grid by the rasteriser, which moves an edge by less than two dots
|
||||
** from the outline scaled exactly; the slop is that allowance, and a test
|
||||
** holds every character of every bundled face to it.
|
||||
*******************************************************************************/
|
||||
public final class TextFit
|
||||
{
|
||||
public static final int SLOP = 2;
|
||||
public static final int STEP_MILLIPOINTS = 250;
|
||||
public static final int MARK = 0x2026;
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Utility class.
|
||||
***************************************************************************/
|
||||
private TextFit()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** What is asked: a face, a box in dots as the text reads (before any
|
||||
** rotation), the resolution, the nominal size and the floor in thousandths
|
||||
** of a point, the line pitch in thousandths of the size (null for the
|
||||
** face's own), the most lines, and whether a text that fits at no size is
|
||||
** cut short rather than refused.
|
||||
***************************************************************************/
|
||||
public record Request(Face face, int width, int height, int dpi, int nominal, int floor, Integer lineHeight, int maxLines, boolean truncate)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** One line: the characters `from` to `to` of the text, followed by the
|
||||
** ellipsis when `marked`. `left` (never positive) and `right` are the sides
|
||||
** of its box measured from the pen at the start of the line; `above` and
|
||||
** `below` are how far its ink reaches from the baseline.
|
||||
***************************************************************************/
|
||||
public record Line(int from, int to, boolean marked, int left, int right, int above, int below)
|
||||
{
|
||||
/************************************************************************
|
||||
** The width of the line's box.
|
||||
************************************************************************/
|
||||
public int width()
|
||||
{
|
||||
return (right - left);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** The answer: the size chosen, the lines, the size of the block they make,
|
||||
** the baseline of the first line from the top of the block, the distance
|
||||
** between baselines, how many characters there were (trailing spaces never
|
||||
** print and are not counted), how many are shown, and whether it fits. When
|
||||
** it does not, the numbers are those of the text broken at any character at
|
||||
** the floor size, which is what a refusal reports.
|
||||
***************************************************************************/
|
||||
public record Block(int milliPoints, List<Line> lines, int width, int height, int baseline, int pitch, int characters, int shown, boolean fits)
|
||||
{
|
||||
/************************************************************************
|
||||
** Whether the text was cut short.
|
||||
************************************************************************/
|
||||
public boolean truncated()
|
||||
{
|
||||
return (shown < characters);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Dots from thousandths of the em at one size and resolution. A size of
|
||||
** `milliPoints` is `milliPoints * dpi / 72000` dots to the em, so a length
|
||||
** of `units` is `units * milliPoints * dpi / 72000000` dots, rounded as the
|
||||
** use says. The products stay far inside a long.
|
||||
***************************************************************************/
|
||||
public record Scale(int milliPoints, int dpi)
|
||||
{
|
||||
private static final long DIVISOR = 72_000_000L;
|
||||
|
||||
|
||||
|
||||
/************************************************************************
|
||||
** Rounded down.
|
||||
************************************************************************/
|
||||
public int floor(long units)
|
||||
{
|
||||
return (Math.toIntExact(Math.floorDiv(units * milliPoints * dpi, DIVISOR)));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/************************************************************************
|
||||
** Rounded up.
|
||||
************************************************************************/
|
||||
public int ceil(long units)
|
||||
{
|
||||
return (-floor(-units));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/************************************************************************
|
||||
** To the nearest dot, a half upward.
|
||||
************************************************************************/
|
||||
public int round(long units)
|
||||
{
|
||||
return (Math.toIntExact(Math.floorDiv(units * milliPoints * dpi + DIVISOR / 2, DIVISOR)));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/************************************************************************
|
||||
** The size of the em in dots, for the font that draws.
|
||||
************************************************************************/
|
||||
public float size()
|
||||
{
|
||||
return ((float) ((double) milliPoints * dpi / 72_000));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** The box of a line as characters are added to it from the left.
|
||||
***************************************************************************/
|
||||
private static final class Extent
|
||||
{
|
||||
private final Scale scale;
|
||||
private long units;
|
||||
private int left;
|
||||
private int right;
|
||||
private int above;
|
||||
private int below;
|
||||
|
||||
|
||||
|
||||
/************************************************************************
|
||||
** An empty line at one size.
|
||||
************************************************************************/
|
||||
Extent(Scale scale)
|
||||
{
|
||||
this.scale = scale;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/************************************************************************
|
||||
** Add the next character at the pen. A space moves the pen and widens
|
||||
** nothing, so spaces at the end of a line take no room.
|
||||
************************************************************************/
|
||||
void add(Glyph glyph)
|
||||
{
|
||||
int pen = scale.round(units);
|
||||
units += glyph.advance();
|
||||
if(!glyph.space())
|
||||
{
|
||||
right = Math.max(right, scale.ceil(units));
|
||||
}
|
||||
if(glyph.ink())
|
||||
{
|
||||
left = Math.min(left, pen + scale.floor(glyph.left()) - SLOP);
|
||||
right = Math.max(right, pen + scale.ceil(glyph.right()) + SLOP);
|
||||
above = Math.max(above, scale.ceil(glyph.top()) + SLOP);
|
||||
below = Math.max(below, scale.ceil(glyph.bottom()) + SLOP);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/************************************************************************
|
||||
** The width of the box so far.
|
||||
************************************************************************/
|
||||
int width()
|
||||
{
|
||||
return (right - left);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Fit a text, given as the measurements of its characters, by the fixed
|
||||
** order. `mark` is the face's ellipsis, used only when the request allows
|
||||
** cutting.
|
||||
***************************************************************************/
|
||||
public static Block fit(Request request, Glyph[] glyphs, Glyph mark)
|
||||
{
|
||||
int count = glyphs.length;
|
||||
while(count > 0 && glyphs[count - 1].space())
|
||||
{
|
||||
count--;
|
||||
}
|
||||
int size = request.nominal();
|
||||
while(true)
|
||||
{
|
||||
Scale scale = new Scale(size, request.dpi());
|
||||
Block words = block(request, scale, lines(request, scale, glyphs, count, true), count, count);
|
||||
if(words.fits())
|
||||
{
|
||||
return (words);
|
||||
}
|
||||
Block characters = block(request, scale, lines(request, scale, glyphs, count, false), count, count);
|
||||
if(characters.fits() || size == request.floor())
|
||||
{
|
||||
return (characters.fits() || !request.truncate() ? characters : cut(request, scale, glyphs, mark, characters));
|
||||
}
|
||||
size = Math.max(request.floor(), size - STEP_MILLIPOINTS);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Break a text into lines at one size, between words or at any character.
|
||||
** Spaces where a line is broken are dropped.
|
||||
***************************************************************************/
|
||||
private static List<Line> lines(Request request, Scale scale, Glyph[] glyphs, int count, boolean words)
|
||||
{
|
||||
List<Line> lines = new ArrayList<>();
|
||||
int from = 0;
|
||||
while(from < count)
|
||||
{
|
||||
int to = end(request, scale, glyphs, from, count, words);
|
||||
lines.add(line(scale, glyphs, from, to, null));
|
||||
from = to;
|
||||
while(from < count && glyphs[from].space())
|
||||
{
|
||||
from++;
|
||||
}
|
||||
}
|
||||
return (lines);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Where the line that starts at `from` ends: after the last word, or the
|
||||
** last character, that still fits the width. The first one is taken even
|
||||
** when it is too wide, so the line is then wider than the box and the
|
||||
** layout does not fit. Broken at any character, a line may end after a
|
||||
** space, which takes no room; so spaces at the start of a text cannot push
|
||||
** its first letter out of the box.
|
||||
***************************************************************************/
|
||||
private static int end(Request request, Scale scale, Glyph[] glyphs, int from, int count, boolean words)
|
||||
{
|
||||
Extent extent = new Extent(scale);
|
||||
int accepted = from;
|
||||
for(int index = from; index < count; index++)
|
||||
{
|
||||
extent.add(glyphs[index]);
|
||||
if(!words || (!glyphs[index].space() && (index + 1 == count || glyphs[index + 1].space())))
|
||||
{
|
||||
boolean wide = extent.width() > request.width();
|
||||
if(wide && accepted > from)
|
||||
{
|
||||
return (accepted);
|
||||
}
|
||||
accepted = index + 1;
|
||||
if(wide)
|
||||
{
|
||||
return (accepted);
|
||||
}
|
||||
}
|
||||
}
|
||||
return (accepted);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Measure one line, with the ellipsis after it when one is given.
|
||||
***************************************************************************/
|
||||
private static Line line(Scale scale, Glyph[] glyphs, int from, int to, Glyph mark)
|
||||
{
|
||||
Extent extent = new Extent(scale);
|
||||
for(int index = from; index < to; index++)
|
||||
{
|
||||
extent.add(glyphs[index]);
|
||||
}
|
||||
if(mark != null)
|
||||
{
|
||||
extent.add(mark);
|
||||
}
|
||||
return (new Line(from, to, mark != null, extent.left, extent.right, extent.above, extent.below));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Stack lines into a block and hold it against the box. A line reserves the
|
||||
** face's ascent above its baseline and its descent below; the block grows
|
||||
** where the ink of a line reaches further.
|
||||
***************************************************************************/
|
||||
private static Block block(Request request, Scale scale, List<Line> lines, int characters, int shown)
|
||||
{
|
||||
if(lines.isEmpty())
|
||||
{
|
||||
return (new Block(scale.milliPoints(), lines, 0, 0, 0, 0, characters, shown, true));
|
||||
}
|
||||
Face face = request.face();
|
||||
int ascent = scale.ceil(face.ascent());
|
||||
int pitch = Math.max(1, scale.ceil(request.lineHeight() == null ? face.ascent() + face.descent() + face.leading() : request.lineHeight()));
|
||||
int top = 0;
|
||||
int bottom = ascent + (lines.size() - 1) * pitch + scale.ceil(face.descent());
|
||||
int width = 0;
|
||||
for(int index = 0; index < lines.size(); index++)
|
||||
{
|
||||
Line line = lines.get(index);
|
||||
int baseline = ascent + index * pitch;
|
||||
top = Math.min(top, baseline - line.above());
|
||||
bottom = Math.max(bottom, baseline + line.below());
|
||||
width = Math.max(width, line.width());
|
||||
}
|
||||
boolean fits = lines.size() <= request.maxLines() && width <= request.width() && bottom - top <= request.height();
|
||||
return (new Block(scale.milliPoints(), lines, width, bottom - top, ascent - top, pitch, characters, shown, fits));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Cut a text that fits at no size: keep as many lines as the box holds, and
|
||||
** end the last of them with the ellipsis after as many of its characters as
|
||||
** still fit with it, down to the ellipsis alone. Something is always left
|
||||
** out, so the ellipsis always stands for text, and spaces before it are
|
||||
** dropped. When not even the ellipsis fits, the answer is the refusal that
|
||||
** was passed in.
|
||||
***************************************************************************/
|
||||
private static Block cut(Request request, Scale scale, Glyph[] glyphs, Glyph mark, Block refused)
|
||||
{
|
||||
int all = refused.lines().size();
|
||||
for(int kept = Math.min(request.maxLines(), all); kept > 0; kept--)
|
||||
{
|
||||
List<Line> lines = new ArrayList<>(refused.lines().subList(0, kept));
|
||||
int from = lines.get(kept - 1).from();
|
||||
for(int to = lines.get(kept - 1).to() - (kept == all ? 1 : 0); to >= from; to--)
|
||||
{
|
||||
if(to > from && glyphs[to - 1].space())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
lines.set(kept - 1, line(scale, glyphs, from, to, mark));
|
||||
Block block = block(request, scale, lines, refused.characters(), to);
|
||||
if(block.fits())
|
||||
{
|
||||
return (block);
|
||||
}
|
||||
}
|
||||
}
|
||||
return (refused);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
/*
|
||||
* 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.text;
|
||||
|
||||
|
||||
import java.awt.Color;
|
||||
import java.awt.Font;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.Rectangle;
|
||||
import java.awt.RenderingHints;
|
||||
import java.awt.font.GlyphVector;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.awt.image.DataBufferByte;
|
||||
import com.kingsrook.qbits.renderer.scene2.Canvas;
|
||||
import com.kingsrook.qbits.renderer.scene2.Frame;
|
||||
import com.kingsrook.qbits.renderer.scene2.Layout;
|
||||
import com.kingsrook.qbits.renderer.scene2.Placement;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Draws a fitted text onto the label. Each glyph is drawn antialiased at the
|
||||
** whole-dot pen position the fit decided, by the same Java rasteriser version 1
|
||||
** raster text uses, and a dot is ink when its grey is darker than the
|
||||
** element's threshold, so a higher threshold prints heavier.
|
||||
**
|
||||
** A line is drawn on a strip: its measured box and a guard of `GUARD` dots all
|
||||
** round. Ink found in the guard, or a glyph whose bitmap the runtime places
|
||||
** anywhere outside the measured box, means the measurement was wrong. Such
|
||||
** ink is never clipped away and never printed outside the box: the render
|
||||
** fails instead.
|
||||
**
|
||||
** A strip of more than `BAND_DOTS` dots (very large type) is drawn in bands of
|
||||
** whole rows, so one line never needs more grey than that. Where the bands
|
||||
** fall depends only on the box of the line, so the picture is the same on
|
||||
** every machine; the rasteriser may shade an edge dot at the border of a band
|
||||
** differently than it would in one piece.
|
||||
*******************************************************************************/
|
||||
final class TextRaster
|
||||
{
|
||||
static final int GUARD = 2;
|
||||
static final int BAND_DOTS = 1 << 23;
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Utility class.
|
||||
***************************************************************************/
|
||||
private TextRaster()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** What to draw and how: the face, the text, its fitted block, where lines
|
||||
** sit across the block, the clockwise rotation, the grey below which a dot
|
||||
** is ink, and whether the dots are ink or (for reverse text) cleared.
|
||||
***************************************************************************/
|
||||
record Drawing(Face face, String text, TextFit.Block block, Placement.Align align, Placement.Valign valign, int rotation, int threshold, boolean ink)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Draw into the element's box, which is given as it appears on the label:
|
||||
** a text turned a quarter reads along the box's height.
|
||||
***************************************************************************/
|
||||
static void paint(Canvas canvas, Frame frame, Drawing drawing, int bandDots)
|
||||
{
|
||||
TextFit.Block block = drawing.block();
|
||||
boolean upright = drawing.rotation() % 180 == 0;
|
||||
Frame reading = new Frame(0, 0, upright ? frame.width() : frame.height(), upright ? frame.height() : frame.width());
|
||||
Frame content = Layout.align(reading, block.width(), block.height(), drawing.align(), drawing.valign());
|
||||
TextFit.Scale scale = new TextFit.Scale(block.milliPoints(), canvas.dpi());
|
||||
Font font = drawing.face().font(scale.size());
|
||||
for(int index = 0; index < block.lines().size(); index++)
|
||||
{
|
||||
TextFit.Line line = block.lines().get(index);
|
||||
int left = content.x() + drawing.align().position().place(block.width(), line.width(), 0);
|
||||
int top = content.y() + block.baseline() + index * block.pitch() - line.above();
|
||||
strip(canvas, frame, reading, drawing, scale, font, line, left, top, bandDots);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Draw one line whose box has its top left at `left`, `top` of the reading
|
||||
** frame, a band of rows at a time.
|
||||
***************************************************************************/
|
||||
private static void strip(Canvas canvas, Frame frame, Frame reading, Drawing drawing, TextFit.Scale scale, Font font, TextFit.Line line, int left, int top, int bandDots)
|
||||
{
|
||||
int width = line.width() + 2 * GUARD;
|
||||
int height = line.above() + line.below() + 2 * GUARD;
|
||||
int rows = Math.max(1, bandDots / width);
|
||||
for(int first = 0; first < height; first += rows)
|
||||
{
|
||||
BufferedImage band = new BufferedImage(width, Math.min(rows, height - first), BufferedImage.TYPE_BYTE_GRAY);
|
||||
Graphics2D graphics = band.createGraphics();
|
||||
boolean clipped = false;
|
||||
try
|
||||
{
|
||||
graphics.setColor(Color.WHITE);
|
||||
graphics.fillRect(0, 0, band.getWidth(), band.getHeight());
|
||||
graphics.setColor(Color.BLACK);
|
||||
graphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
|
||||
graphics.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
|
||||
graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
long units = 0;
|
||||
for(int index = line.from(); index < line.to() + (line.marked() ? 1 : 0); index++)
|
||||
{
|
||||
char character = index < line.to() ? drawing.text().charAt(index) : (char) TextFit.MARK;
|
||||
Face.Glyph glyph = drawing.face().glyph(character);
|
||||
if(glyph.ink())
|
||||
{
|
||||
GlyphVector vector = font.createGlyphVector(Face.CONTEXT, new char[] {character});
|
||||
int pen = GUARD - line.left() + scale.round(units);
|
||||
Rectangle pixels = vector.getGlyphPixelBounds(0, Face.CONTEXT, pen, GUARD + line.above());
|
||||
clipped |= !pixels.isEmpty() && (pixels.x < GUARD || pixels.y < GUARD || pixels.x + pixels.width > width - GUARD || pixels.y + pixels.height > height - GUARD);
|
||||
graphics.drawGlyphVector(vector, pen, GUARD + line.above() - first);
|
||||
}
|
||||
units += glyph.advance();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
graphics.dispose();
|
||||
}
|
||||
byte[] grey = ((DataBufferByte) band.getRaster().getDataBuffer()).getData();
|
||||
for(int y = 0; y < band.getHeight(); y++)
|
||||
{
|
||||
for(int x = 0; x < width; x++)
|
||||
{
|
||||
if((grey[y * width + x] & 255) < drawing.threshold())
|
||||
{
|
||||
int row = first + y;
|
||||
if(x < GUARD || x >= width - GUARD || row < GUARD || row >= height - GUARD)
|
||||
{
|
||||
throw new IllegalStateException("TEXT_INK_OUTSIDE_MEASURE");
|
||||
}
|
||||
dot(canvas, frame, reading, drawing, left + x - GUARD, top + row - GUARD);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////
|
||||
// ink that fell wholly beyond the guard was clipped by the strip and left no dot //
|
||||
// to find; the bounds of its glyph say so //
|
||||
/////////////////////////////////////////////////////////////////////////////////////
|
||||
if(clipped)
|
||||
{
|
||||
throw new IllegalStateException("TEXT_INK_OUTSIDE_MEASURE");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Put one dot of the reading frame on the label, turned clockwise.
|
||||
***************************************************************************/
|
||||
private static void dot(Canvas canvas, Frame frame, Frame reading, Drawing drawing, int x, int y)
|
||||
{
|
||||
int turnedX = switch(drawing.rotation())
|
||||
{
|
||||
case 90 -> reading.height() - 1 - y;
|
||||
case 180 -> reading.width() - 1 - x;
|
||||
case 270 -> y;
|
||||
default -> x;
|
||||
};
|
||||
int turnedY = switch(drawing.rotation())
|
||||
{
|
||||
case 90 -> x;
|
||||
case 180 -> reading.height() - 1 - y;
|
||||
case 270 -> reading.width() - 1 - x;
|
||||
default -> y;
|
||||
};
|
||||
canvas.set(frame.x() + turnedX, frame.y() + turnedY, drawing.ink());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
9e6c74a889a700d707613d24548fe4ffa6bc59559a0689d2cf9e133bdcdafb2f ibm-plex-sans/IBMPlexSans-Bold.ttf
|
||||
975dcda37d80f038dcd143c22e33ca2d97a0cc5a929aace1c749153b0fe1afa5 ibm-plex-sans/IBMPlexSans-Regular.ttf
|
||||
7e6b2818edbd8f6a01ae80641cc8f16a51080d08fb4e532be3a0b6f74adb07da ibm-plex-sans/LICENSE.txt
|
||||
723e245902b45a49f8cf602d15891bae08c31309dce7c56d209d10cd471b88c7 ibm-plex-sans-condensed/IBMPlexSansCondensed-Bold.ttf
|
||||
7e6b2818edbd8f6a01ae80641cc8f16a51080d08fb4e532be3a0b6f74adb07da ibm-plex-sans-condensed/LICENSE.txt
|
||||
5590990c82e097397517f275f430af4546e1c45cff408bde4255dad142479dcb jetbrains-mono/JetBrainsMono-Bold.ttf
|
||||
a0bf60ef0f83c5ed4d7a75d45838548b1f6873372dfac88f71804491898d138f jetbrains-mono/JetBrainsMono-Regular.ttf
|
||||
30f0c136e3c88e422d0791acd97238870f9054a9729bc34cf2ff0d4ed8cac4ad jetbrains-mono/OFL.txt
|
||||
@@ -0,0 +1,93 @@
|
||||
Copyright © 2017 IBM Corp. with Reserved Font Name "Plex"
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
|
||||
This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
@@ -0,0 +1,23 @@
|
||||
# IBM Plex Sans Condensed (Bold)
|
||||
|
||||
An unmodified file from the official release of the IBM Plex project, retrieved 2026-11-12.
|
||||
|
||||
| | |
|
||||
| --- | --- |
|
||||
| Project | https://github.com/IBM/plex |
|
||||
| Release | `@ibm/plex-sans-condensed@2.0.0` |
|
||||
| Archive | https://github.com/IBM/plex/releases/download/%40ibm/plex-sans-condensed%402.0.0/ibm-plex-sans-condensed.zip |
|
||||
| Archive SHA-256 | `f86ba73abbeed7cf7690ead863192a87211dfc2b0cef0ac6bf4700533ec5ab9e` |
|
||||
| Licence | SIL Open Font License 1.1, `LICENSE.txt` in this directory (the archive's `ibm-plex-sans-condensed/LICENSE.txt`, byte for byte) |
|
||||
| Reserved Font Name | "Plex" (the file is not modified and not renamed) |
|
||||
|
||||
| File here | Path in the archive | Font version (name table) | SHA-256 |
|
||||
| --- | --- | --- | --- |
|
||||
| `IBMPlexSansCondensed-Bold.ttf` | `ibm-plex-sans-condensed/fonts/complete/ttf/IBMPlexSansCondensed-Bold.ttf` | Version 3.000 | `723e245902b45a49f8cf602d15891bae08c31309dce7c56d209d10cd471b88c7` |
|
||||
| `LICENSE.txt` | `ibm-plex-sans-condensed/LICENSE.txt` | | `7e6b2818edbd8f6a01ae80641cc8f16a51080d08fb4e532be3a0b6f74adb07da` |
|
||||
|
||||
The licence was confirmed from the release archive itself: its `LICENSE.txt` states "This Font Software is
|
||||
licensed under the SIL Open Font License, Version 1.1" and carries the full text; the licence field of the
|
||||
font's name table says the same, and the font's embedding bits (`OS/2.fsType`) are 0, installable embedding.
|
||||
|
||||
Scene name: `qqq-condensed-bold`.
|
||||
@@ -0,0 +1,93 @@
|
||||
Copyright © 2017 IBM Corp. with Reserved Font Name "Plex"
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
|
||||
This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
@@ -0,0 +1,24 @@
|
||||
# IBM Plex Sans (Regular, Bold)
|
||||
|
||||
Unmodified files from the official release of the IBM Plex project, retrieved 2026-11-12.
|
||||
|
||||
| | |
|
||||
| --- | --- |
|
||||
| Project | https://github.com/IBM/plex |
|
||||
| Release | `@ibm/plex-sans@1.1.0` |
|
||||
| Archive | https://github.com/IBM/plex/releases/download/%40ibm/plex-sans%401.1.0/ibm-plex-sans.zip |
|
||||
| Archive SHA-256 | `fb365d910566e6d199cc2c15579a7dd9a267128e18431a394ed81f1970c69200` |
|
||||
| Licence | SIL Open Font License 1.1, `LICENSE.txt` in this directory (the archive's `ibm-plex-sans/LICENSE.txt`, byte for byte) |
|
||||
| Reserved Font Name | "Plex" (the files are not modified and not renamed) |
|
||||
|
||||
| File here | Path in the archive | Font version (name table) | SHA-256 |
|
||||
| --- | --- | --- | --- |
|
||||
| `IBMPlexSans-Regular.ttf` | `ibm-plex-sans/fonts/complete/ttf/IBMPlexSans-Regular.ttf` | Version 3.005 | `975dcda37d80f038dcd143c22e33ca2d97a0cc5a929aace1c749153b0fe1afa5` |
|
||||
| `IBMPlexSans-Bold.ttf` | `ibm-plex-sans/fonts/complete/ttf/IBMPlexSans-Bold.ttf` | Version 3.005 | `9e6c74a889a700d707613d24548fe4ffa6bc59559a0689d2cf9e133bdcdafb2f` |
|
||||
| `LICENSE.txt` | `ibm-plex-sans/LICENSE.txt` | | `7e6b2818edbd8f6a01ae80641cc8f16a51080d08fb4e532be3a0b6f74adb07da` |
|
||||
|
||||
The licence was confirmed from the release archive itself: its `LICENSE.txt` states "This Font Software is
|
||||
licensed under the SIL Open Font License, Version 1.1" and carries the full text; the licence field of each
|
||||
font's name table says the same, and each font's embedding bits (`OS/2.fsType`) are 0, installable embedding.
|
||||
|
||||
Scene names: `qqq-sans` (Regular), `qqq-sans-bold` (Bold).
|
||||
@@ -0,0 +1,93 @@
|
||||
Copyright 2020 The JetBrains Mono Project Authors (https://github.com/JetBrains/JetBrainsMono)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
https://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
@@ -0,0 +1,24 @@
|
||||
# JetBrains Mono (Regular, Bold)
|
||||
|
||||
Unmodified files from the official release of the JetBrains Mono project, retrieved 2026-11-12.
|
||||
|
||||
| | |
|
||||
| --- | --- |
|
||||
| Project | https://github.com/JetBrains/JetBrainsMono |
|
||||
| Release | `v2.304` |
|
||||
| Archive | https://github.com/JetBrains/JetBrainsMono/releases/download/v2.304/JetBrainsMono-2.304.zip |
|
||||
| Archive SHA-256 | `6f6376c6ed2960ea8a963cd7387ec9d76e3f629125bc33d1fdcd7eb7012f7bbf` |
|
||||
| Licence | SIL Open Font License 1.1, `OFL.txt` in this directory (the archive's `OFL.txt`, byte for byte) |
|
||||
| Reserved Font Name | none declared |
|
||||
|
||||
| File here | Path in the archive | Font version (name table) | SHA-256 |
|
||||
| --- | --- | --- | --- |
|
||||
| `JetBrainsMono-Regular.ttf` | `fonts/ttf/JetBrainsMono-Regular.ttf` | Version 2.304; ttfautohint (v1.8.4.7-5d5b) | `a0bf60ef0f83c5ed4d7a75d45838548b1f6873372dfac88f71804491898d138f` |
|
||||
| `JetBrainsMono-Bold.ttf` | `fonts/ttf/JetBrainsMono-Bold.ttf` | Version 2.304; ttfautohint (v1.8.4.7-5d5b) | `5590990c82e097397517f275f430af4546e1c45cff408bde4255dad142479dcb` |
|
||||
| `OFL.txt` | `OFL.txt` | | `30f0c136e3c88e422d0791acd97238870f9054a9729bc34cf2ff0d4ed8cac4ad` |
|
||||
|
||||
The licence was confirmed from the release archive itself: its `OFL.txt` states "This Font Software is
|
||||
licensed under the SIL Open Font License, Version 1.1" and carries the full text; the licence field of each
|
||||
font's name table says the same, and each font's embedding bits (`OS/2.fsType`) are 0, installable embedding.
|
||||
|
||||
Scene names: `qqq-mono` (Regular), `qqq-mono-bold` (Bold).
|
||||
@@ -22,6 +22,8 @@ package com.kingsrook.qbits.renderer;
|
||||
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
@@ -31,6 +33,7 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import com.fasterxml.jackson.core.JsonParseException;
|
||||
import com.fasterxml.jackson.core.exc.StreamConstraintsException;
|
||||
@@ -83,6 +86,8 @@ class MutationCampaignTest
|
||||
|
||||
private static final Pattern LENGTH = Pattern.compile("-?[0-9]{1,5}(\\.[0-9]{1,3})?(mm|in|pt|%|dot)");
|
||||
private static final Pattern ELEMENT_ID = Pattern.compile("[A-Za-z][A-Za-z0-9_-]{0,31}");
|
||||
private static final Pattern TYPE_SIZE = Pattern.compile("([0-9]{1,5}(?:\\.[0-9]{1,3})?)(mm|in|pt)");
|
||||
private static final Set<String> FACES = Set.of("qqq-sans", "qqq-sans-bold", "qqq-condensed-bold", "qqq-mono", "qqq-mono-bold");
|
||||
|
||||
|
||||
|
||||
@@ -305,13 +310,15 @@ 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 and groups (the types this renderer
|
||||
** draws), groups at most four deep, every length a number and a unit, and
|
||||
** 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. Every other input is refused with a `Refusal`, which carries a
|
||||
** 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
|
||||
** 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.
|
||||
@@ -320,7 +327,7 @@ class MutationCampaignTest
|
||||
void theVersionTwoSceneParserAcceptsOnlyVersionTwoScenes() throws Exception
|
||||
{
|
||||
List<byte[]> corpus = new ArrayList<>();
|
||||
for(String name : List.of("anchors", "rules", "groups"))
|
||||
for(String name : List.of("anchors", "rules", "groups", "text-faces", "text-fit", "text-turned"))
|
||||
{
|
||||
try(InputStream stream = MutationCampaignTest.class.getResourceAsStream("/scene2/cases/" + name + ".json"))
|
||||
{
|
||||
@@ -329,6 +336,8 @@ class MutationCampaignTest
|
||||
}
|
||||
MutationCampaign campaign = new MutationCampaign(corpus, List.of("\"schema\"", "\"qqq-zpl-scene/2\"", "\"qqq-zpl-scene/1\"", "\"design\"", "\"elements\"", "\"overrides\"", "\"when\"", "\"dpi\"", "\"set\"",
|
||||
"\"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",
|
||||
"mm", "in", "pt", "%", "dot", "-", ".", "203", "300", "600", "99999999999", "4294967499", "{", "}", "[", "]", ":", ",", "\\u0000"));
|
||||
int iterations = MutationCampaign.iterations(ITERATIONS);
|
||||
for(long seed : MutationCampaign.seeds(SEEDS))
|
||||
@@ -406,7 +415,12 @@ class MutationCampaignTest
|
||||
}
|
||||
catch(Refusal refusal)
|
||||
{
|
||||
if(render)
|
||||
///////////////////////////////////////////////////////////////////////////////////////
|
||||
// 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 //
|
||||
///////////////////////////////////////////////////////////////////////////////////////
|
||||
if(render && !"VALUE_MISSING".equals(refusal.diagnostic().code()))
|
||||
{
|
||||
Refusal validated = assertThrows(Refusal.class, () -> RendererMain.process(SceneFixture.request("VALIDATE", stock, source)), "refused to render what validation accepts");
|
||||
assertEquals(refusal.diagnostic().wire(), validated.diagnostic().wire());
|
||||
@@ -436,7 +450,7 @@ class MutationCampaignTest
|
||||
assertTrue(length != null && LENGTH.matcher(length).matches() && !length.startsWith("-") && !length.endsWith("%") && !length.endsWith("dot"), () -> "accepted design " + side + " " + length);
|
||||
}
|
||||
Set<String> ids = new HashSet<>();
|
||||
assertVersionTwoElements(accepted.path("elements"), 1, ids);
|
||||
assertVersionTwoElements(accepted.path("elements"), 1, ids, accepted);
|
||||
assertTrue(ids.size() <= 1000, () -> "accepted " + ids.size() + " elements");
|
||||
for(JsonNode override : accepted.path("overrides"))
|
||||
{
|
||||
@@ -451,7 +465,7 @@ class MutationCampaignTest
|
||||
/***************************************************************************
|
||||
** The independent reading of a version 2 element list.
|
||||
***************************************************************************/
|
||||
private static void assertVersionTwoElements(JsonNode elements, int depth, Set<String> ids)
|
||||
private static void assertVersionTwoElements(JsonNode elements, int depth, Set<String> ids, JsonNode scene)
|
||||
{
|
||||
assertTrue(elements.isArray(), "accepted elements that are not a list");
|
||||
for(JsonNode element : elements)
|
||||
@@ -459,23 +473,109 @@ 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), () -> "accepted element type " + element.path("type"));
|
||||
assertTrue("box".equals(type) || "group".equals(type) || "text".equals(type), () -> "accepted element type " + element.path("type"));
|
||||
for(String name : List.of("x", "y", "width", "height", "thickness"))
|
||||
{
|
||||
assertTrue(!element.has(name) || (element.get(name).isTextual() && LENGTH.matcher(element.get(name).textValue()).matches()), () -> "accepted " + name + " " + element.get(name));
|
||||
}
|
||||
Set<String> allowed = "box".equals(type) ? Set.of("id", "type", "anchor", "x", "y", "width", "height", "align", "valign", "thickness", "fill")
|
||||
: "text".equals(type) ? Set.of("id", "type", "anchor", "x", "y", "width", "height", "align", "valign", "value", "valuePath", "font", "size", "lineHeight", "fit", "maxLength", "threshold", "reverse", "rotation")
|
||||
: 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))
|
||||
{
|
||||
assertTrue(depth <= 4, "accepted groups more than four deep");
|
||||
assertVersionTwoElements(element.path("children"), depth + 1, ids);
|
||||
assertVersionTwoElements(element.path("children"), depth + 1, ids, scene);
|
||||
}
|
||||
if("text".equals(type))
|
||||
{
|
||||
assertVersionTwoText(element, scene);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** The independent reading of an accepted text element, as it stands at each
|
||||
** resolution: the scene's defaults, then the element, then the overrides
|
||||
** for that resolution in order. It names a bundled face, has a physical
|
||||
** size from 1 to 1000 pt, exactly one of a literal and a pointer into the
|
||||
** record, and only fit, length, threshold, rotation, reverse and line
|
||||
** height values of the documented kinds and ranges.
|
||||
***************************************************************************/
|
||||
private static void assertVersionTwoText(JsonNode element, JsonNode scene)
|
||||
{
|
||||
for(int dpi : List.of(203, 300, 600))
|
||||
{
|
||||
ObjectNode stated = INDEPENDENT.createObjectNode();
|
||||
scene.path("defaults").properties().forEach(property -> stated.set(property.getKey(), property.getValue()));
|
||||
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 text " + element.path("id") + " at " + dpi + " dpi with ";
|
||||
assertTrue(stated.path("font").isTextual() && FACES.contains(stated.path("font").textValue()), () -> about + "font " + stated.path("font"));
|
||||
assertTrue(typeSize(stated.path("size"), 1, 1000), () -> about + "size " + stated.path("size"));
|
||||
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().codePointCount(0, stated.get("value").textValue().length()) <= 1024), () -> about + "a value that is not text");
|
||||
assertTrue(!stated.has("valuePath") || (stated.get("valuePath").isTextual() && stated.get("valuePath").textValue().startsWith("/")), () -> about + "valuePath " + stated.path("valuePath"));
|
||||
JsonNode fit = stated.path("fit");
|
||||
assertTrue(fit.isMissingNode() || fit.isObject(), () -> about + "fit " + fit);
|
||||
fit.fieldNames().forEachRemaining(name -> assertTrue(Set.of("maxLines", "shrinkTo", "overflow").contains(name), () -> about + "fit property " + name));
|
||||
assertTrue(whole(fit.path("maxLines"), 1, 64) && whole(stated.path("threshold"), 1, 254) && whole(stated.path("maxLength"), 1, 1024), () -> about + "a count out of range");
|
||||
assertTrue(fit.path("shrinkTo").isMissingNode() || typeSize(fit.path("shrinkTo"), 1, 1000), () -> about + "shrinkTo " + fit.path("shrinkTo"));
|
||||
assertTrue(fit.path("overflow").isMissingNode() || Set.of("refuse", "truncate").contains(String.valueOf(fit.path("overflow").textValue())), () -> about + "overflow " + fit.path("overflow"));
|
||||
assertTrue(stated.path("rotation").isMissingNode() || (stated.path("rotation").isInt() && Set.of(0, 90, 180, 270).contains(stated.path("rotation").intValue())), () -> about + "rotation " + stated.path("rotation"));
|
||||
assertTrue(stated.path("reverse").isMissingNode() || stated.path("reverse").isBoolean(), () -> about + "reverse " + stated.path("reverse"));
|
||||
JsonNode lineHeight = stated.path("lineHeight");
|
||||
assertTrue(lineHeight.isMissingNode() || (lineHeight.isNumber() && lineHeight.doubleValue() >= 0.5 && lineHeight.doubleValue() <= 5), () -> about + "lineHeight " + lineHeight);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Whether a value is absent or a whole number in a range.
|
||||
***************************************************************************/
|
||||
private static boolean whole(JsonNode value, int minimum, int maximum)
|
||||
{
|
||||
return (value.isMissingNode() || (value.isInt() && value.intValue() >= minimum && value.intValue() <= maximum));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Whether a value is a type size: an unsigned physical length between two
|
||||
** numbers of points, by decimal arithmetic with a hair of tolerance for the
|
||||
** renderer's rounding to thousandths of a point.
|
||||
***************************************************************************/
|
||||
private static boolean typeSize(JsonNode value, int minimum, int maximum)
|
||||
{
|
||||
Matcher matcher = TYPE_SIZE.matcher(value.isTextual() ? value.textValue() : "");
|
||||
if(!matcher.matches())
|
||||
{
|
||||
return (false);
|
||||
}
|
||||
BigDecimal points = new BigDecimal(matcher.group(1)).multiply(switch(matcher.group(2))
|
||||
{
|
||||
case "in" -> BigDecimal.valueOf(72);
|
||||
case "mm" -> BigDecimal.valueOf(72).divide(new BigDecimal("25.4"), 12, RoundingMode.HALF_EVEN);
|
||||
default -> BigDecimal.ONE;
|
||||
});
|
||||
return (points.compareTo(new BigDecimal(minimum).subtract(new BigDecimal("0.001"))) >= 0 && points.compareTo(new BigDecimal(maximum).add(new BigDecimal("0.001"))) <= 0);
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** The target: static validation of one scene on a stock all corpus scenes fit.
|
||||
***************************************************************************/
|
||||
|
||||
@@ -508,9 +508,10 @@ class Scene2Test
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Text, barcodes and images are parsed and placed by this renderer and then
|
||||
** Barcodes and images are parsed and placed by this renderer and then
|
||||
** refused by name, with the box they would have had. The design's own
|
||||
** example scene is the case.
|
||||
** 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.
|
||||
***************************************************************************/
|
||||
@Test
|
||||
void anElementThisRendererCannotDrawIsRefusedByName() throws Exception
|
||||
@@ -523,7 +524,7 @@ class Scene2Test
|
||||
+ "\"align\":\"center\",\"valign\":\"middle\",\"size\":\"11pt\",\"maxLength\":48,\"fit\":{\"maxLines\":2,\"shrinkTo\":\"7pt\",\"overflow\":\"refuse\"}}],"
|
||||
+ "\"overrides\":[{\"when\":{\"dpi\":[203]},\"set\":{\"caption\":{\"threshold\":144}}}]}";
|
||||
Map<String, Object> code = Map.of("code", "RENDERER_TOO_OLD", "severity", "ERROR", "element", "code", "box", List.of(6, 4, 751, 87));
|
||||
Map<String, Object> caption = Map.of("code", "RENDERER_TOO_OLD", "severity", "ERROR", "element", "caption", "box", List.of(6, 100, 751, 51));
|
||||
Map<String, Object> caption = Map.of("code", "VALUE_MISSING", "severity", "ERROR", "element", "caption", "box", List.of(6, 100, 751, 51));
|
||||
assertEquals(code, refused(example));
|
||||
assertEquals(code, SceneFixture.refusal(() -> RendererMain.process(SceneFixture.request("RENDER", example))));
|
||||
|
||||
@@ -539,10 +540,10 @@ class Scene2Test
|
||||
assertEquals(invalid("logo", "/elements/0/rotation"), refusedElements("{\"id\":\"logo\",\"type\":\"image\",\"asset\":\"logo\",\"rotation\":" + rotation + "}"), rotation);
|
||||
}
|
||||
assertEquals(Map.of("code", "SCENE_INVALID", "severity", "ERROR", "element", "t", "pointer", "/elements/0", "measured", Map.of("unknownProperties", 1), "limit", Map.of("unknownProperties", 0)),
|
||||
refusedElements("{\"id\":\"t\",\"type\":\"text\",\"value\":\"TEST-0001\",\"symbology\":\"qr\"}"));
|
||||
refusedElements("{\"id\":\"t\",\"type\":\"text\",\"value\":\"TEST-0001\",\"font\":\"qqq-sans\",\"size\":\"9pt\",\"symbology\":\"qr\"}"));
|
||||
|
||||
assertEquals(Map.of("code", "SCENE_INVALID", "severity", "ERROR", "element", "t", "pointer", "/elements/0", "measured", Map.of("widthDots", 0, "heightDots", 155), "limit", Map.of("widthDots", 1, "heightDots", 1)),
|
||||
refusedElements("{\"id\":\"t\",\"type\":\"text\",\"value\":\"TEST-0001\",\"width\":\"0.1mm\"}"));
|
||||
refusedElements("{\"id\":\"t\",\"type\":\"text\",\"value\":\"TEST-0001\",\"font\":\"qqq-sans\",\"size\":\"9pt\",\"width\":\"0.1mm\"}"));
|
||||
|
||||
Diagnostics diagnostics = new Diagnostics(false);
|
||||
diagnostics.declare("t", "/elements/0");
|
||||
@@ -554,7 +555,7 @@ class Scene2Test
|
||||
assertEquals(144, source.value("threshold").intValue());
|
||||
assertNull(source.value("size"));
|
||||
assertEquals(new Placement(Placement.Anchor.TOP_LEFT, Length.ZERO, Length.ZERO, null, null, Placement.Align.LEFT, Placement.Valign.TOP), source.placement());
|
||||
SceneElement placeholder = ElementFactory.FACTORIES.get("text").create(source);
|
||||
SceneElement placeholder = ElementFactory.FACTORIES.get("image").create(source);
|
||||
assertEquals("t", placeholder.id());
|
||||
assertEquals(List.of(), placeholder.children());
|
||||
assertEquals(Map.of("code", "RENDERER_TOO_OLD", "severity", "ERROR", "element", "t", "box", List.of(1, 2, 3, 4)),
|
||||
@@ -574,8 +575,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\":\"text\",\"value\":\"TEST-0001\"}]},"
|
||||
+ "{\"id\":\"words\",\"type\":\"text\",\"value\":\"TEST-0001\",\"y\":\"10mm\",\"height\":\"5mm\"},"
|
||||
+ "{\"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\":\"also\",\"type\":\"box\",\"anchor\":\"bottom-left\",\"width\":\"8dot\",\"height\":\"2dot\",\"fill\":true}");
|
||||
List<Map<String, Object>> expected = List.of(
|
||||
Map.of("code", "DEVICE_UNIT_USED", "severity", "WARNING", "element", "good", "pointer", "/elements/0/x"),
|
||||
|
||||
@@ -48,8 +48,12 @@ import static org.junit.jupiter.api.Assertions.fail;
|
||||
** Golden labels for the layout cases in `scene2/cases`, at 203, 300 and 600
|
||||
** dpi. Each case has a picture (the page a printer would draw, compared dot
|
||||
** for dot) and the exact ZPL. A box is whole dots of pure ink, so both are
|
||||
** exact on every machine. A golden changes only with a reviewed change to the
|
||||
** layout rules: what this build drew is left in `target/scene2-actual`.
|
||||
** exact on every machine. Text is measured by integer arithmetic and drawn
|
||||
** by the Java runtime's rasteriser from the bundled faces only; the text
|
||||
** goldens were generated on Linux (Temurin 21.0.9, the CI image) and are the
|
||||
** same bytes on Windows (Temurin 21.0.12), and they are compared exactly on
|
||||
** both. A golden changes only with a reviewed change to the layout rules or
|
||||
** the runtime: what this build drew is left in `target/scene2-actual`.
|
||||
*******************************************************************************/
|
||||
class SceneGoldenTest
|
||||
{
|
||||
|
||||
@@ -0,0 +1,297 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.renderer.scene2.text;
|
||||
|
||||
|
||||
import java.awt.Font;
|
||||
import java.awt.Rectangle;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HexFormat;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
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.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The bundled faces: what ships, under which licence, with which hash, and
|
||||
** that the numbers layout relies on are the font files' own.
|
||||
*******************************************************************************/
|
||||
class FaceTest
|
||||
{
|
||||
/***************************************************************************
|
||||
** A resource of the renderer jar, or null.
|
||||
***************************************************************************/
|
||||
private static byte[] resource(String path) throws Exception
|
||||
{
|
||||
try(InputStream stream = Face.class.getResourceAsStream(path))
|
||||
{
|
||||
return (stream == null ? null : stream.readAllBytes());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** The lowercase hexadecimal SHA-256 of some bytes.
|
||||
***************************************************************************/
|
||||
private static String sha256(byte[] bytes) throws Exception
|
||||
{
|
||||
return (HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(bytes)));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Every character of the Basic Multilingual Plane a face draws.
|
||||
***************************************************************************/
|
||||
static List<Integer> drawn(Face face)
|
||||
{
|
||||
List<Integer> drawn = new ArrayList<>();
|
||||
for(int codePoint = 0; codePoint <= Character.MAX_VALUE; codePoint++)
|
||||
{
|
||||
if(face.draws(codePoint))
|
||||
{
|
||||
drawn.add(codePoint);
|
||||
}
|
||||
}
|
||||
return (drawn);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** The set is the five faces the design names, by the names it gives them,
|
||||
** and no other name is a face.
|
||||
***************************************************************************/
|
||||
@Test
|
||||
void theBundledSetIsFiveNamedFaces()
|
||||
{
|
||||
assertEquals(List.of("qqq-sans", "qqq-sans-bold", "qqq-condensed-bold", "qqq-mono", "qqq-mono-bold"), Face.all().stream().map(Face::scene).toList());
|
||||
assertEquals(List.of(false, true, true, false, true), Face.all().stream().map(Face::bold).toList());
|
||||
for(Face face : Face.all())
|
||||
{
|
||||
assertEquals(face, Face.of(face.scene()));
|
||||
assertTrue(face.scene().startsWith("qqq-"), "a bundled face has the reserved prefix");
|
||||
}
|
||||
for(String other : List.of("", "qqq-", "qqq-serif", "QQQ-SANS", "qqq-sans ", "Abel", "IBM Plex Sans"))
|
||||
{
|
||||
assertNull(Face.of(other), other);
|
||||
}
|
||||
assertNull(Face.of(null));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Every bundled face ships with its licence text, a provenance note and a
|
||||
** hash, and the bytes in the jar are the bytes that were hashed. The build
|
||||
** fails when any of the three is missing or a file changes.
|
||||
***************************************************************************/
|
||||
@Test
|
||||
void everyBundledFaceHasItsLicenceProvenanceAndAMatchingHash() throws Exception
|
||||
{
|
||||
Map<String, String> sums = new HashMap<>();
|
||||
for(String line : new String(resource("/fonts/SHA256SUMS"), StandardCharsets.US_ASCII).lines().toList())
|
||||
{
|
||||
assertTrue(line.matches("[0-9a-f]{64} [A-Za-z0-9./-]+"), line);
|
||||
assertNull(sums.put(line.substring(66), line.substring(0, 64)), "listed twice: " + line);
|
||||
}
|
||||
Set<String> licences = Set.of("ibm-plex-sans/LICENSE.txt", "ibm-plex-sans-condensed/LICENSE.txt", "jetbrains-mono/OFL.txt");
|
||||
assertEquals(Face.all().size() + licences.size(), sums.size(), "the list is exactly the faces and their licences");
|
||||
for(Face face : Face.all())
|
||||
{
|
||||
String directory = face.file().substring(0, face.file().indexOf('/') + 1);
|
||||
byte[] font = resource("/fonts/" + face.file());
|
||||
assertNotNull(font, face.file());
|
||||
assertTrue(font[0] == 0 && font[1] == 1 && font[2] == 0 && font[3] == 0, "static TrueType outlines: " + face.file());
|
||||
assertEquals(sums.get(face.file()), sha256(font), face.file());
|
||||
|
||||
String licence = licences.stream().filter(name -> name.startsWith(directory)).findFirst().orElseThrow();
|
||||
byte[] text = resource("/fonts/" + licence);
|
||||
assertNotNull(text, licence);
|
||||
assertEquals(sums.get(licence), sha256(text), licence);
|
||||
String words = new String(text, StandardCharsets.UTF_8);
|
||||
assertTrue(words.contains("This Font Software is licensed under the SIL Open Font License, Version 1.1."), licence);
|
||||
assertTrue(words.contains("SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007") && words.contains("PERMISSION & CONDITIONS") && words.contains("TERMINATION"), "the whole licence text: " + licence);
|
||||
|
||||
byte[] provenance = resource("/fonts/" + directory + "SOURCE.md");
|
||||
assertNotNull(provenance, "provenance of " + face.file());
|
||||
String note = new String(provenance, StandardCharsets.UTF_8);
|
||||
String name = face.file().substring(directory.length());
|
||||
assertTrue(note.contains("`" + name + "`") && note.contains(sums.get(face.file())) && note.contains(sums.get(licence)), "the provenance names the file and both hashes: " + name);
|
||||
assertTrue(note.contains("https://github.com/") && note.contains("/releases/download/") && note.contains("Archive SHA-256") && (note.contains("Unmodified") || note.contains("unmodified")), name);
|
||||
assertTrue(note.contains("`" + face.scene() + "`"), "the provenance gives the scene name " + face.scene());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** A file that is not a font is a broken build and says so; it is never a
|
||||
** silent fallback to another face.
|
||||
***************************************************************************/
|
||||
@Test
|
||||
void aFileThatIsNotAFontIsABrokenBuild()
|
||||
{
|
||||
assertEquals("BUNDLED_FONT_UNREADABLE", assertThrows(IllegalStateException.class, () -> Face.load("/fonts/SHA256SUMS")).getMessage());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** The bounds each face states are the font file's: the widest character
|
||||
** (the lowest of those that share the greatest advance, among characters
|
||||
** with ink), and the furthest any character reaches past its advance on the
|
||||
** left and right, above the baseline and below it. They are what a length
|
||||
** is proven against, so they are recomputed here from every character.
|
||||
***************************************************************************/
|
||||
@Test
|
||||
void theBoundsOfAFaceAreThoseOfItsFontFile()
|
||||
{
|
||||
for(Face face : Face.all())
|
||||
{
|
||||
int widest = -1;
|
||||
int advance = 0;
|
||||
int left = 0;
|
||||
int right = 0;
|
||||
int top = 0;
|
||||
int bottom = 0;
|
||||
List<Integer> drawn = drawn(face);
|
||||
for(int codePoint : drawn)
|
||||
{
|
||||
Face.Glyph glyph = face.glyph(codePoint);
|
||||
assertTrue(glyph.advance() >= 0 && glyph.advance() <= 2 * Face.REFERENCE, "advance of " + codePoint);
|
||||
assertEquals(glyph.space(), codePoint == ' ');
|
||||
if(!glyph.ink())
|
||||
{
|
||||
assertEquals(new Face.Glyph(glyph.advance(), 0, 0, 0, 0, false, codePoint == ' '), glyph);
|
||||
continue;
|
||||
}
|
||||
if(glyph.advance() > advance)
|
||||
{
|
||||
advance = glyph.advance();
|
||||
widest = codePoint;
|
||||
}
|
||||
left = Math.max(left, -glyph.left());
|
||||
right = Math.max(right, glyph.right() - glyph.advance());
|
||||
top = Math.max(top, glyph.top());
|
||||
bottom = Math.max(bottom, glyph.bottom());
|
||||
}
|
||||
assertTrue(drawn.size() > 800, face.scene() + " draws " + drawn.size() + " characters");
|
||||
assertEquals(new Face.Glyph(advance, -left, advance + right, top, bottom, true, false), face.worst(), face.scene());
|
||||
assertEquals(widest, face.widest(), face.scene());
|
||||
assertEquals(face.glyph(face.widest()).advance(), face.worst().advance());
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////
|
||||
// the faces have 1000 units to the em, so the reference size reads whole numbers //
|
||||
/////////////////////////////////////////////////////////////////////////////////////
|
||||
assertTrue(face.ascent() > 900 && face.ascent() < 1100 && face.descent() > 200 && face.descent() < 350 && face.leading() == 0, face.scene());
|
||||
assertTrue(face.capHeight() > 650 && face.capHeight() < 760, face.scene());
|
||||
}
|
||||
assertEquals(List.of(1025, 275, 698), List.of(Face.SANS.ascent(), Face.SANS.descent(), Face.SANS.capHeight()));
|
||||
assertEquals(List.of(1020, 300, 730), List.of(Face.MONO_BOLD.ascent(), Face.MONO_BOLD.descent(), Face.MONO_BOLD.capHeight()));
|
||||
assertEquals(new Face.Glyph(600, 0, 0, 0, 0, false, true), Face.MONO.glyph(' '));
|
||||
assertEquals(Set.of(600), drawn(Face.MONO).stream().map(codePoint -> Face.MONO.glyph(codePoint).advance()).collect(Collectors.toSet()), "the monospace has one advance");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** A face draws a character only as one glyph of its own, left to right.
|
||||
** Controls, formats (which would print nothing), marks that must be placed
|
||||
** on another letter, scripts that need shaping, and characters outside the
|
||||
** Basic Multilingual Plane are not drawn; nor is a character the face lacks.
|
||||
***************************************************************************/
|
||||
@Test
|
||||
void aFaceDrawsOnlyWhatItCanDrawFaithfully()
|
||||
{
|
||||
for(Face face : Face.all())
|
||||
{
|
||||
for(int codePoint = 0x20; codePoint < 0x7F; codePoint++)
|
||||
{
|
||||
assertTrue(face.draws(codePoint), face.scene() + " draws " + codePoint);
|
||||
}
|
||||
assertTrue(face.draws(TextFit.MARK), "the ellipsis that marks a cut");
|
||||
assertTrue(face.draws(0xE9) && face.draws(0xC5) && face.draws(0xA0) && face.draws(0x20AC));
|
||||
for(int codePoint : List.of(0x00, 0x09, 0x0A, 0x0D, 0x7F, 0x85, 0xAD, 0x200B, 0x200D, 0x202E, 0x2028, 0x2029, 0xFEFF, 0x0301, 0x0627, 0x05D0, 0x0915, 0x4E2D, 0xD83D, 0xE0A0, 0xFFFF, 0x1F600, 0x10FFFF))
|
||||
{
|
||||
assertFalse(face.draws(codePoint), face.scene() + " must not draw " + Integer.toHexString(codePoint));
|
||||
}
|
||||
assertEquals(0, face.missing("TEST-0001 (01)00012345678905 café")); // e acute
|
||||
assertEquals(0, face.missing(""));
|
||||
assertEquals(4, face.missing("A\tB́😀C中")); // a tab, a combining acute, one emoji (two units, one character), a CJK ideograph
|
||||
assertEquals(3, face.glyphs("A B").length);
|
||||
assertTrue(face.glyphs("A B")[1].space() && !face.glyphs("A B")[0].space());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** The rasteriser keeps every glyph inside the box layout measures for it:
|
||||
** the outline at the reference size, scaled exactly, and `TextFit.SLOP`
|
||||
** dots. Held for every character of every face over the sizes and
|
||||
** resolutions labels use. (That the ink itself stays inside is held where
|
||||
** text is drawn: `TextRasterTest`.)
|
||||
***************************************************************************/
|
||||
@Test
|
||||
void everyGlyphIsRasterisedInsideItsMeasuredBox()
|
||||
{
|
||||
for(Face face : Face.all())
|
||||
{
|
||||
List<Integer> drawn = drawn(face);
|
||||
for(int dpi : Scene2.RESOLUTIONS)
|
||||
{
|
||||
for(int milliPoints : List.of(5000, 6250, 7000, 8000, 9000, 10_500, 12_000, 18_000, 36_000, 72_000))
|
||||
{
|
||||
TextFit.Scale scale = new TextFit.Scale(milliPoints, dpi);
|
||||
Font font = face.font(scale.size());
|
||||
for(int codePoint : drawn)
|
||||
{
|
||||
Face.Glyph glyph = face.glyph(codePoint);
|
||||
Rectangle pixels = font.createGlyphVector(Face.CONTEXT, new char[] {(char) codePoint}).getGlyphPixelBounds(0, Face.CONTEXT, 0, 0);
|
||||
if(!glyph.ink())
|
||||
{
|
||||
assertTrue(pixels.isEmpty(), "ink where none is measured");
|
||||
continue;
|
||||
}
|
||||
boolean inside = pixels.x >= scale.floor(glyph.left()) - TextFit.SLOP && pixels.x + pixels.width <= scale.ceil(glyph.right()) + TextFit.SLOP
|
||||
&& -pixels.y <= scale.ceil(glyph.top()) + TextFit.SLOP && pixels.y + pixels.height <= scale.ceil(glyph.bottom()) + TextFit.SLOP;
|
||||
assertTrue(inside, () -> face.scene() + " U+" + Integer.toHexString(codePoint) + " at " + milliPoints + " millipoints, " + dpi + " dpi: " + pixels + " outside " + glyph);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,720 @@
|
||||
/*
|
||||
* 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.text;
|
||||
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import com.kingsrook.qbits.renderer.RendererMain;
|
||||
import com.kingsrook.qbits.renderer.Wire;
|
||||
import com.kingsrook.qbits.renderer.scene2.Refusal;
|
||||
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.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The `text` element through the worker's entry point: what a scene may say,
|
||||
** what each refusal reports, and what reaches the label. The stock is the
|
||||
** design document's example: 4 x 1 in with 3 mm margins, a printable area of
|
||||
** 764 x 155 dots at 24, 24 on a page of 812 x 203 dots at 203 dpi.
|
||||
*******************************************************************************/
|
||||
class TextElementTest
|
||||
{
|
||||
private static final String HEAD = "{'schema':'qqq-zpl-scene/2','design':{'width':'4in','height':'1in'},'defaults':{'font':'qqq-sans-bold','size':'9pt'},'elements':[";
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** JSON written with single quotes, for reading.
|
||||
***************************************************************************/
|
||||
private static String json(String text)
|
||||
{
|
||||
return (text.replace('\'', '"'));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** A scene with the default face and size and these elements.
|
||||
***************************************************************************/
|
||||
private static String scene(String elements)
|
||||
{
|
||||
return (json(HEAD + elements + "]}"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** A scene of one text element `t` with these properties.
|
||||
***************************************************************************/
|
||||
private static String text(String properties)
|
||||
{
|
||||
return (scene("{'id':'t','type':'text'," + properties + "}"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** A request whose record holds one value at `/record/name`.
|
||||
***************************************************************************/
|
||||
private static ObjectNode bound(String operation, String source, String value)
|
||||
{
|
||||
ObjectNode request = SceneFixture.request(operation, source);
|
||||
((ObjectNode) request.get("context")).putObject("record").put("name", value);
|
||||
return (request);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** The refusal a request ends in.
|
||||
***************************************************************************/
|
||||
private static Map<String, Object> refused(ObjectNode request)
|
||||
{
|
||||
return (SceneFixture.refusal(() -> RendererMain.process(request)));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** The findings of a request that is answered.
|
||||
***************************************************************************/
|
||||
@SuppressWarnings("unchecked")
|
||||
private static List<Map<String, Object>> findings(ObjectNode request) throws Exception
|
||||
{
|
||||
Map<String, Object> answer = RendererMain.process(request);
|
||||
assertEquals("OK", answer.get("outcome"));
|
||||
return ((List<Map<String, Object>>) answer.get("diagnostics"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** The findings with one code.
|
||||
***************************************************************************/
|
||||
private static List<Map<String, Object>> findings(ObjectNode request, String code) throws Exception
|
||||
{
|
||||
return (findings(request).stream().filter(finding -> code.equals(finding.get("code"))).toList());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** The page a request prints.
|
||||
***************************************************************************/
|
||||
private static boolean[][] page(ObjectNode request) throws Exception
|
||||
{
|
||||
return (SceneFixture.raster(SceneFixture.render(request)));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** The ink of a page: left, top, right and bottom (the last two exclusive),
|
||||
** and how many dots.
|
||||
***************************************************************************/
|
||||
private static List<Integer> ink(boolean[][] page)
|
||||
{
|
||||
int left = Integer.MAX_VALUE;
|
||||
int top = Integer.MAX_VALUE;
|
||||
int right = 0;
|
||||
int bottom = 0;
|
||||
int dots = 0;
|
||||
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 + 1);
|
||||
bottom = Math.max(bottom, y + 1);
|
||||
dots++;
|
||||
}
|
||||
}
|
||||
}
|
||||
return (List.of(left, top, right, bottom, dots));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** `SCENE_INVALID` for element `t` at a pointer.
|
||||
***************************************************************************/
|
||||
private static Map<String, Object> invalid(String pointer)
|
||||
{
|
||||
return (Map.of("code", "SCENE_INVALID", "severity", "ERROR", "element", "t", "pointer", pointer));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** A literal value in the default face is drawn in its box, at the top left
|
||||
** unless the scene says otherwise, and a render is the same bytes twice,
|
||||
** whatever the locale of the machine.
|
||||
***************************************************************************/
|
||||
@Test
|
||||
void aLiteralValueIsDrawnInItsBoxTheSameEveryTime() throws Exception
|
||||
{
|
||||
String source = text("'value':'LOT L240611A','x':'10mm','y':'5mm','width':'40mm','height':'6mm'");
|
||||
byte[] label = SceneFixture.render(SceneFixture.request("RENDER", source));
|
||||
List<Integer> ink = ink(SceneFixture.raster(label));
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// the box is 319 x 47 dots at 24 + 80, 24 + 40. Bold capitals of 9 pt are 17 dots high //
|
||||
// and sit under the ascent of 27 dots //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////
|
||||
assertTrue(ink.get(0) >= 104 && ink.get(0) <= 108 && ink.get(2) < 104 + 319 && ink.get(2) > 104 + 150, "ink " + ink);
|
||||
assertEquals(List.of(64 + 27 - 18, 64 + 27), List.of(ink.get(1), ink.get(3)), "capital height above the baseline: " + ink);
|
||||
assertEquals(List.of(), findings(SceneFixture.request("RENDER", source)));
|
||||
assertEquals(List.of(), findings(SceneFixture.request("VALIDATE", source)));
|
||||
|
||||
assertArrayEquals(label, SceneFixture.render(SceneFixture.request("RENDER", source)));
|
||||
Locale standing = Locale.getDefault();
|
||||
try
|
||||
{
|
||||
Locale.setDefault(Locale.forLanguageTag("tr-TR"));
|
||||
assertArrayEquals(label, SceneFixture.render(SceneFixture.request("RENDER", source)));
|
||||
}
|
||||
finally
|
||||
{
|
||||
Locale.setDefault(standing);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// an empty value and one of spaces print nothing and are not refused //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////
|
||||
for(String blank : List.of("", " "))
|
||||
{
|
||||
assertEquals(0, ink(page(SceneFixture.request("RENDER", text("'value':'" + blank + "'")))).get(4));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** The size is a physical length: 9 pt, an eighth of an inch and 3.175 mm
|
||||
** are one size and one picture. Each bundled face is drawn, and they
|
||||
** differ.
|
||||
***************************************************************************/
|
||||
@Test
|
||||
void theSizeIsAPhysicalLengthAndEveryFaceIsDrawn() throws Exception
|
||||
{
|
||||
boolean[][] points = page(SceneFixture.request("RENDER", text("'value':'Sample 0123','size':'9pt'")));
|
||||
assertTrue(ink(points).get(4) > 500);
|
||||
for(String size : List.of("0.125in", "3.175mm"))
|
||||
{
|
||||
assertTrue(Arrays.deepEquals(points, page(SceneFixture.request("RENDER", text("'value':'Sample 0123','size':'" + size + "'")))), size);
|
||||
}
|
||||
assertFalse(Arrays.deepEquals(points, page(SceneFixture.request("RENDER", text("'value':'Sample 0123','size':'9.25pt'")))));
|
||||
|
||||
List<List<Integer>> pictures = new ArrayList<>();
|
||||
for(Face face : Face.all())
|
||||
{
|
||||
List<Integer> ink = ink(page(SceneFixture.request("RENDER", text("'value':'Sample 0123','font':'" + face.scene() + "','size':'12pt'"))));
|
||||
assertTrue(ink.get(4) > 600, face.scene() + " " + ink);
|
||||
assertFalse(pictures.contains(ink), face.scene() + " is not another face's picture");
|
||||
pictures.add(ink);
|
||||
}
|
||||
assertTrue(pictures.get(1).get(4) > pictures.get(0).get(4) && pictures.get(4).get(4) > pictures.get(3).get(4), "a bold face has more ink than its regular");
|
||||
assertTrue(pictures.get(2).get(2) < pictures.get(1).get(2), "the condensed bold is narrower than the bold");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** A property that is not what the schema says is `SCENE_INVALID` at that
|
||||
** property, in the element, in the defaults or in the override that states
|
||||
** it; whatever is wrong inside `fit` is reported at `fit`.
|
||||
***************************************************************************/
|
||||
@Test
|
||||
void aPropertyTheSchemaDoesNotAllowIsRefusedAtThatProperty() throws Exception
|
||||
{
|
||||
Map<String, List<String>> wrong = Map.ofEntries(
|
||||
Map.entry("font", List.of("'font':12", "'font':null", "'font':['qqq-sans']")),
|
||||
Map.entry("size", List.of("'size':'9'", "'size':9", "'size':'0.999pt'", "'size':'1000.001pt'", "'size':'50%'", "'size':'12dot'", "'size':'-9pt'", "'size':'0.3mm'", "'size':'14in'")),
|
||||
Map.entry("lineHeight", List.of("'lineHeight':'1.2'", "'lineHeight':0.499", "'lineHeight':5.001", "'lineHeight':1.2345", "'lineHeight':true", "'lineHeight':1e400", "'lineHeight':-1", "'lineHeight':99999999999999999999")),
|
||||
Map.entry("threshold", List.of("'threshold':0", "'threshold':255", "'threshold':127.5", "'threshold':'128'", "'threshold':4294967424")),
|
||||
Map.entry("maxLength", List.of("'maxLength':0", "'maxLength':1025", "'maxLength':'48'", "'maxLength':4294967297", "'maxLength':null")),
|
||||
Map.entry("fit", List.of("'fit':[]", "'fit':'shrink'", "'fit':{'maxLines':0}", "'fit':{'maxLines':65}", "'fit':{'maxLines':'2'}", "'fit':{'maxLines':1.5}", "'fit':{'shrinkTo':'small'}", "'fit':{'shrinkTo':'50%'}",
|
||||
"'fit':{'shrinkTo':'6dot'}", "'fit':{'shrinkTo':'0.999pt'}", "'fit':{'shrinkTo':'9.001pt'}", "'fit':{'shrinkTo':7}", "'fit':{'overflow':'clip'}", "'fit':{'overflow':true}", "'fit':{'lines':2}",
|
||||
"'fit':{'maxLines':2,'Overflow':'refuse'}")),
|
||||
Map.entry("valuePath", List.of("'valuePath':12", "'valuePath':'record/name'", "'valuePath':''", "'valuePath':'/" + "a".repeat(256) + "'")),
|
||||
Map.entry("reverse", List.of("'reverse':'yes'", "'reverse':1")),
|
||||
Map.entry("rotation", List.of("'rotation':45", "'rotation':'90'")));
|
||||
wrong.forEach((name, cases) ->
|
||||
{
|
||||
for(String stated : cases)
|
||||
{
|
||||
String rest = name.equals("valuePath") ? "" : ",'value':'TEST-0001'";
|
||||
assertEquals(invalid("/elements/0/" + name), refused(SceneFixture.request("VALIDATE", text(stated + rest))), stated);
|
||||
assertEquals(invalid("/elements/0/" + name), refused(SceneFixture.request("RENDER", text(stated + rest))), stated);
|
||||
}
|
||||
});
|
||||
for(String value : List.of("'value':'A','valuePath':'/record/name'", "'size':'9pt'", "'value':12", "'value':null", "'value':['A']", "'value':'" + "A".repeat(1025) + "'"))
|
||||
{
|
||||
assertEquals(invalid("/elements/0/value"), refused(SceneFixture.request("VALIDATE", text(value))), value);
|
||||
}
|
||||
assertEquals("OK", RendererMain.process(SceneFixture.request("VALIDATE", text("'value':'" + "A".repeat(1024) + "','fit':{'maxLines':64,'overflow':'truncate'}"))).get("outcome"), "1024 characters is the most a value may have");
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// a face and a size are required, in the element or in the defaults; a wrong default is //
|
||||
// reported where the scene states it, for the element that takes it, and a default the //
|
||||
// element does not take is not read //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////
|
||||
String bare = "{'schema':'qqq-zpl-scene/2','design':{'width':'4in','height':'1in'},'elements':[{'id':'t','type':'text','value':'A'%s}]}";
|
||||
assertEquals(invalid("/elements/0/font"), refused(SceneFixture.request("VALIDATE", json(String.format(bare, ",'size':'9pt'")))));
|
||||
assertEquals(invalid("/elements/0/size"), refused(SceneFixture.request("VALIDATE", json(String.format(bare, ",'font':'qqq-mono'")))));
|
||||
String defaults = "{'schema':'qqq-zpl-scene/2','design':{'width':'4in','height':'1in'},'defaults':{%s},'elements':[{'id':'b','type':'box','fill':true,'width':'1mm','height':'1mm'},{'id':'t','type':'text','value':'A'%s}]}";
|
||||
assertEquals(invalid("/defaults/size"), refused(SceneFixture.request("VALIDATE", json(String.format(defaults, "'font':'qqq-mono','size':'1001pt'", "")))));
|
||||
assertEquals(invalid("/elements/1/size"), refused(SceneFixture.request("VALIDATE", json(String.format(defaults, "'font':'qqq-mono','size':'9pt'", ",'size':'1001pt'")))));
|
||||
assertEquals("OK", RendererMain.process(SceneFixture.request("VALIDATE", json(String.format(defaults, "'font':'qqq-mono','size':'1001pt'", ",'size':'9pt'")))).get("outcome"));
|
||||
assertEquals("OK", RendererMain.process(SceneFixture.request("VALIDATE", json(String.format(defaults, "'size':'9pt'", ",'font':'qqq-mono'")))).get("outcome"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** A face the renderer does not bundle is `FONT_UNKNOWN` where the scene
|
||||
** names it. Every resolution is read, so a face named only in an override
|
||||
** for another printer is refused now and not when that printer is used.
|
||||
***************************************************************************/
|
||||
@Test
|
||||
void aFaceThatIsNotBundledIsRefusedByNameAtEveryResolution()
|
||||
{
|
||||
for(String font : List.of("qqq-serif", "qqq-sans-italic", "Abel", "QQQ-SANS", "IBM Plex Sans"))
|
||||
{
|
||||
assertEquals(Map.of("code", "FONT_UNKNOWN", "severity", "ERROR", "element", "t", "pointer", "/elements/0/font"), refused(SceneFixture.request("VALIDATE", text("'value':'A','font':'" + font + "'"))), font);
|
||||
}
|
||||
String defaults = "{'schema':'qqq-zpl-scene/2','design':{'width':'4in','height':'1in'},'defaults':{'font':'Abel','size':'9pt'},'elements':[{'id':'t','type':'text','value':'A'}]}";
|
||||
assertEquals(Map.of("code", "FONT_UNKNOWN", "severity", "ERROR", "element", "t", "pointer", "/defaults/font"), refused(SceneFixture.request("RENDER", json(defaults))));
|
||||
String override = json(HEAD + "{'id':'t','type':'text','value':'A'}],'overrides':[{'when':{'dpi':[600]},'set':{'t':{'font':'qqq-heavy'}}}]}");
|
||||
assertEquals(Map.of("code", "FONT_UNKNOWN", "severity", "ERROR", "element", "t", "pointer", "/overrides/0/set/t/font"), refused(SceneFixture.request("RENDER", override)));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** A bound value is read from the record when rendering: text as it is, a
|
||||
** number or a truth value as JSON writes it. Anything else, or nothing, is
|
||||
** `VALUE_MISSING`. Validation reads no record.
|
||||
***************************************************************************/
|
||||
@Test
|
||||
void aBoundValueIsReadFromTheRecordOnlyWhenRendering() throws Exception
|
||||
{
|
||||
String source = text("'valuePath':'/record/name','maxLength':12,'width':'60mm','height':'6mm'");
|
||||
boolean[][] literal = page(SceneFixture.request("RENDER", text("'value':'TEST-0001','width':'60mm','height':'6mm'")));
|
||||
assertTrue(Arrays.deepEquals(literal, page(bound("RENDER", source, "TEST-0001"))));
|
||||
for(String scalar : List.of("12345", "12.5", "true"))
|
||||
{
|
||||
ObjectNode request = SceneFixture.request("RENDER", source);
|
||||
((ObjectNode) request.get("context")).putObject("record").set("name", Wire.JSON.readTree(scalar));
|
||||
assertTrue(Arrays.deepEquals(page(SceneFixture.request("RENDER", text("'value':'" + scalar + "','width':'60mm','height':'6mm'"))), page(request)), scalar);
|
||||
}
|
||||
Map<String, Object> missing = Map.of("code", "VALUE_MISSING", "severity", "ERROR", "element", "t", "box", List.of(0, 0, 479, 47));
|
||||
assertEquals(missing, refused(SceneFixture.request("RENDER", source)));
|
||||
for(String other : List.of("null", "{\"first\":\"A\"}", "[\"A\"]"))
|
||||
{
|
||||
ObjectNode request = SceneFixture.request("RENDER", source);
|
||||
((ObjectNode) request.get("context")).putObject("record").set("name", Wire.JSON.readTree(other));
|
||||
assertEquals(missing, refused(request), other);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// validation accepts the scene with no record, and with a record it would refuse to //
|
||||
// print: the record is not its business //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////
|
||||
assertEquals(List.of(), findings(SceneFixture.request("VALIDATE", source)));
|
||||
assertEquals(List.of(), findings(bound("VALIDATE", source, "A value far longer than twelve characters")));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** `maxLength` makes a length refusal a design-time event. When the scene is
|
||||
** validated, that many of the face's widest character must fit, or the
|
||||
** scene is refused; when printing, a longer value is refused before layout.
|
||||
** A bound element without `maxLength` is warned about; a literal needs none.
|
||||
***************************************************************************/
|
||||
@Test
|
||||
void maxLengthProvesTheFitWhenValidatingAndBoundsTheValueWhenPrinting() throws Exception
|
||||
{
|
||||
String proven = text("'valuePath':'/record/name','font':'qqq-mono-bold','maxLength':20,'width':'60mm','height':'6mm'");
|
||||
assertEquals(List.of(), findings(SceneFixture.request("VALIDATE", proven)));
|
||||
assertEquals(List.of(), findings(bound("RENDER", proven, "L240611A-RETURNED-01")));
|
||||
assertEquals(Map.of("code", "VALUE_TOO_LONG", "severity", "ERROR", "element", "t", "box", List.of(0, 0, 479, 47), "measured", Map.of("characters", 21), "limit", Map.of("characters", 20)),
|
||||
refused(bound("RENDER", proven, "L240611A-RETURNED-012")));
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// 40 of the monospace's characters at 9 pt need 609 dots and more; shrunk to the floor //
|
||||
// of 6.3 pt they still need 427 of the 319 the box has //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////
|
||||
String unproven = text("'valuePath':'/record/name','font':'qqq-mono-bold','maxLength':40,'width':'40mm','height':'6mm'");
|
||||
Map<String, Object> refusal = refused(SceneFixture.request("VALIDATE", unproven));
|
||||
assertEquals("TEXT_DOES_NOT_FIT", refusal.get("code"));
|
||||
assertEquals("t", refusal.get("element"));
|
||||
assertEquals(List.of(0, 0, 319, 47), refusal.get("box"));
|
||||
assertEquals(Map.of("widthDots", 319, "heightDots", 47, "lines", 1), refusal.get("limit"));
|
||||
Map<?, ?> measured = (Map<?, ?>) refusal.get("measured");
|
||||
assertEquals(40, measured.get("characters"));
|
||||
assertEquals(630, measured.get("sizeCentipoints"));
|
||||
assertTrue((int) measured.get("lines") > 1 && (int) measured.get("widthDots") <= 319, "measured broken at any character at the floor: " + measured);
|
||||
assertTrue(findings(bound("RENDER", unproven, "SHORT")).isEmpty(), "the record decides when printing");
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// with no stated length nothing is proven, which the author is told at validation and //
|
||||
// at render; the hard limit of 1024 characters still holds //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////
|
||||
String open = text("'valuePath':'/record/name','width':'60mm','height':'6mm'");
|
||||
Map<String, Object> warning = Map.of("code", "FIT_NOT_PROVEN", "severity", "WARNING", "element", "t", "box", List.of(0, 0, 479, 47));
|
||||
assertEquals(List.of(warning), findings(SceneFixture.request("VALIDATE", open)));
|
||||
assertEquals(List.of(warning), findings(bound("RENDER", open, "TEST-0001")));
|
||||
assertEquals(Map.of("characters", 1025), refused(bound("RENDER", open, "A".repeat(1025))).get("measured"));
|
||||
assertEquals(Map.of("characters", 1024), refused(bound("RENDER", open, "A".repeat(1025))).get("limit"));
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// a literal is laid out as it is; its stated length is still its limit //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////
|
||||
assertEquals(List.of(), findings(SceneFixture.request("VALIDATE", text("'value':'TEST-0001'"))));
|
||||
assertEquals("VALUE_TOO_LONG", refused(SceneFixture.request("VALIDATE", text("'value':'TEST-0001','maxLength':8"))).get("code"));
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// the proof holds for the worst value: 20 per mille signs, the widest character of the //
|
||||
// sans, print in the box that proved 20 //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////
|
||||
String sans = text("'valuePath':'/record/name','font':'qqq-sans','size':'8pt','maxLength':20,'width':'90mm','height':'6mm'");
|
||||
assertTrue(findings(SceneFixture.request("VALIDATE", sans), "TEXT_DOES_NOT_FIT").isEmpty());
|
||||
assertTrue(ink(page(bound("RENDER", sans, "\u2030".repeat(20)))).get(4) > 1000); // per mille
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** A character the face cannot draw as one glyph is refused with a count; the
|
||||
** value is drawn whole or not at all.
|
||||
***************************************************************************/
|
||||
@Test
|
||||
void aValueTheFaceCannotDrawIsRefusedWithACount()
|
||||
{
|
||||
String source = text("'valuePath':'/record/name','maxLength':40");
|
||||
assertEquals(Map.of("code", "FONT_MISSING_GLYPH", "severity", "ERROR", "element", "t", "box", List.of(0, 0, 764, 155), "measured", Map.of("missingGlyphs", 3, "characters", 9), "limit", Map.of("missingGlyphs", 0)),
|
||||
refused(bound("RENDER", source, "A\tB\u4E2DC\uD83D\uDE00DEF"))); // a tab, a CJK ideograph and one emoji
|
||||
assertEquals("FONT_MISSING_GLYPH", refused(bound("RENDER", source, "line\nbreak")).get("code"));
|
||||
assertEquals("FONT_MISSING_GLYPH", refused(SceneFixture.request("VALIDATE", text("'value':'e\u0301'"))).get("code")); // e and a combining acute
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** The default fit: one line, shrink to 70% of the size and not below 5 pt,
|
||||
** then refuse. The refusal names the element and gives the measured width,
|
||||
** height, lines and size against the box.
|
||||
***************************************************************************/
|
||||
@Test
|
||||
void theDefaultIsOneLineShrunkToSeventyPercentThenRefused() throws Exception
|
||||
{
|
||||
String value = "'value':'(01)00012345678905(10)L240611A','font':'qqq-mono-bold','height':'8mm'";
|
||||
assertEquals(List.of(), findings(SceneFixture.request("RENDER", text(value + ",'size':'10pt','width':'90mm'"))));
|
||||
|
||||
Map<String, Object> shrunk = findings(SceneFixture.request("RENDER", text(value + ",'size':'10pt','width':'60mm'")), "TEXT_SHRUNK").get(0);
|
||||
assertEquals("INFO", shrunk.get("severity"));
|
||||
assertEquals(List.of(0, 0, 479, 63), shrunk.get("box"));
|
||||
assertEquals(Map.of("sizeCentipoints", 1000, "floorCentipoints", 700), shrunk.get("limit"));
|
||||
int chosen = (int) ((Map<?, ?>) shrunk.get("measured")).get("sizeCentipoints");
|
||||
assertTrue(chosen < 1000 && chosen >= 700 && chosen % 25 == 0, "a quarter-point step: " + chosen);
|
||||
assertEquals(findings(SceneFixture.request("RENDER", text(value + ",'size':'10pt','width':'60mm'"))), findings(SceneFixture.request("VALIDATE", text(value + ",'size':'10pt','width':'60mm'"))));
|
||||
|
||||
Map<String, Object> refusal = refused(SceneFixture.request("RENDER", text(value + ",'size':'10pt','width':'40mm'")));
|
||||
assertEquals("TEXT_DOES_NOT_FIT", refusal.get("code"));
|
||||
assertEquals(List.of(0, 0, 319, 63), refusal.get("box"));
|
||||
assertEquals(Map.of("widthDots", 319, "heightDots", 63, "lines", 1), refusal.get("limit"));
|
||||
Map<?, ?> measured = (Map<?, ?>) refusal.get("measured");
|
||||
assertEquals(List.of(700, 30, 2), List.of(measured.get("sizeCentipoints"), measured.get("characters"), measured.get("lines")));
|
||||
assertEquals(refusal, refused(SceneFixture.request("VALIDATE", text(value + ",'size':'10pt','width':'40mm'"))));
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// the floor is 70% of the size, 5 pt when that is less, and the size itself when the //
|
||||
// size is under 5 pt; a stated floor is used as it is //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////
|
||||
for(int[] sizes : new int[][] { {10, 700}, {8, 560}, {7, 500}, {6, 500}, {4, 400}})
|
||||
{
|
||||
Map<String, Object> small = refused(SceneFixture.request("RENDER", text(value + ",'size':'" + sizes[0] + "pt','width':'10mm'")));
|
||||
assertEquals(sizes[1], ((Map<?, ?>) small.get("measured")).get("sizeCentipoints"), sizes[0] + " pt");
|
||||
}
|
||||
Map<String, Object> stated = refused(SceneFixture.request("RENDER", text(value + ",'size':'10pt','width':'10mm','fit':{'shrinkTo':'1mm'}")));
|
||||
assertEquals(283, ((Map<?, ?>) stated.get("measured")).get("sizeCentipoints"), "1 mm is 2.835 pt");
|
||||
assertEquals("TEXT_DOES_NOT_FIT", refused(SceneFixture.request("RENDER", text(value + ",'size':'10pt','width':'60mm','fit':{'shrinkTo':'10pt'}"))).get("code"), "a floor at the size allows no shrinking");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** The fit an element may choose instead: more lines, and cutting with an
|
||||
** ellipsis. Each is reported: lines as information, a cut as a warning with
|
||||
** how many characters are shown.
|
||||
***************************************************************************/
|
||||
@Test
|
||||
void wrappingAndTruncationAreChosenByTheElementAndReported() throws Exception
|
||||
{
|
||||
String value = "'value':'(01)00012345678905(10)L240611A-RETURNED-000123','font':'qqq-mono-bold','size':'10pt','width':'45mm','height':'12mm'";
|
||||
assertEquals("TEXT_DOES_NOT_FIT", refused(SceneFixture.request("RENDER", text(value))).get("code"));
|
||||
|
||||
List<Map<String, Object>> wrapped = findings(SceneFixture.request("RENDER", text(value + ",'fit':{'maxLines':2}")));
|
||||
assertEquals(List.of("TEXT_SHRUNK", "TEXT_WRAPPED"), wrapped.stream().map(finding -> finding.get("code")).toList());
|
||||
assertEquals(Map.of("code", "TEXT_WRAPPED", "severity", "INFO", "element", "t", "box", List.of(0, 0, 359, 95), "measured", Map.of("lines", 2), "limit", Map.of("lines", 2)), wrapped.get(1));
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// the line height is the distance between baselines as a multiple of the size. The text //
|
||||
// is shrunk to 9 pt: at 1 its lines are 26 dots apart, where the face's own 1.32 is 34 //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////
|
||||
List<Integer> natural = ink(page(SceneFixture.request("RENDER", text(value + ",'fit':{'maxLines':2}"))));
|
||||
List<Integer> tight = ink(page(SceneFixture.request("RENDER", text(value + ",'fit':{'maxLines':2},'lineHeight':1"))));
|
||||
assertEquals(List.of(natural.get(0), natural.get(1), natural.get(2), natural.get(3) - 8), tight.subList(0, 4));
|
||||
assertEquals(tight, ink(page(SceneFixture.request("RENDER", text(value + ",'fit':{'maxLines':2},'lineHeight':1.000")))));
|
||||
assertEquals(natural.get(3) - 5, ink(page(SceneFixture.request("RENDER", text(value + ",'fit':{'maxLines':2},'lineHeight':1.125")))).get(3), "9 pt x 1.125 is 28.5 dots, drawn 29 apart");
|
||||
|
||||
List<Map<String, Object>> cut = findings(SceneFixture.request("RENDER", text(value + ",'fit':{'overflow':'truncate'}")));
|
||||
assertEquals(List.of("TEXT_SHRUNK", "TEXT_TRUNCATED"), cut.stream().map(finding -> finding.get("code")).toList(), "a text is cut at the floor size, so it is shrunk as well");
|
||||
assertEquals(Map.of("sizeCentipoints", 700), cut.get(0).get("measured"));
|
||||
assertEquals("WARNING", cut.get(1).get("severity"));
|
||||
assertEquals(Map.of("lines", 1), cut.get(1).get("limit"));
|
||||
Map<?, ?> measured = (Map<?, ?>) cut.get(1).get("measured");
|
||||
assertEquals(List.of(46, 1), List.of(measured.get("characters"), measured.get("lines")));
|
||||
assertTrue((int) measured.get("shownCharacters") > 20 && (int) measured.get("shownCharacters") < 46, "shown " + measured);
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// cut, the line ends in the three dots of the ellipsis, which the whole value does not: //
|
||||
// the last ink is lower than the capitals are and near the right of the box //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////
|
||||
boolean[][] page = page(SceneFixture.request("RENDER", text(value + ",'fit':{'overflow':'truncate'},'align':'right'")));
|
||||
List<Integer> ink = ink(page);
|
||||
assertTrue(ink.get(2) <= 24 + 359 && ink.get(2) >= 24 + 359 - 6, "against the right of the box: " + ink);
|
||||
int column = ink.get(2) - 1;
|
||||
int first = 0;
|
||||
while(!page[first][column])
|
||||
{
|
||||
first++;
|
||||
}
|
||||
assertTrue(first > ink.get(1) + (ink.get(3) - ink.get(1)) / 2, "the last character is dots on the baseline");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Small type is warned about, by the height of its capitals in dots at the
|
||||
** render target: any face under 12 dots, and a regular weight under 20. A
|
||||
** resolution override is how a scene answers: a heavier face or a higher
|
||||
** threshold for the coarse printer only.
|
||||
***************************************************************************/
|
||||
@Test
|
||||
void smallTypeIsWarnedAboutAndAnOverrideAnswersForOneResolution() throws Exception
|
||||
{
|
||||
String small = text("'value':'Lot 0O1lI','font':'qqq-sans','size':'5.5pt'");
|
||||
Map<String, Object> tiny = Map.of("code", "TEXT_SMALL_FOR_DPI", "severity", "WARNING", "element", "t", "box", List.of(0, 0, 764, 155), "measured", Map.of("capHeightDots", 10, "sizeCentipoints", 550), "limit", Map.of("capHeightDots", 12));
|
||||
Map<String, Object> light = Map.of("code", "TEXT_LIGHT_FACE", "severity", "WARNING", "element", "t", "box", List.of(0, 0, 764, 155), "measured", Map.of("capHeightDots", 10, "sizeCentipoints", 550), "limit", Map.of("capHeightDots", 20));
|
||||
assertEquals(List.of(tiny, light), findings(SceneFixture.request("RENDER", small)));
|
||||
assertEquals(List.of(tiny, light), findings(SceneFixture.request("VALIDATE", small)));
|
||||
assertEquals(List.of("TEXT_LIGHT_FACE"), findings(SceneFixture.request("RENDER", text("'value':'Lot 0O1lI','font':'qqq-mono','size':'9pt'"))).stream().map(finding -> finding.get("code")).toList());
|
||||
assertEquals(List.of("TEXT_SMALL_FOR_DPI"), findings(SceneFixture.request("RENDER", text("'value':'Lot 0O1lI','font':'qqq-mono-bold','size':'5.5pt'"))).stream().map(finding -> finding.get("code")).toList());
|
||||
assertEquals(List.of(), findings(SceneFixture.request("RENDER", text("'value':'Lot 0O1lI','font':'qqq-sans','size':'11pt'"))));
|
||||
assertEquals(List.of(), findings(SceneFixture.request("RENDER", small).put("targetDpi", 600)), "the same type is 31 dots high at 600 dpi");
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// a bound element is judged at its stated size when validated, and at the size it was //
|
||||
// shrunk to when rendered //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////
|
||||
String shrinking = text("'valuePath':'/record/name','font':'qqq-sans-bold','size':'8pt','width':'28mm','height':'5mm','fit':{'shrinkTo':'5pt'}");
|
||||
assertEquals(List.of("FIT_NOT_PROVEN"), findings(SceneFixture.request("VALIDATE", shrinking)).stream().map(finding -> finding.get("code")).toList());
|
||||
assertEquals(List.of("FIT_NOT_PROVEN", "TEXT_SHRUNK", "TEXT_SMALL_FOR_DPI"), findings(bound("RENDER", shrinking, "L240611A-RETURNED-000123")).stream().map(finding -> finding.get("code")).toList());
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// the override: bold and a heavier threshold at 203 dpi only //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////
|
||||
String plain = json(HEAD + "{'id':'t','type':'text','value':'Lot 0O1lI','font':'qqq-sans','size':'8pt'}]}");
|
||||
String heavier = json(HEAD + "{'id':'t','type':'text','value':'Lot 0O1lI','font':'qqq-sans','size':'8pt'}],'overrides':[{'when':{'dpi':[203]},'set':{'t':{'threshold':200}}}]}");
|
||||
String bolder = json(HEAD + "{'id':'t','type':'text','value':'Lot 0O1lI','font':'qqq-sans','size':'8pt'}],'overrides':[{'when':{'dpi':[203]},'set':{'t':{'font':'qqq-sans-bold'}}}]}");
|
||||
int ink = ink(page(SceneFixture.request("RENDER", plain))).get(4);
|
||||
assertTrue(ink(page(SceneFixture.request("RENDER", heavier))).get(4) > ink * 23 / 20, "a threshold of 200 prints heavier than 128");
|
||||
assertTrue(ink(page(SceneFixture.request("RENDER", bolder))).get(4) > ink * 23 / 20, "the bold face prints heavier than the regular");
|
||||
assertEquals(List.of("TEXT_LIGHT_FACE"), findings(SceneFixture.request("RENDER", heavier)).stream().map(finding -> finding.get("code")).toList());
|
||||
assertEquals(List.of(), findings(SceneFixture.request("RENDER", bolder)));
|
||||
for(String scene : List.of(heavier, bolder))
|
||||
{
|
||||
assertTrue(Arrays.deepEquals(page(SceneFixture.request("RENDER", plain).put("targetDpi", 300)), page(SceneFixture.request("RENDER", scene).put("targetDpi", 300))), "at 300 dpi the override does not apply");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Reverse text is white letters on its own box filled black: the picture is
|
||||
** the plain one with every dot of the box reversed. It is counted twice
|
||||
** against what one scene may paint, since it paints the box and then the
|
||||
** letters.
|
||||
***************************************************************************/
|
||||
@Test
|
||||
void reverseTextIsWhiteOnItsOwnBlackBox() throws Exception
|
||||
{
|
||||
String element = "'value':'ZONE 7','size':'14pt','x':'5mm','y':'3mm','width':'30mm','height':'8mm','align':'center','valign':'middle'";
|
||||
boolean[][] plain = page(SceneFixture.request("RENDER", text(element)));
|
||||
boolean[][] reverse = page(SceneFixture.request("RENDER", text(element + ",'reverse':true")));
|
||||
assertTrue(ink(plain).get(4) > 800);
|
||||
for(int y = 0; y < plain.length; y++)
|
||||
{
|
||||
for(int x = 0; x < plain[y].length; x++)
|
||||
{
|
||||
boolean inside = x >= 24 + 40 && x < 24 + 40 + 239 && y >= 24 + 24 && y < 24 + 24 + 63;
|
||||
assertEquals(inside != plain[y][x], reverse[y][x], "dot " + x + ", " + y);
|
||||
assertFalse(plain[y][x] && !inside, "plain ink outside the box at " + x + ", " + y);
|
||||
}
|
||||
}
|
||||
assertEquals(List.of(), findings(SceneFixture.request("RENDER", text(element + ",'reverse':true"))));
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// sixteen plain texts the size of the label are sixteen coats and are allowed; eight //
|
||||
// reverse ones are too, and a ninth is refused as painting 1800% of the label //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////
|
||||
StringBuilder plainTexts = new StringBuilder();
|
||||
StringBuilder reverseTexts = new StringBuilder();
|
||||
for(int index = 0; index < 16; index++)
|
||||
{
|
||||
plainTexts.append(index == 0 ? "" : ",").append("{'id':'p").append(index).append("','type':'text','value':'A'}");
|
||||
}
|
||||
for(int index = 0; index < 9; index++)
|
||||
{
|
||||
reverseTexts.append(index == 0 ? "" : ",").append("{'id':'r").append(index).append("','type':'text','value':'A','reverse':true}");
|
||||
}
|
||||
assertEquals("OK", RendererMain.process(SceneFixture.request("VALIDATE", scene(plainTexts.toString()))).get("outcome"));
|
||||
assertEquals(Map.of("code", "SCENE_INVALID", "severity", "ERROR", "element", "r8", "pointer", "/elements/8", "box", List.of(0, 0, 764, 155), "measured", Map.of("paintedPercent", 1800), "limit", Map.of("paintedPercent", 1600)),
|
||||
refused(SceneFixture.request("VALIDATE", scene(reverseTexts.toString()))));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Rotation turns the text clockwise in a box given as it appears on the
|
||||
** label: turned a quarter, the line runs down the box, is fitted to the
|
||||
** box's height and aligned along it.
|
||||
***************************************************************************/
|
||||
@Test
|
||||
void rotationTurnsTheTextInABoxGivenAsItAppears() throws Exception
|
||||
{
|
||||
String element = "'value':'TEST-001','font':'qqq-mono-bold','size':'10pt','x':'10mm','width':'6mm','height':'19mm'";
|
||||
assertEquals("TEXT_DOES_NOT_FIT", refused(SceneFixture.request("RENDER", text(element + ",'fit':{'shrinkTo':'10pt'}"))).get("code"), "upright, eight letters do not fit 6 mm");
|
||||
for(int rotation : List.of(90, 270))
|
||||
{
|
||||
List<Integer> ink = ink(page(SceneFixture.request("RENDER", text(element + ",'fit':{'shrinkTo':'10pt'},'rotation':" + rotation))));
|
||||
assertTrue(ink.get(0) >= 24 + 80 && ink.get(2) <= 24 + 80 + 47 && ink.get(1) >= 24 && ink.get(3) <= 24 + 151, rotation + ": " + ink);
|
||||
assertTrue(ink.get(3) - ink.get(1) > 3 * (ink.get(2) - ink.get(0)), "the line runs down the box: " + ink);
|
||||
assertEquals(rotation == 90, ink.get(1) < 24 + 4, "its start is at the top when turned clockwise, at the bottom when turned the other way: " + ink);
|
||||
}
|
||||
List<Integer> upright = ink(page(SceneFixture.request("RENDER", text("'value':'TEST-0001','font':'qqq-mono-bold','size':'10pt','width':'40mm','height':'6mm'"))));
|
||||
List<Integer> turned = ink(page(SceneFixture.request("RENDER", text("'value':'TEST-0001','font':'qqq-mono-bold','size':'10pt','width':'40mm','height':'6mm','rotation':180"))));
|
||||
assertEquals(upright.get(4), turned.get(4));
|
||||
assertEquals(List.of(24 + 319 - (upright.get(2) - 24), 24 + 47 - (upright.get(3) - 24)), List.of(turned.get(0), turned.get(1)), "the same picture turned about the middle of the box");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** The prefix `qqq-` belongs to the bundled faces. A request that supplies an
|
||||
** asset under that prefix could shadow one, so a text element refuses it
|
||||
** with the count, whatever face the element itself names.
|
||||
***************************************************************************/
|
||||
@Test
|
||||
void aSuppliedAssetWithTheReservedPrefixIsRefused() throws Exception
|
||||
{
|
||||
byte[] font;
|
||||
try(InputStream stream = Face.class.getResourceAsStream("/fonts/" + Face.MONO.file()))
|
||||
{
|
||||
font = stream.readAllBytes();
|
||||
}
|
||||
String source = text("'value':'TEST-0001','font':'qqq-sans-bold'");
|
||||
for(String name : List.of("qqq-sans-bold", "qqq-brand"))
|
||||
{
|
||||
ObjectNode request = SceneFixture.request("RENDER", source);
|
||||
((ArrayNode) request.get("assets")).addObject().put("name", name).put("mediaType", "font/ttf").put("base64", Base64.getEncoder().encodeToString(font)).put("sha256", Wire.sha256(font)).put("fontFamily", "Brand");
|
||||
assertEquals(Map.of("code", "RESERVED_FONT_NAME", "severity", "ERROR", "element", "t", "box", List.of(0, 0, 764, 155), "measured", Map.of("reservedAssets", 1), "limit", Map.of("reservedAssets", 0)), refused(request), name);
|
||||
request.put("operation", "VALIDATE");
|
||||
assertEquals("RESERVED_FONT_NAME", refused(request).get("code"));
|
||||
}
|
||||
ObjectNode other = SceneFixture.request("RENDER", source);
|
||||
((ArrayNode) other.get("assets")).addObject().put("name", "brand-sans").put("mediaType", "font/ttf").put("base64", Base64.getEncoder().encodeToString(font)).put("sha256", Wire.sha256(font)).put("fontFamily", "Brand");
|
||||
assertEquals(List.of(), findings(other));
|
||||
assertEquals("FONT_UNKNOWN", refused(SceneFixture.request("RENDER", text("'value':'TEST-0001','font':'brand-sans'"))).get("code"), "a supplied font is not a face of version 2 text yet");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** No answer and no exception carries a value: the record holds a marker,
|
||||
** the element is refused in every way a value can cause, with and without
|
||||
** collecting, and the marker is nowhere in what comes back.
|
||||
***************************************************************************/
|
||||
@Test
|
||||
void noRefusalCarriesTheValue() throws Exception
|
||||
{
|
||||
String marker = "Jane-Example-0012345";
|
||||
List<String> elements = List.of("'valuePath':'/record/name','maxLength':8", "'valuePath':'/record/name','width':'10mm'", "'valuePath':'/record/name','width':'10mm','fit':{'overflow':'truncate','maxLines':2}",
|
||||
"'valuePath':'/record/name','font':'qqq-mono','size':'5pt'", "'valuePath':'/record/missing'", "'valuePath':'/record/name','rotation':90,'reverse':true,'width':'4mm'");
|
||||
for(String element : elements)
|
||||
{
|
||||
for(String value : List.of(marker, marker + "\t" + marker, marker + "\u4E2D")) // a CJK ideograph
|
||||
{
|
||||
for(boolean collect : List.of(false, true))
|
||||
{
|
||||
ObjectNode request = bound("RENDER", text(element), value);
|
||||
if(collect)
|
||||
{
|
||||
request.put("diagnostics", "COLLECT");
|
||||
}
|
||||
String answer;
|
||||
try
|
||||
{
|
||||
Map<String, Object> answered = new LinkedHashMap<>(RendererMain.process(request));
|
||||
answered.remove("base64");
|
||||
answer = Wire.JSON.writeValueAsString(answered);
|
||||
}
|
||||
catch(Refusal refusal)
|
||||
{
|
||||
answer = refusal.getMessage() + " " + refusal + " " + Wire.JSON.writeValueAsString(refusal.diagnostic().wire());
|
||||
}
|
||||
assertFalse(answer.contains("Jane") || answer.contains("0012345"), element + ": " + answer);
|
||||
}
|
||||
}
|
||||
}
|
||||
assertThrows(Refusal.class, () -> RendererMain.process(bound("RENDER", text(elements.get(0)), marker)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,600 @@
|
||||
/*
|
||||
* 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.text;
|
||||
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
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 fit rules, by example and as properties over seeded, deterministic
|
||||
** generators (fixed seeds, no clock): text, face, box, size, floor, lines and
|
||||
** resolution.
|
||||
*******************************************************************************/
|
||||
class TextFitTest
|
||||
{
|
||||
static final long[] SEEDS = {20261112L, 7L, 4242L};
|
||||
static final int CASES = 600;
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** One generated case.
|
||||
***************************************************************************/
|
||||
record Case(TextFit.Request request, String text)
|
||||
{
|
||||
/************************************************************************
|
||||
** The same case with another nominal size and floor, and no cutting.
|
||||
************************************************************************/
|
||||
Case at(int nominal, int floor)
|
||||
{
|
||||
TextFit.Request from = request;
|
||||
return (new Case(new TextFit.Request(from.face(), from.width(), from.height(), from.dpi(), nominal, floor, from.lineHeight(), from.maxLines(), false), text));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/************************************************************************
|
||||
** The same case for another text.
|
||||
************************************************************************/
|
||||
Case of(String other)
|
||||
{
|
||||
return (new Case(request, other));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/************************************************************************
|
||||
** Fit it.
|
||||
************************************************************************/
|
||||
TextFit.Block fit()
|
||||
{
|
||||
return (TextFit.fit(request, request.face().glyphs(text), request.face().glyph(TextFit.MARK)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** A request in a box, with the face's own line pitch, one line, no cutting.
|
||||
***************************************************************************/
|
||||
static TextFit.Request request(Face face, int width, int height, int nominal, int floor)
|
||||
{
|
||||
return (new TextFit.Request(face, width, height, 203, nominal, floor, null, 1, false));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** The same request with more lines, and cutting or not.
|
||||
***************************************************************************/
|
||||
static TextFit.Request lines(TextFit.Request request, int maxLines, boolean truncate)
|
||||
{
|
||||
return (new TextFit.Request(request.face(), request.width(), request.height(), request.dpi(), request.nominal(), request.floor(), request.lineHeight(), maxLines, truncate));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Fit a text of a face's own characters.
|
||||
***************************************************************************/
|
||||
static TextFit.Block fit(TextFit.Request request, String text)
|
||||
{
|
||||
return (new Case(request, text).fit());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** The characters of each line, with `~` where the ellipsis is.
|
||||
***************************************************************************/
|
||||
static List<String> shown(TextFit.Block block, String text)
|
||||
{
|
||||
return (block.lines().stream().map(line -> text.substring(line.from(), line.to()) + (line.marked() ? "~" : "")).toList());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** A generated case: labels are small boxes, short values and type from 5 to
|
||||
** 30 pt. A third of the texts have no space, as identifiers do.
|
||||
***************************************************************************/
|
||||
static Case generate(Random random)
|
||||
{
|
||||
Face face = Face.all().get(random.nextInt(Face.all().size()));
|
||||
int dpi = Scene2.RESOLUTIONS.get(random.nextInt(3));
|
||||
int nominal = 5000 + random.nextInt(101) * 250 + (random.nextInt(4) == 0 ? random.nextInt(250) : 0);
|
||||
int floor = switch(random.nextInt(4))
|
||||
{
|
||||
case 0 -> nominal;
|
||||
case 1 -> Math.max(1000, nominal * 7 / 10);
|
||||
default -> 1000 + random.nextInt(nominal - 999);
|
||||
};
|
||||
Integer lineHeight = random.nextInt(3) == 0 ? Integer.valueOf(700 + random.nextInt(1500)) : null;
|
||||
int width = (20 + random.nextInt(700)) * dpi / 203;
|
||||
int height = (8 + random.nextInt(160)) * dpi / 203;
|
||||
String alphabet = switch(random.nextInt(3))
|
||||
{
|
||||
case 0 -> "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-()/.";
|
||||
case 1 -> "abcdefghijklmnopqrstuvwxyzWMfjgyÅé‰ "; // A ring, e acute, per mille
|
||||
default -> "il1.,' WM@‰⁄ǺĢ "; // per mille, fraction slash, A ring acute, G cedilla: the widest and furthest-reaching
|
||||
};
|
||||
StringBuilder text = new StringBuilder();
|
||||
int length = random.nextInt(48);
|
||||
boolean spaces = random.nextInt(3) != 0;
|
||||
for(int index = 0; index < length; index++)
|
||||
{
|
||||
text.append(spaces && random.nextInt(6) == 0 ? ' ' : alphabet.charAt(random.nextInt(alphabet.length())));
|
||||
}
|
||||
return (new Case(new TextFit.Request(face, width, height, dpi, nominal, floor, lineHeight, 1 + random.nextInt(4), random.nextInt(3) == 0), text.toString()));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Dots are thousandths of the em times the size times the resolution over
|
||||
** 72 000 000, rounded down, up or to the nearest: checked against exact
|
||||
** decimal arithmetic, negative lengths included.
|
||||
***************************************************************************/
|
||||
@Test
|
||||
void theScaleRoundsExactly()
|
||||
{
|
||||
Random random = new Random(SEEDS[0]);
|
||||
for(int index = 0; index < 5000; index++)
|
||||
{
|
||||
TextFit.Scale scale = new TextFit.Scale(1000 + random.nextInt(999_001), Scene2.RESOLUTIONS.get(random.nextInt(3)));
|
||||
long units = random.nextInt(3_000_001) - 1_500_000L;
|
||||
BigDecimal exact = BigDecimal.valueOf(units).multiply(BigDecimal.valueOf(scale.milliPoints())).multiply(BigDecimal.valueOf(scale.dpi())).divide(BigDecimal.valueOf(72_000_000L), 12, RoundingMode.HALF_EVEN);
|
||||
assertEquals(exact.setScale(0, RoundingMode.FLOOR).intValueExact(), scale.floor(units));
|
||||
assertEquals(exact.setScale(0, RoundingMode.CEILING).intValueExact(), scale.ceil(units));
|
||||
assertEquals(exact.add(new BigDecimal("0.5")).setScale(0, RoundingMode.FLOOR).intValueExact(), scale.round(units));
|
||||
}
|
||||
assertEquals(25.375f, new TextFit.Scale(9000, 203).size());
|
||||
assertEquals(100f, new TextFit.Scale(12_000, 600).size());
|
||||
assertEquals(List.of(25, 26, 25), List.of(new TextFit.Scale(9000, 203).floor(1000), new TextFit.Scale(9000, 203).ceil(1000), new TextFit.Scale(9000, 203).round(1000)));
|
||||
assertEquals(List.of(-26, -25, -25), List.of(new TextFit.Scale(9000, 203).floor(-1000), new TextFit.Scale(9000, 203).ceil(-1000), new TextFit.Scale(9000, 203).round(-1000)));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** The default: one line. A text that fits at its size keeps it; one that
|
||||
** does not is reduced a quarter point at a time to the largest size that
|
||||
** fits; one that does not fit at the floor is refused with what it
|
||||
** measured there.
|
||||
***************************************************************************/
|
||||
@Test
|
||||
void oneLineShrinksToTheLargestSizeThatFitsThenRefuses()
|
||||
{
|
||||
////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// 9 pt at 203 dpi is 25.375 dots to the em and the monospace advances 0.6 em: 12 letters //
|
||||
// are 183 dots, and the line is 2 dots wider on each inked side //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////
|
||||
TextFit.Block fits = fit(request(Face.MONO_BOLD, 200, 40, 9000, 6300), "ABCDEFGHIJKL");
|
||||
assertTrue(fits.fits() && !fits.truncated());
|
||||
assertEquals(9000, fits.milliPoints());
|
||||
assertEquals(List.of("ABCDEFGHIJKL"), shown(fits, "ABCDEFGHIJKL"));
|
||||
assertEquals(12, fits.characters());
|
||||
assertTrue(fits.width() >= 183 && fits.width() <= 187, "width " + fits.width());
|
||||
assertEquals(fits.width(), fits.lines().get(0).width());
|
||||
assertEquals(List.of(26, 34), List.of(fits.baseline(), fits.height()), "ascent 1.02 em and descent 0.3 em, each rounded up");
|
||||
|
||||
TextFit.Block shrunk = fit(request(Face.MONO_BOLD, 160, 40, 9000, 6300), "ABCDEFGHIJKL");
|
||||
assertTrue(shrunk.fits() && shrunk.milliPoints() < 9000 && shrunk.milliPoints() >= 6300 && shrunk.milliPoints() % 250 == 0);
|
||||
assertTrue(shrunk.width() <= 160);
|
||||
assertFalse(fit(request(Face.MONO_BOLD, 160, 40, shrunk.milliPoints() + 250, shrunk.milliPoints() + 250), "ABCDEFGHIJKL").fits(), "a quarter point larger does not fit");
|
||||
|
||||
TextFit.Block refused = fit(request(Face.MONO_BOLD, 100, 40, 9000, 6300), "ABCDEFGHIJKL");
|
||||
assertFalse(refused.fits());
|
||||
assertEquals(6300, refused.milliPoints());
|
||||
assertTrue(refused.lines().size() > 1 && refused.width() <= 100, "measured broken at any character: " + refused);
|
||||
assertEquals(12, refused.shown());
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// the floor need not be on the quarter-point grid from the size: it is the last size //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////
|
||||
TextFit.Block floor = fit(request(Face.MONO_BOLD, 134, 40, 9000, 6400), "ABCDEFGHIJKL");
|
||||
assertTrue(floor.fits());
|
||||
assertEquals(6400, floor.milliPoints());
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// too tall is refused as too wide is: the box is 20 dots high and the floor needs 25 //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////
|
||||
TextFit.Block tall = fit(request(Face.MONO_BOLD, 400, 20, 9000, 6300), "ABC");
|
||||
assertFalse(tall.fits());
|
||||
assertTrue(tall.height() > 20 && tall.lines().size() == 1);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Nothing to draw always fits: an empty value, or one of spaces only.
|
||||
** Spaces at the end of a text take no room; spaces at its start do.
|
||||
***************************************************************************/
|
||||
@Test
|
||||
void spacesAtTheEndTakeNoRoom()
|
||||
{
|
||||
for(String blank : List.of("", " ", " "))
|
||||
{
|
||||
TextFit.Block block = fit(request(Face.SANS, 1, 1, 9000, 9000), blank);
|
||||
assertTrue(block.fits() && block.lines().isEmpty() && !block.truncated());
|
||||
assertEquals(List.of(0, 0, 0, 0), List.of(block.width(), block.height(), block.characters(), block.shown()));
|
||||
}
|
||||
TextFit.Request request = request(Face.SANS_BOLD, 300, 40, 9000, 9000);
|
||||
TextFit.Block plain = fit(request, "AB");
|
||||
assertEquals(plain, fit(request, "AB "));
|
||||
TextFit.Block led = fit(request, " AB");
|
||||
int shift = new TextFit.Scale(9000, 203).round(2L * Face.SANS_BOLD.glyph(' ').advance());
|
||||
assertTrue(shift > 8 && led.width() >= plain.width() + shift - 2 * TextFit.SLOP && led.width() <= plain.width() + shift + 1, plain.width() + " then " + led.width());
|
||||
assertEquals(4, led.characters());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** With more lines allowed, a line is broken between words while that fits,
|
||||
** and at any character when it does not: a larger size is preferred to
|
||||
** whole words, and a value with no space wraps.
|
||||
***************************************************************************/
|
||||
@Test
|
||||
void linesBreakBetweenWordsAndThenAtAnyCharacter()
|
||||
{
|
||||
////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// the monospace at 9 pt, 203 dpi advances 15.225 dots: a 100 dot box holds 6 letters //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////
|
||||
TextFit.Request box = request(Face.MONO, 100, 200, 9000, 9000);
|
||||
assertEquals(List.of("AAA", "BBB", "CCC"), shown(fit(lines(box, 3, false), "AAA BBB CCC"), "AAA BBB CCC"));
|
||||
assertEquals(List.of("AAA BB", "CCC"), shown(fit(lines(box, 2, false), "AAA BB CCC"), "AAA BB CCC"), "spaces at a break are dropped");
|
||||
assertEquals(List.of("AAA BB", "B CCC"), shown(fit(lines(box, 2, false), "AAA BBB CCC"), "AAA BBB CCC"), "two lines only when broken at any character");
|
||||
assertEquals(List.of("L24061", "1A-RET", "URNED"), shown(fit(lines(box, 3, false), "L240611A-RETURNED"), "L240611A-RETURNED"));
|
||||
assertEquals(9000, fit(lines(box, 2, false), "AAA BBB CCC").milliPoints());
|
||||
|
||||
TextFit.Block refused = fit(lines(box, 2, false), "L240611A-RETURNED");
|
||||
assertFalse(refused.fits());
|
||||
assertEquals(3, refused.lines().size());
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// a stated line height sets the distance between baselines; the block is one ascent, //
|
||||
// the pitches between, and one descent //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////
|
||||
TextFit.Block natural = fit(lines(box, 3, false), "AAA BBB CCC");
|
||||
assertEquals(List.of(34, 26, 26 + 2 * 34 + 8), List.of(natural.pitch(), natural.baseline(), natural.height()));
|
||||
TextFit.Block tight = TextFit.fit(new TextFit.Request(Face.MONO, 100, 200, 203, 9000, 9000, 1000, 3, false), Face.MONO.glyphs("AAA BBB CCC"), null);
|
||||
assertEquals(List.of(26, 26, 26 + 2 * 26 + 8), List.of(tight.pitch(), tight.baseline(), tight.height()));
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// a character wider than the box stands alone on a line that does not fit //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////
|
||||
TextFit.Block narrow = fit(lines(request(Face.MONO, 10, 200, 9000, 9000), 4, false), "AB");
|
||||
assertFalse(narrow.fits());
|
||||
assertEquals(List.of("A", "B"), shown(narrow, "AB"));
|
||||
assertTrue(narrow.width() > 10);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Cutting happens only when the element asked for it and only when the text
|
||||
** fits at no size down to the floor. The last line that fits ends with the
|
||||
** ellipsis after as many characters as fit beside it; when not even the
|
||||
** ellipsis fits, the text is refused.
|
||||
***************************************************************************/
|
||||
@Test
|
||||
void truncationIsOnlyWhenAskedAndAlwaysShowsTheMark()
|
||||
{
|
||||
String text = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
TextFit.Request box = request(Face.MONO, 100, 80, 9000, 9000);
|
||||
assertFalse(fit(box, text).fits());
|
||||
assertFalse(fit(lines(box, 2, false), text).truncated());
|
||||
|
||||
TextFit.Block cut = fit(lines(box, 1, true), text);
|
||||
assertTrue(cut.fits() && cut.truncated());
|
||||
assertEquals(List.of("ABCDE~"), shown(cut, text));
|
||||
assertEquals(List.of(26, 5), List.of(cut.characters(), cut.shown()));
|
||||
assertTrue(cut.width() <= 100);
|
||||
|
||||
TextFit.Block two = fit(lines(box, 2, true), text);
|
||||
assertEquals(List.of("ABCDEF", "GHIJK~"), shown(two, text));
|
||||
assertEquals(11, two.shown());
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// the box is high enough for one line only: the second is given up for the mark //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////
|
||||
TextFit.Block low = fit(lines(request(Face.MONO, 100, 40, 9000, 9000), 3, true), text);
|
||||
assertEquals(List.of("ABCDE~"), shown(low, text));
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// a text that fits is not cut and has no mark, though cutting is allowed; one that fits //
|
||||
// after shrinking is shrunk, not cut //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////
|
||||
assertEquals(List.of("ABCDEF"), shown(fit(lines(box, 1, true), "ABCDEF"), "ABCDEF"));
|
||||
TextFit.Block shrunk = fit(lines(request(Face.MONO, 100, 80, 9000, 6000), 1, true), "ABCDEFGH");
|
||||
assertTrue(shrunk.fits() && !shrunk.truncated() && shrunk.milliPoints() < 9000);
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// spaces before the mark are dropped; the mark may stand alone; and where it cannot //
|
||||
// stand at all, or a line is too high, the answer is the refusal //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////
|
||||
assertEquals(List.of("ABC~"), shown(fit(lines(box, 1, true), "ABC DEFGHIJKL"), "ABC DEFGHIJKL"));
|
||||
assertEquals(List.of("~"), shown(fit(lines(request(Face.MONO, 22, 80, 9000, 9000), 1, true), text), text));
|
||||
assertFalse(fit(lines(request(Face.MONO, 12, 80, 9000, 9000), 1, true), text).fits());
|
||||
assertFalse(fit(lines(request(Face.MONO, 100, 20, 9000, 9000), 1, true), text).fits());
|
||||
assertFalse(fit(lines(request(Face.MONO, 400, 20, 9000, 9000), 1, true), "ABC").fits(), "a line that is too high is too high with the mark");
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// the last line of a text is cut too when the text fits at no size: here one letter is //
|
||||
// wider than the box and the mark is not, and here an accent above the ascent is too //
|
||||
// high for the box and is given up for the mark //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////
|
||||
assertEquals(List.of("~"), shown(fit(lines(request(Face.SANS, 26, 80, 9000, 9000), 1, true), "\u2030"), "\u2030")); // per mille
|
||||
TextFit.Request shallow = lines(request(Face.SANS_BOLD, 200, 36, 9000, 9000), 1, true);
|
||||
assertTrue(fit(shallow, "ABC").fits() && !fit(shallow, "ABC").truncated());
|
||||
assertEquals(List.of("AB~"), shown(fit(shallow, "AB\u01FA"), "AB\u01FA")); // A ring acute
|
||||
assertFalse(fit(lines(shallow, 1, false), "AB\u01FA").fits()); // A ring acute
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// broken at any character, spaces at the start of a text stay on a line of their own //
|
||||
// rather than push the first letter out of the box //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////
|
||||
assertEquals(List.of(" ", "ABCDE"), shown(fit(lines(box, 2, false), " ABCDE"), " ABCDE"));
|
||||
assertEquals(List.of(" ABCD", "E"), shown(fit(lines(box, 2, false), " ABCDE"), " ABCDE"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Property: the answer is a layout inside the box or a refusal whose
|
||||
** measurement exceeds its limit. The size is never above the nominal and
|
||||
** never below the floor; it is on the quarter-point steps from the nominal
|
||||
** or is the floor. The lines are the text in order, whole unless cut, and
|
||||
** never more than allowed. A text is cut only when asked, only at the
|
||||
** floor, and then its last line carries the mark.
|
||||
***************************************************************************/
|
||||
@Test
|
||||
void theAnswerIsALayoutInsideTheBoxOrARefusalThatMeasuresMore()
|
||||
{
|
||||
int fitted = 0;
|
||||
int refused = 0;
|
||||
int cut = 0;
|
||||
for(long seed : SEEDS)
|
||||
{
|
||||
Random random = new Random(seed);
|
||||
for(int index = 0; index < CASES; index++)
|
||||
{
|
||||
Case generated = generate(random);
|
||||
TextFit.Request request = generated.request();
|
||||
TextFit.Block block = generated.fit();
|
||||
String about = "seed " + seed + " case " + index + ": " + request + " for " + generated.text().length() + " characters gave " + block;
|
||||
String trimmed = generated.text().stripTrailing();
|
||||
assertEquals(trimmed.length(), block.characters(), about);
|
||||
assertTrue(block.milliPoints() <= request.nominal() && block.milliPoints() >= request.floor(), about);
|
||||
assertTrue(block.milliPoints() == request.floor() || (request.nominal() - block.milliPoints()) % TextFit.STEP_MILLIPOINTS == 0, about);
|
||||
int next = 0;
|
||||
for(int line = 0; line < block.lines().size(); line++)
|
||||
{
|
||||
TextFit.Line each = block.lines().get(line);
|
||||
assertTrue(trimmed.substring(next, each.from()).isBlank() && each.to() >= each.from() && each.left() <= 0 && each.width() <= block.width(), about);
|
||||
assertTrue(each.to() > each.from() || each.marked(), about);
|
||||
assertTrue(each.to() == each.from() || trimmed.charAt(each.to() - 1) != ' ' || !each.marked(), about + ": a space before the mark");
|
||||
assertEquals(each.marked(), block.truncated() && line == block.lines().size() - 1, about);
|
||||
next = each.to();
|
||||
}
|
||||
assertEquals(block.shown(), next, about);
|
||||
if(block.fits())
|
||||
{
|
||||
fitted++;
|
||||
assertTrue(block.lines().size() <= request.maxLines() && block.width() <= request.width() && block.height() <= request.height(), about);
|
||||
}
|
||||
else
|
||||
{
|
||||
refused++;
|
||||
assertTrue(block.lines().size() > request.maxLines() || block.width() > request.width() || block.height() > request.height(), about);
|
||||
assertEquals(request.floor(), block.milliPoints(), about);
|
||||
assertFalse(block.truncated(), about);
|
||||
}
|
||||
if(block.truncated())
|
||||
{
|
||||
cut++;
|
||||
assertTrue(request.truncate() && block.fits() && block.milliPoints() == request.floor() && block.shown() < block.characters(), about);
|
||||
assertFalse(generated.at(request.floor(), request.floor()).fit().fits(), about);
|
||||
}
|
||||
else
|
||||
{
|
||||
assertEquals(trimmed.length(), block.shown(), about);
|
||||
}
|
||||
}
|
||||
}
|
||||
assertTrue(fitted > 300 && refused > 300 && cut > 50, "the cases must reach every outcome: " + fitted + " fitted, " + refused + " refused, " + cut + " cut");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Property: a text that fits at its nominal size is never shrunk, and the
|
||||
** size chosen is the largest that fits: the step above it does not.
|
||||
***************************************************************************/
|
||||
@Test
|
||||
void aTextThatFitsIsNeverShrunkAndTheChosenSizeIsTheLargestThatFits()
|
||||
{
|
||||
int kept = 0;
|
||||
int shrunk = 0;
|
||||
for(long seed : SEEDS)
|
||||
{
|
||||
Random random = new Random(seed);
|
||||
for(int index = 0; index < CASES; index++)
|
||||
{
|
||||
Case generated = generate(random);
|
||||
TextFit.Request request = generated.request();
|
||||
TextFit.Block block = generated.fit();
|
||||
String about = "seed " + seed + " case " + index + ": " + request + " gave " + block;
|
||||
TextFit.Block nominal = generated.at(request.nominal(), request.nominal()).fit();
|
||||
if(nominal.fits())
|
||||
{
|
||||
kept++;
|
||||
assertEquals(nominal, block, about);
|
||||
assertEquals(request.nominal(), block.milliPoints(), about);
|
||||
}
|
||||
else if(block.fits() && !block.truncated())
|
||||
{
|
||||
shrunk++;
|
||||
assertTrue(block.milliPoints() < request.nominal(), about);
|
||||
int steps = (request.nominal() - block.milliPoints() - 1) / TextFit.STEP_MILLIPOINTS;
|
||||
int above = request.nominal() - steps * TextFit.STEP_MILLIPOINTS;
|
||||
assertTrue(above > block.milliPoints() && above - block.milliPoints() <= TextFit.STEP_MILLIPOINTS, about);
|
||||
assertFalse(generated.at(above, above).fit().fits(), about + ", yet it fits at " + above);
|
||||
assertEquals(block, generated.at(block.milliPoints(), block.milliPoints()).fit(), about);
|
||||
}
|
||||
}
|
||||
}
|
||||
assertTrue(kept > 200 && shrunk > 100, "the cases must reach both: " + kept + " kept, " + shrunk + " shrunk");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Property: in the same box a shorter text never gets a smaller size than a
|
||||
** longer one that starts with it, and fits whenever the longer one does.
|
||||
***************************************************************************/
|
||||
@Test
|
||||
void aShorterTextNeverGetsASmallerSizeThanALongerOne()
|
||||
{
|
||||
int compared = 0;
|
||||
for(long seed : SEEDS)
|
||||
{
|
||||
Random random = new Random(seed);
|
||||
for(int index = 0; index < CASES; index++)
|
||||
{
|
||||
Case generated = generate(random);
|
||||
if(generated.text().isEmpty())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
Case all = generated.at(generated.request().nominal(), generated.request().floor());
|
||||
TextFit.Block longer = all.fit();
|
||||
TextFit.Block shorter = all.of(generated.text().substring(0, random.nextInt(generated.text().length()))).fit();
|
||||
String about = "seed " + seed + " case " + index + ": " + all.request() + " gave " + shorter + " and then " + longer;
|
||||
if(longer.fits())
|
||||
{
|
||||
compared++;
|
||||
assertTrue(shorter.fits() && shorter.milliPoints() >= longer.milliPoints() && shorter.lines().size() <= longer.lines().size(), about);
|
||||
}
|
||||
else
|
||||
{
|
||||
assertTrue(!shorter.fits() || shorter.milliPoints() >= longer.milliPoints(), about);
|
||||
}
|
||||
}
|
||||
}
|
||||
assertTrue(compared > 300, "compared " + compared);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Property: when the longest value is proven to fit (that many of the
|
||||
** face's worst character), no text of up to that many characters is
|
||||
** refused, whatever its characters, with spaces or without, cut or not.
|
||||
***************************************************************************/
|
||||
@Test
|
||||
void aProvenLengthImpliesNoRefusalForAnyTextUpToThatLength()
|
||||
{
|
||||
int proven = 0;
|
||||
int tried = 0;
|
||||
for(long seed : SEEDS)
|
||||
{
|
||||
Random random = new Random(seed);
|
||||
for(int index = 0; index < CASES; index++)
|
||||
{
|
||||
Case generated = generate(random);
|
||||
TextFit.Request request = generated.request();
|
||||
Face face = request.face();
|
||||
int maxLength = 1 + random.nextInt(40);
|
||||
Face.Glyph[] worst = new Face.Glyph[maxLength];
|
||||
Arrays.fill(worst, face.worst());
|
||||
TextFit.Block proof = TextFit.fit(request, worst, face.glyph(TextFit.MARK));
|
||||
if(!proof.fits())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
proven++;
|
||||
List<Integer> drawn = FaceTest.drawn(face);
|
||||
for(int attempt = 0; attempt < 12; attempt++)
|
||||
{
|
||||
StringBuilder text = new StringBuilder();
|
||||
int length = attempt == 0 ? maxLength : 1 + random.nextInt(maxLength);
|
||||
for(int character = 0; character < length; character++)
|
||||
{
|
||||
int choice = attempt % 4;
|
||||
text.appendCodePoint(choice == 0 ? face.widest() : choice == 1 && random.nextInt(5) == 0 ? ' ' : choice == 3 ? "⁄Ǻ̉Wj".charAt(random.nextInt(6)) : drawn.get(random.nextInt(drawn.size())));
|
||||
}
|
||||
tried++;
|
||||
TextFit.Block block = generated.of(text.toString()).fit();
|
||||
assertTrue(block.fits(), () -> "seed " + seed + ": " + request + " proves " + maxLength + " characters with " + proof + " yet refuses " + text.length() + " as " + block);
|
||||
assertTrue(block.milliPoints() >= proof.milliPoints(), "a real text is never smaller than the proof");
|
||||
}
|
||||
}
|
||||
}
|
||||
assertTrue(proven > 150 && tried > 1500, "proven " + proven + ", tried " + tried);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** The proof agrees with rendering for the widest character: the longest
|
||||
** length it proves is drawn, and it is short of the longest that is really
|
||||
** drawn by no more than one character a line, which is the room the proof
|
||||
** keeps for characters that reach past their advance.
|
||||
***************************************************************************/
|
||||
@Test
|
||||
void theProofAgreesWithTheWidestCharacters()
|
||||
{
|
||||
for(Face face : Face.all())
|
||||
{
|
||||
for(int dpi : Scene2.RESOLUTIONS)
|
||||
{
|
||||
for(int maxLines : List.of(1, 2, 3))
|
||||
{
|
||||
TextFit.Request request = new TextFit.Request(face, 700 * dpi / 203, 150 * dpi / 203, dpi, 11_000, 7000, null, maxLines, false);
|
||||
int proven = 0;
|
||||
int drawn = 0;
|
||||
for(int length = 1; length <= 200; length++)
|
||||
{
|
||||
Face.Glyph[] worst = new Face.Glyph[length];
|
||||
Arrays.fill(worst, face.worst());
|
||||
proven = TextFit.fit(request, worst, null).fits() ? length : proven;
|
||||
drawn = fit(request, new String(Character.toChars(face.widest())).repeat(length)).fits() ? length : drawn;
|
||||
}
|
||||
String about = face.scene() + " at " + dpi + " dpi on " + maxLines + " lines: proven " + proven + ", drawn " + drawn;
|
||||
assertTrue(proven >= 20 * maxLines && drawn < 200, about);
|
||||
assertTrue(proven <= drawn && drawn - proven <= maxLines, about);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,392 @@
|
||||
/*
|
||||
* 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.text;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import com.kingsrook.qbits.renderer.scene2.Canvas;
|
||||
import com.kingsrook.qbits.renderer.scene2.Frame;
|
||||
import com.kingsrook.qbits.renderer.scene2.Layout;
|
||||
import com.kingsrook.qbits.renderer.scene2.Placement;
|
||||
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.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** What is drawn is what was measured: the ink of every line is inside the
|
||||
** line's measured box, the block is where `align` and `valign` put it, and
|
||||
** nothing is clipped. These read the canvas the renderer composes.
|
||||
*******************************************************************************/
|
||||
class TextRasterTest
|
||||
{
|
||||
private static final int MARGIN = 8;
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Fit a text and draw it in a box that sits `MARGIN` dots inside a canvas,
|
||||
** so that ink outside the box would be seen. The box is given as the text
|
||||
** reads; turned a quarter, it is laid on its side.
|
||||
***************************************************************************/
|
||||
private static Canvas draw(TextFit.Request request, String text, Placement.Align align, Placement.Valign valign, int rotation, int threshold, int bandDots)
|
||||
{
|
||||
TextFit.Block block = TextFit.fit(request, request.face().glyphs(text), request.face().glyph(TextFit.MARK));
|
||||
assertTrue(block.fits(), "the case must fit: " + request);
|
||||
boolean upright = rotation % 180 == 0;
|
||||
Frame frame = new Frame(MARGIN, MARGIN, upright ? request.width() : request.height(), upright ? request.height() : request.width());
|
||||
Canvas canvas = new Canvas(frame.width() + 2 * MARGIN, frame.height() + 2 * MARGIN, request.dpi());
|
||||
TextRaster.paint(canvas, frame, new TextRaster.Drawing(request.face(), text, block, align, valign, rotation, threshold, true), bandDots);
|
||||
return (canvas);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** The box of the ink on a canvas: left, top, right and bottom (the last two
|
||||
** exclusive), or null when it is blank.
|
||||
***************************************************************************/
|
||||
private static Frame ink(Canvas canvas)
|
||||
{
|
||||
int left = Integer.MAX_VALUE;
|
||||
int top = -1;
|
||||
int right = -1;
|
||||
int bottom = -1;
|
||||
for(int y = 0; y < canvas.height(); y++)
|
||||
{
|
||||
int first = canvas.firstInk(y);
|
||||
if(first >= 0)
|
||||
{
|
||||
top = top < 0 ? y : top;
|
||||
bottom = y + 1;
|
||||
left = Math.min(left, first);
|
||||
right = Math.max(right, canvas.lastInk(y) + 1);
|
||||
}
|
||||
}
|
||||
return (top < 0 ? null : new Frame(left, top, right - left, bottom - top));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** How many dots of a canvas hold ink.
|
||||
***************************************************************************/
|
||||
private static int dots(Canvas canvas)
|
||||
{
|
||||
int dots = 0;
|
||||
for(int y = 0; y < canvas.height(); y++)
|
||||
{
|
||||
for(int x = 0; x < canvas.width(); x++)
|
||||
{
|
||||
dots += canvas.get(x, y) ? 1 : 0;
|
||||
}
|
||||
}
|
||||
return (dots);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Whether two canvases hold the same picture.
|
||||
***************************************************************************/
|
||||
private static void assertSamePicture(Canvas expected, Canvas actual, String about)
|
||||
{
|
||||
assertEquals(expected.width(), actual.width(), about);
|
||||
assertEquals(expected.height(), actual.height(), about);
|
||||
for(int y = 0; y < expected.height(); y++)
|
||||
{
|
||||
for(int x = 0; x < expected.width(); x++)
|
||||
{
|
||||
if(expected.get(x, y) != actual.get(x, y))
|
||||
{
|
||||
assertEquals(expected.get(x, y), actual.get(x, y), about + ": dot " + x + ", " + y);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Every character of every bundled face, at sizes and resolutions labels
|
||||
** use, is drawn with all of its ink inside the box that was measured for
|
||||
** its line. The renderer itself fails a render whose ink leaves that box
|
||||
** (the guard round each line), so drawing them all is the test; the ink is
|
||||
** then also read back from the canvas and held to the line's box.
|
||||
***************************************************************************/
|
||||
@Test
|
||||
void everyCharacterOfEveryFaceIsDrawnInsideItsMeasuredLine()
|
||||
{
|
||||
int[][] sizes = {{203, 6000}, {203, 9000}, {203, 14_250}, {300, 7000}, {300, 11_000}, {600, 5000}, {600, 12_250}};
|
||||
for(Face face : Face.all())
|
||||
{
|
||||
List<Integer> drawn = FaceTest.drawn(face);
|
||||
for(int[] size : sizes)
|
||||
{
|
||||
TextFit.Scale scale = new TextFit.Scale(size[1], size[0]);
|
||||
int inked = 0;
|
||||
for(int start = 0; start < drawn.size(); start += 16)
|
||||
{
|
||||
StringBuilder text = new StringBuilder();
|
||||
drawn.subList(start, Math.min(drawn.size(), start + 16)).forEach(text::appendCodePoint);
|
||||
TextFit.Request request = new TextFit.Request(face, scale.ceil(1500) * 16 + 16, scale.ceil(2000), size[0], size[1], size[1], null, 1, false);
|
||||
Canvas canvas = draw(request, text.toString(), Placement.Align.LEFT, Placement.Valign.TOP, 0, 128, TextRaster.BAND_DOTS);
|
||||
TextFit.Block block = TextFit.fit(request, face.glyphs(text.toString()), null);
|
||||
Frame ink = ink(canvas);
|
||||
if(ink != null)
|
||||
{
|
||||
inked++;
|
||||
TextFit.Line line = block.lines().get(0);
|
||||
Frame measured = new Frame(MARGIN, MARGIN + block.baseline() - line.above(), line.width(), line.above() + line.below());
|
||||
assertTrue(measured.contains(ink), () -> face.scene() + " at " + size[1] + " millipoints, " + size[0] + " dpi: ink " + ink + " outside the measured " + measured);
|
||||
assertTrue(new Frame(MARGIN, MARGIN, block.width(), block.height()).contains(measured));
|
||||
}
|
||||
}
|
||||
assertTrue(inked >= drawn.size() / 16 - 4, face.scene() + " drew " + inked + " lines");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Property: for generated texts, faces, boxes, sizes and resolutions, at
|
||||
** every rotation, no ink is outside the element's box and none is clipped,
|
||||
** and the upright picture turned is the turned picture. Up to 100 dots to
|
||||
** the em, where glyphs are drawn as bitmaps, the picture is also exactly
|
||||
** the same drawn a row at a time, so bands lose and repeat nothing.
|
||||
***************************************************************************/
|
||||
@Test
|
||||
void inkIsNeverOutsideTheBoxAndNeverClipped()
|
||||
{
|
||||
Random random = new Random(TextFitTest.SEEDS[0]);
|
||||
List<Placement.Align> aligns = List.of(Placement.Align.values());
|
||||
List<Placement.Valign> valigns = List.of(Placement.Valign.values());
|
||||
int drawn = 0;
|
||||
int banded = 0;
|
||||
for(int index = 0; drawn < 160; index++)
|
||||
{
|
||||
TextFitTest.Case generated = TextFitTest.generate(random);
|
||||
if(!generated.fit().fits() || generated.text().isBlank())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
drawn++;
|
||||
TextFit.Request request = generated.request();
|
||||
Placement.Align align = aligns.get(random.nextInt(3));
|
||||
Placement.Valign valign = valigns.get(random.nextInt(3));
|
||||
int threshold = 1 + random.nextInt(254);
|
||||
String about = "case " + index + ": " + request;
|
||||
Canvas upright = draw(request, generated.text(), align, valign, 0, threshold, TextRaster.BAND_DOTS);
|
||||
Frame ink = ink(upright);
|
||||
Frame box = new Frame(MARGIN, MARGIN, request.width(), request.height());
|
||||
assertTrue(ink == null || box.contains(ink), about + ": ink " + ink + " outside " + box);
|
||||
if(new TextFit.Scale(generated.fit().milliPoints(), request.dpi()).size() <= 100)
|
||||
{
|
||||
banded++;
|
||||
assertSamePicture(upright, draw(request, generated.text(), align, valign, 0, threshold, 1), about + ", row by row");
|
||||
}
|
||||
for(int rotation : List.of(90, 180, 270))
|
||||
{
|
||||
Canvas turned = draw(request, generated.text(), align, valign, rotation, threshold, TextRaster.BAND_DOTS);
|
||||
Canvas expected = new Canvas(turned.width(), turned.height(), request.dpi());
|
||||
for(int y = 0; y < request.height(); y++)
|
||||
{
|
||||
for(int x = 0; x < request.width(); x++)
|
||||
{
|
||||
if(upright.get(MARGIN + x, MARGIN + y))
|
||||
{
|
||||
int turnedX = rotation == 90 ? request.height() - 1 - y : rotation == 180 ? request.width() - 1 - x : y;
|
||||
int turnedY = rotation == 90 ? x : rotation == 180 ? request.height() - 1 - y : request.width() - 1 - x;
|
||||
expected.set(MARGIN + turnedX, MARGIN + turnedY, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
assertSamePicture(expected, turned, about + ", turned " + rotation);
|
||||
}
|
||||
}
|
||||
assertTrue(banded > 60, "drawn row by row: " + banded);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** The block sits in the box where `align` and `valign` say, a centred block
|
||||
** within one dot of the middle, and each line sits in the block the same
|
||||
** way. Read from the ink of a two-line text.
|
||||
***************************************************************************/
|
||||
@Test
|
||||
void theBlockIsPlacedByAlignAndValignAndCentredWithinOneDot()
|
||||
{
|
||||
String text = "HHHHHHHH HH";
|
||||
TextFit.Request request = new TextFit.Request(Face.SANS_BOLD, 241, 151, 203, 12_000, 12_000, null, 2, false);
|
||||
TextFit.Block block = TextFit.fit(request, Face.SANS_BOLD.glyphs(text), null);
|
||||
assertEquals(List.of("HHHHHHHH", "HH"), TextFitTest.shown(block, text));
|
||||
TextFit.Line wide = block.lines().get(0);
|
||||
TextFit.Line narrow = block.lines().get(1);
|
||||
for(Placement.Align align : Placement.Align.values())
|
||||
{
|
||||
for(Placement.Valign valign : Placement.Valign.values())
|
||||
{
|
||||
Canvas canvas = draw(request, text, align, valign, 0, 128, TextRaster.BAND_DOTS);
|
||||
Frame content = Layout.align(new Frame(MARGIN, MARGIN, 241, 151), block.width(), block.height(), align, valign);
|
||||
Frame ink = ink(canvas);
|
||||
assertTrue(content.contains(ink), align + " " + valign + ": ink " + ink + " outside the block " + content);
|
||||
int slackLeft = content.x() - MARGIN;
|
||||
int slackRight = MARGIN + 241 - content.right();
|
||||
int slackTop = content.y() - MARGIN;
|
||||
int slackBottom = MARGIN + 151 - content.bottom();
|
||||
switch(align)
|
||||
{
|
||||
case LEFT -> assertEquals(0, slackLeft);
|
||||
case CENTER -> assertTrue(slackLeft >= 0 && Math.abs(slackLeft - slackRight) <= 1);
|
||||
default -> assertEquals(0, slackRight);
|
||||
}
|
||||
switch(valign)
|
||||
{
|
||||
case TOP -> assertEquals(0, slackTop);
|
||||
case MIDDLE -> assertTrue(slackTop >= 0 && Math.abs(slackTop - slackBottom) <= 1);
|
||||
default -> assertEquals(0, slackBottom);
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
// the ink of each line, read from the rows of its own baseline, is in the line's //
|
||||
// box, and that box is against the same side of the block (or in its middle) //
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
int capitals = new TextFit.Scale(12_000, 203).floor(Face.SANS_BOLD.capHeight());
|
||||
int first = content.y() + block.baseline() - capitals / 2;
|
||||
int second = first + block.pitch();
|
||||
int narrowLeft = content.x() + align.position().place(block.width(), narrow.width(), 0);
|
||||
assertTrue(canvas.firstInk(first) >= content.x() && canvas.lastInk(first) < content.x() + wide.width(), "the widest line fills the block");
|
||||
assertTrue(canvas.firstInk(second) >= narrowLeft && canvas.lastInk(second) < narrowLeft + narrow.width(), align + ": the short line " + canvas.firstInk(second) + " to " + canvas.lastInk(second));
|
||||
int gapLeft = canvas.firstInk(second) - canvas.firstInk(first);
|
||||
int gapRight = canvas.lastInk(first) - canvas.lastInk(second);
|
||||
switch(align)
|
||||
{
|
||||
case LEFT -> assertTrue(Math.abs(gapLeft) <= 1 && gapRight > 50);
|
||||
case CENTER -> assertTrue(Math.abs(gapLeft - gapRight) <= 2 && gapLeft > 25);
|
||||
default -> assertTrue(Math.abs(gapRight) <= 1 && gapLeft > 50);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** A dot is ink when its grey is darker than the threshold, so a higher
|
||||
** threshold prints every dot a lower one prints and more: heavier strokes,
|
||||
** which is the control for small type on a thermal head.
|
||||
***************************************************************************/
|
||||
@Test
|
||||
void aHigherThresholdPrintsHeavier()
|
||||
{
|
||||
TextFit.Request request = new TextFit.Request(Face.SANS, 500, 60, 203, 14_000, 14_000, null, 1, false);
|
||||
List<Canvas> pictures = new ArrayList<>();
|
||||
for(int threshold : List.of(1, 60, 128, 200, 254))
|
||||
{
|
||||
pictures.add(draw(request, "Lot L240611A 0O1lI", Placement.Align.LEFT, Placement.Valign.TOP, 0, threshold, TextRaster.BAND_DOTS));
|
||||
}
|
||||
for(int index = 1; index < pictures.size(); index++)
|
||||
{
|
||||
Canvas lighter = pictures.get(index - 1);
|
||||
Canvas heavier = pictures.get(index);
|
||||
assertTrue(dots(heavier) > dots(lighter), "threshold step " + index + ": " + dots(lighter) + " then " + dots(heavier));
|
||||
for(int y = 0; y < lighter.height(); y++)
|
||||
{
|
||||
for(int x = 0; x < lighter.width(); x++)
|
||||
{
|
||||
assertFalse(lighter.get(x, y) && !heavier.get(x, y), "a dot printed at the lower threshold is printed at the higher");
|
||||
}
|
||||
}
|
||||
}
|
||||
assertTrue(dots(pictures.get(0)) > 100, "full ink is black at every threshold");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Ink outside the measured line is never clipped away and never printed:
|
||||
** the render fails. Forced here with lines whose boxes are stated too
|
||||
** small: one that ends in the gap between two letters, so the second
|
||||
** letter is wholly beyond the guard and only its bounds tell, and two that
|
||||
** cut through the letters, whose ink is found in the guard.
|
||||
***************************************************************************/
|
||||
@Test
|
||||
void inkOutsideTheMeasuredLineFailsTheRender()
|
||||
{
|
||||
TextFit.Request request = new TextFit.Request(Face.SANS_BOLD, 200, 60, 203, 12_000, 12_000, null, 1, false);
|
||||
TextFit.Block honest = TextFit.fit(request, Face.SANS_BOLD.glyphs("HH"), null);
|
||||
TextFit.Line line = honest.lines().get(0);
|
||||
Canvas canvas = new Canvas(220, 80, 203);
|
||||
Frame frame = new Frame(10, 10, 200, 60);
|
||||
for(TextFit.Line wrong : List.of(new TextFit.Line(0, 2, false, line.left(), line.right() / 2, line.above(), line.below()), new TextFit.Line(0, 2, false, line.left(), line.right(), line.above() / 3, line.below()),
|
||||
new TextFit.Line(0, 2, false, line.left(), line.right(), line.above(), -line.above() / 2)))
|
||||
{
|
||||
TextFit.Block forged = new TextFit.Block(honest.milliPoints(), List.of(wrong), wrong.width(), honest.height(), honest.baseline(), honest.pitch(), 2, 2, true);
|
||||
TextRaster.Drawing drawing = new TextRaster.Drawing(Face.SANS_BOLD, "HH", forged, Placement.Align.LEFT, Placement.Valign.TOP, 0, 128, true);
|
||||
assertEquals("TEXT_INK_OUTSIDE_MEASURE", assertThrows(IllegalStateException.class, () -> TextRaster.paint(canvas, frame, drawing, TextRaster.BAND_DOTS)).getMessage());
|
||||
}
|
||||
Canvas clean = new Canvas(220, 80, 203);
|
||||
TextRaster.paint(clean, frame, new TextRaster.Drawing(Face.SANS_BOLD, "HH", honest, Placement.Align.LEFT, Placement.Valign.TOP, 0, 128, true), TextRaster.BAND_DOTS);
|
||||
assertTrue(new Frame(10, 10, honest.width(), honest.height()).contains(ink(clean)));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Very large type is drawn in bands of whole rows, so a line never needs
|
||||
** more grey than the band allows. 200 pt at 600 dpi is 1667 dots to the em.
|
||||
** The bands lose no row and repeat none: the picture is the one drawn in
|
||||
** one piece, but for a few edge dots where the rasteriser, given a band,
|
||||
** shades a curve differently: fewer than one ink dot in ten thousand (4 of
|
||||
** 121 thousand, 5 of 265 thousand and 40 of 1.06 million were seen).
|
||||
***************************************************************************/
|
||||
@Test
|
||||
void veryLargeTypeIsDrawnInBands()
|
||||
{
|
||||
for(int dpi : Scene2.RESOLUTIONS)
|
||||
{
|
||||
TextFit.Request request = new TextFit.Request(Face.CONDENSED_BOLD, 2400 * dpi / 600, 2400 * dpi / 600, dpi, 200_000, 200_000, null, 1, false);
|
||||
Canvas whole = draw(request, "7g", Placement.Align.CENTER, Placement.Valign.MIDDLE, 0, 128, Integer.MAX_VALUE);
|
||||
assertSamePicture(whole, draw(request, "7g", Placement.Align.CENTER, Placement.Valign.MIDDLE, 0, 128, TextRaster.BAND_DOTS), "200 pt at " + dpi + " dpi fits one band");
|
||||
Canvas banded = draw(request, "7g", Placement.Align.CENTER, Placement.Valign.MIDDLE, 0, 128, 1 << 16);
|
||||
int different = 0;
|
||||
for(int y = 0; y < whole.height(); y++)
|
||||
{
|
||||
for(int x = 0; x < whole.width(); x++)
|
||||
{
|
||||
different += whole.get(x, y) == banded.get(x, y) ? 0 : 1;
|
||||
}
|
||||
}
|
||||
assertTrue(different * 10_000L <= dots(whole), "200 pt at " + dpi + " dpi: " + different + " dots differ of " + dots(whole));
|
||||
System.out.println("200 pt at " + dpi + " dpi in bands: " + different + " of " + dots(whole) + " ink dots differ from the picture drawn in one piece");
|
||||
assertEquals(ink(whole), ink(banded));
|
||||
int capitals = new TextFit.Scale(200_000, dpi).floor(Face.CONDENSED_BOLD.capHeight());
|
||||
assertTrue(ink(banded).height() > capitals && ink(banded).height() < 2 * capitals && ink(banded).width() > capitals, "the digits are the size asked: " + ink(banded));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"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"},
|
||||
"defaults": {"size": "8pt"},
|
||||
"elements": [
|
||||
{"id": "sans", "type": "text", "value": "Sans 0O 1lI 5S 8B Åéj", "font": "qqq-sans", "y": "0mm", "height": "4.2mm"},
|
||||
{"id": "sans-bold", "type": "text", "value": "Bold 0O 1lI 5S 8B Åéj", "font": "qqq-sans-bold", "y": "4.2mm", "height": "4.2mm"},
|
||||
{"id": "condensed-bold", "type": "text", "value": "Condensed 0O 1lI 5S 8B Åéj", "font": "qqq-condensed-bold", "y": "8.4mm", "height": "4.2mm"},
|
||||
{"id": "mono", "type": "text", "value": "Mono 0O 1lI 5S 8B Åéj", "font": "qqq-mono", "y": "12.6mm", "height": "4.2mm"},
|
||||
{"id": "mono-bold", "type": "text", "value": "Bold 0O 1lI 5S 8B Åéj", "font": "qqq-mono-bold", "y": "16.8mm", "height": "4.2mm"}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"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"},
|
||||
"defaults": {"font": "qqq-sans-bold", "size": "10pt"},
|
||||
"elements": [
|
||||
{"id": "kept-box", "type": "box", "width": "22mm", "height": "5mm", "thickness": "0.1mm"},
|
||||
{"id": "kept", "type": "text", "value": "LOT 240611", "width": "22mm", "height": "5mm", "align": "center", "valign": "middle"},
|
||||
{"id": "right-box", "type": "box", "anchor": "top-right", "width": "22mm", "height": "5mm", "thickness": "0.1mm"},
|
||||
{"id": "right", "type": "text", "value": "9.5 kg", "font": "qqq-condensed-bold", "size": "11pt", "anchor": "top-right", "width": "22mm", "height": "5mm", "align": "right", "valign": "bottom"},
|
||||
{"id": "shrunk-box", "type": "box", "y": "5.5mm", "width": "44mm", "height": "4mm", "thickness": "0.1mm"},
|
||||
{"id": "shrunk", "type": "text", "value": "(01)00012345678905(10)L240611A", "font": "qqq-mono-bold", "size": "9pt", "y": "5.5mm", "width": "44mm", "height": "4mm", "valign": "middle"},
|
||||
{"id": "wrapped-box", "type": "box", "y": "10mm", "width": "36mm", "height": "7.5mm", "thickness": "0.1mm"},
|
||||
{"id": "wrapped", "type": "text", "value": "(01)00012345678905(10)L240611A-RETURNED-000123", "font": "qqq-mono", "size": "7pt", "y": "10mm", "width": "36mm", "height": "7.5mm", "align": "center", "valign": "middle",
|
||||
"maxLength": 46, "fit": {"maxLines": 2}},
|
||||
{"id": "cut-box", "type": "box", "anchor": "bottom-left", "width": "30mm", "height": "3.6mm", "thickness": "0.1mm"},
|
||||
{"id": "cut", "type": "text", "value": "A description of the item that is too long", "font": "qqq-sans", "size": "7pt", "anchor": "bottom-left", "width": "30mm", "height": "3.6mm", "fit": {"overflow": "truncate", "shrinkTo": "7pt"}}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"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"},
|
||||
"defaults": {"font": "qqq-sans-bold", "size": "10pt"},
|
||||
"elements": [
|
||||
{"id": "up", "type": "text", "value": "TEST-0001", "font": "qqq-mono-bold", "rotation": 90, "width": "5mm", "align": "center"},
|
||||
{"id": "down", "type": "text", "value": "TEST-0001", "font": "qqq-mono-bold", "rotation": 270, "anchor": "top-right", "width": "5mm", "align": "center"},
|
||||
{"id": "flip", "type": "text", "value": "THIS WAY UP", "rotation": 180, "x": "7mm", "width": "30mm", "height": "5mm", "align": "center"},
|
||||
{"id": "panel", "type": "text", "value": "ZONE 7", "font": "qqq-condensed-bold", "size": "16pt", "reverse": true, "x": "7mm", "y": "6mm", "width": "30mm", "height": "8mm", "align": "center", "valign": "middle"},
|
||||
{"id": "fine", "type": "text", "value": "Best before 2027-06-11", "font": "qqq-sans", "size": "6.5pt", "x": "7mm", "y": "15mm", "width": "32mm", "height": "5mm", "valign": "bottom"}
|
||||
],
|
||||
"overrides": [
|
||||
{"when": {"dpi": [203]}, "set": {"fine": {"font": "qqq-sans-bold", "threshold": 150}}}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
@@ -0,0 +1,3 @@
|
||||
^XA^PW406^LL203^LH0,0^CI28
|
||||
^FO19,19^GFA,5600,5600,35,00000000000000000000000000000000000000000000000038000000000000000000000000000000000000000000000000000000000000000000004C0000000000000000000000000000000000000000000000000000000000000000000044000000000000000000000000000000000000000000000000000000000000000000004C00000000000000000000000000000000000000000000000000000000000000000000380000000000000000000000000000000000000000000000800000000000000000000000006080000000000000001F0000000000007C00FC000700C3E007F80F8003E03FC0003000C0C0000000000000007FC00000000000FE01FE000F00C7F007F83FE007F83FF000380080800000000000000060E00000000001C30303001B00C080040030700C18303000780080000000000000000040000000000001810601803300C080040020000C0C3018006C00000000000000000000C000F02780F801818601806300C080040060000C0C3018004C03C080000000000000006003F83FC1FC01018401800300C0800C0030000C08301800CC07F0C00000000000000070020C30E30403018C00C00300C0800CE038000618303000C60C30C0000000000000003F000C30630003018C00C00300C0800FF81F8003E03FE001860818C0000000000000000FC00C30638003018C00C00300C0800E1807E007F03FF001861818C00000000000000000C0FC3061F003018C00C00300C080000C00600C1C301801831FF8C0000000000000000063FC30607C01018400800300C080000C0030080C301803FF1FF8C00000000000000000660C30600C01818601800300C080000C0030180C301803031800C00000000000000000660C30600601818601800300C080000C0030180C301803019800C000000000000000C0E60C30630601830303000300C0800C1860700C0C301806018C18C0000000000000007BC71E30638C00E703CE003FF0E3E007783DE00F383FF006018E30C0000000000000003F83E63061F8007E00FC003FF067F003F01FC007F03FE00600C3E0C000000000000000000000000000000000000000000000000000000000000000000000C000000000000000000000000000000000000000000000000000000000000000000000C000000000000000000000000000000000000000000000000000000000000000000000C000000000000000000000000000000000000000000000000000000000000000000003800000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000C000000000000000000000000000000000000000000000000000000000000000000003E000000000000000000000000000000000000000000000000000000000000000000003300000000000000000000000000000000000000000000000000000000000000000000630000000000000000000000000000000000000000000000000000000000000000000073000000000000000000000000000000000000000000000000000000000000000000003E001800000000000000000000000E00380000000000000380000000000000000000001C00381800000000000000FF80000E003800F803F0001F838FE00FFC0FC001F83FE0001E00383C00000000000000FFE0000E003801FE07F8001F838FF00FFC1FF007FC3FF8003E00703C00000000000000FFE0000E003803FF0FFC003F838FF00FFC3FF007FE3FF8003F00201C00000000000000E1E0000E0038078F1E1E007F8383C00E0038600F0E3878003F00000000000000000000E0F07C0E0F3807871C0E00F78383C00E0078000F0E383C007700F81C00000000000000E0E1FE0E1FF80707BC0F00E78383C00C007800070E3838007783FC1C00000000000000E1E1FF0E1FF80707BC0F00478383C00CF83F00079C3878007383DE1C00000000000000FFC3C70E3C380707BC0F00078383C00FFC3FE001F83FF000E3878E1C00000000000000FFE3878E3C380707BC0F00078383C00F7C1FF003FC3FF800E3C70F1C00000000000000FFF3878E38380707BC0F00078383C0001E03F8079E3FFC00FFC7FF1C00000000000000E0F3878E38380707BC0F00078383C0001E00780F0E383C01FFC7FF1C00000000000000E073878E383807871C0E00078383C0001E00780E0F381C01FFE7001C00000000000000E0F3C78E3C38078F1E1E00078383C00C1C30780F0F383C01C1E7801C00000000000000FFF1EF0F1EF803FF0FFE007FF3CFE01FFC7FF00F9E3FFC03C0E3CE1C00000000000000FFE1FE0F1FF801FE0FFC007FF3CFF00FF83FF007FE3FF80380F3FE1C00000000000000FFC0FC070FB800FC03F0007FF1CFF007F01FC003F83FF00380F1FC1C000000000000000000000000000000000000000000000000000000000000000000001C000000000000000000000000000000000000000000000000000000000000000000001C000000000000000000000000000000000000000000000000000000000000000000007C0000000000000000000000000000000000000000000000000000000000000000000078000000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000000001F000000000000000000000000000000000000000000000000000000000000000000001980000000000000000000000000000000000000000000000000000000000000000000198000000000000000000000000000000000000000000000000000000000000000000019800000000000Line truncated
|
||||
^XZ
|
||||
|
After Width: | Height: | Size: 3.0 KiB |
@@ -0,0 +1,7 @@
|
||||
^XA^PW600^LL300^LH0,0^CI28
|
||||
^FO27,26^GFA,1640,1640,41,0000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000007C00000000000000000000000000000000000000000000000000000000000000000000000000000000E600000000000000000000000000000000000000000000000000000000000000000000000000000000C200000000000000000000000000000000000000000000000000000000000000000000000000000000C300000000000000000000000000000000000000000000000000000000000000000000000000000000C6000000000000000000000000000000000000000000000000000000000000000000000000000000007E000000000000000000000000000000000000000000000000000000000000000000000000000000003C0000C000000000000000000000000000000000000000000600000000000000000000000000000000000000E01800380000000000000000003C0000E000000000070000000000000E000001E00000000000000001C01C01FF800000000000000000FF0007FE000007C00707FC000FFF807FE0000FFC03FFE000003C0001C01C03FFC00000000000000001FF800FFF00000FC00707FC000FFF80FFF0001FFE03FFF000007C000380180781E0000000000000000381C01E0780001DC00700E0000E0001E078003C0F03807800007C000300000E00E0000000000000000700E03803C00039C00700E0000C0003803800380703803800006E000000000E0000000000000000000700E07001C00071C00700E0000C0003800000380703801C0000EE000000000E00000E0000600060000600607000E000E1C00700E0000C0003800000380703801C0000C6000300000E00007F803BF803FC000E00706000E00041C00700E0000C0003800000380703801C0000C7001FE01C0E0000FFE03FFC07FE000E0070E000E00001C00700E0000C0003800000380703801C0001C7003FF01C078001C0E03C1E0E07000E0070E000600001C00700E0000C0001E000001C0E0380380001838078381C07E0008070380E0E00000E0070E000700001C00700E0001DFC01F800000F3C03807000038380601C1C01FE000070380E0E00000E0070E000700001C00700E0001FFF007F800007F803FFE000038180E01C1C007F80007038060E00000E0070E000700001C00700E0001E07801FE0000FFC03FFF0000301C0E00C1C000FC0007038060F00000E0070E000700001C00700E0000C038003F0001C0E038078000701C0C00E1C0001E01FF0380607F0000E0070E000700001C00700E000000380007800380703801C000701C0FFFE1C0000E0FFF0380603FE000E0070E000600001C00700E0000001C0003800700383801C0007FFE0FFFE1C000071E0703806003F000E0070E000E00001C00700E0000001C0001C00700383800E000FFFE0C0001C000071C07038060007000600606000E00001C00700E0000001C0001C00700383800E000E00E0C0001C000071807038060003000700E07000E00001C00700E000000180001C00700383800E000C0070E0001C0C00E3807038060003000700E07801C00001C00700E000080383003800700383801C001C0070E0081C0E00E1C07038060C07000381C03C03C00001C00700E0001C0383803800380703801C001C0070701C1C0F83C1C1F038060E070003C3C01E0F800001E00700E0001F0F03E0F0003C0F0380F80018003878381C03FF80FF3C380607FE0001FF800FFF00007FFF07C7FC000FFE00FFE0001FFE03FFF0003800383FF81C01FF007E1C380603FC0000FF0003FC00007FFF03C7FC0003FC007FC00007F803FFE0003800380FE01C000000000000000000000000000000000000000000000000000000000000000000000000000000001C000000000000000000000000000000000000000000000000000000000000000000000000000000001C0000000000000000000000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000000000000000000000001800000000000000000000000000000000000000000000000000000000000000000000000000000000F800000000000000000000000000000000000000000000000000000000000000000000000000000000F800000000000000000000000000000000000000000000000000000000000000000000000000000000E00^FS
|
||||
^FO27,77^GFA,1680,1680,42,000000000000000000000000000000000000000000000000000000000000000000000001F80000000000000000000000000000000000000000000000000000000000000000000000000000000003FC00000000000000000000000000000000000000000000000000000000000000000000000000000000039E00000000000000000000000000000000000000000000000000000000000000000000000000000000078E00000000000000000000000000000000000000000000000000000000000000000000000000000000078E00000000000000000000000000000000000000000000000000000000000000000000000000000000038E0000000000000000000000000000000000000000000000000000000000000000000000000000000003FE0000C00000000000000000000000000000000000000000000000000000000000000000000000000001FC0000F000000000000000780007C000000000000000000001E000000000000000000000000000000000F80001F00F000000000000F80007C0000F00003C0000000003E000000000000078000007800000000000000001E01F00FFFC000000F80007C0007FE001FF800003FC03E0FFE001FFF803FF00003FF00FFFC00001FC0003E01F80FFFE000000F80007C000FFF007FFC00007FC03E0FFE001FFF80FFFC0007FF80FFFE00001FC0003C01F80FFFF000000F80007C001FFF80FFFE00007FC03E0FFE001FFF81FFFC000FFFC0FFFF00003FC0003801F00FFFF800000F80007C001FFF80FFFF0000FFC03E0FFE001FFF81FFFC001F87E0FFFF80003FE0000000600F81F800000F80007C003F0FC1F83F8001F7C03E01F0001E0003F078001F03E0F81F80003FE0000000000F80F803E00F803C7C003E07C1F00F8003E7C03E01F0001E0003E010001F03E0F80F80007DE0007C00F00F80F80FF80F80FF7C003E07C3F00F8003E7C03E01F0001E0003E000001F03E0F80F80007DF001FF01F00F80F83FFC0F81FF7C007E07E3E007C003C7C03E01F0001E0003E000001F03E0F80F800079F007FF81F00F81F03FFE0F81FFFC007C03E3E007C00187C03E01F0001E7E03FC00000F87C0F81F0000F8F007FFC1F00FFFF07E3F0F83F0FC007C03E3E007C00007C03E01F0001CFF01FFC00007FF80FFFF0000F8F80F87E1F00FFFC07C1F0F83E07C007C03E3E007C00007C03E01F0003DFF81FFF80003FF00FFFC0000F0F80F83E1F00FFFE0781F0F83E07C007C03E3E007C00007C03E01F0003FFFC0FFFC0003FF00FFFE0001F0780F03E1F00FFFF0F81F0F83E07C007C03E3E007C00007C03E01F0003F0FC03FFC000FFFC0FFFF0001F07C1F83E1F00F81F8F81F8F87E07C007C03E3E007C00007C03E01F0000007E003FE001F87E0F81F8001FFFC1FFFE1F00F80FCF81F8F87E07C007E07E3E007C00007C03E01F0000007E0007E001F03E0F80FC003FFFC1FFFF1F00F807CF81F8F87E07C007E07E3E00FC00007C03E01F0000007E0003E003F03F0F807C003FFFE1FFFE1F00F807CF81F0F83E07C003E07C3F00F800007C03E01F0000007E0003E003F03F0F807C003FFFE1F0001F00F80FC7C1F0F83E07C003E07C1F01F800007C03E01F0001C07C1C03E003F03F0F80FC007E03E0F8001F00F81FC7C1F0F83F0FC003F0FC1FC3F800007C03E01F0003E0FC3F07E001F03E0F81FC007C01F0F8181F00FFFF87FFE0FE3FFFC001FFF80FFFF0001FFFE3F8FFE003FFFC3FFFC001FFFE0FFFF8007C01F0FFFC1F00FFFF83FFE07E1FFFC001FFF807FFE0001FFFE1F8FFE001FFF83FFFC000FFFC0FFFF800FC01F07FFE1F00FFFF01FFC07E0FF7C000FFF003FFC0001FFFE1F8FFE000FFF01FFF80007FF80FFFF000F801F83FFC1F00FFFC00FF001E07E7C0003FC001FF00001FFFE078FFE0007FC007FE00003FF00FFFC000F800F80FF01F00000000000000000000000000000000000000000000000000000000000000000000000000000000001F00000000000000000000000000000000000000000000000000000000000000000000000000000000001F00000000000000000000000000000000000000000000000000000000000000000000000000000000001F00000000000000000000000000000000000000000000000000000000000000000000000000000000007F00000000000000000000000000000000000000000000000000000000000000000000000000000000007F00000000000000000000000000000000000000000000000000000000000000000000000000000000007E00000000000000000000000000000000000000000000000000000000000000000000000000000000007800^FS
|
||||
^FO26,126^GFA,1960,1960,49,00000000000000000000000000000000000000000000000000000000000000000000000000000000000000003F0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007F80000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000071C0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000071C0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000071C0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000071C000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007BC000600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003F8000780000000000000000003C00000000000000000001E0000000000000000001E000000000000000000000000000001F0000F80E00F00000000000003C00000000000000000001E0001E0001C000000001F000000000000E0000078000000000000000F01F07FC0000000000003C00000000000000000001E0007F800FF80000FE01F1FFC007FFC07FC0003FF03FFF00003F8001E01F0FFF0000000000003C00000000000000000001E001FFC01FFE0001FE01F1FFC007FFC1FFF0007FF83FFF80003F8001E01F1FFF0000000000003C00000000000000000001E001FFE03FFF0003FE01F1FFC00FFFC1FFF800FFFC3FFF80007F8001C01F3FFF8000000000003C00000000000000000001E003F3F07FFF0003DE01F0FF800FFFC3FFF000F87C3FFFC0007F800000003E0FC000000000003C00000000000000000001E003E1F07C1F8007DE01F03E000F0003E06000F03C3E07C0007BC00000007C07007C01C3801E3C03E00E1C00F800F800F1E003C0F0F80F800F9E01F03E000F0003C00000F03C3E07C0007BC003E00E7C0001FF03EFE07F3C0FF81F7F03FF03FE03F9E007C0F8F80780071E01F03E000F0003C00000F03C3E07C000F3C00FF81F7C0003FF83EFE07FBC1FFC1F7F07FF87FF03FDE007C0F8F807C0001E01F03E000F0003E00000F03C3E07C000F3E01FFC1F7C0007FFC3FFF0FFFC3FFE1FFF87FF0FFF87FFE007C0F8F807C0001E01F03E000F3F03F0000078783E07C000F3E03FFE1F7C0007C7C3F1F0F87C3E3E1F8F8F860F8F87C3E007C0F8F807C0001E01F03E000F7F83FF00007FF83FFF8001F1E03E3E1F7C000F83C3E1F0F07C7C1E1F0F8F801F078783E007C0F8F807C0001E01F03E000F7FC1FFE0001FE03FFE0001F1E07C1E1FFC000F83E3E1F1F03C7C1F1F0F8F801F07CF81E007C0F9F807C0001E01F03E000FFFC0FFF0003FF03FFF0001E1F07C1F1F7C000F83E3E1F1F03C7C1F1F0F87F01F07CF81E007C0F8F807C0001E01F03E000F87E03FF8007FF83FFF8001E1F07C1F1F7C000F83E3E1F1F03C7FFF1F0F87FE1FFFCF81E007C0F8F807C0001E01F03E000003E003F800F87C3E07C003FFF07FFF1F7C000F83E3E1F1F03C7FFF1F0F83FF1FFFCF81E007C0F8F807C0001E01F03E000003E000F801F03E3E03E003FFF87FFF1F7C000F83E3E1F1F03C7C001F0F80FF9F000F81E007C0F8F807C0001E01F03E000003E000F801F03E3E03E003FFF87C001F7C020F83E3E1F1F03C7C001F0F800F9F000F81E007C0F8F80780001E01F03E000003E000F801F03E3E03E003FFF87C001F7C078F83E3E1F1F03C7C001F0F80079F000F81E003C0F0F80F80001E01F03E000603E180F801F03E3E03E007C0787C001F3E0F8783C3E1F0F87C3E0C1F0F86078F8307C3E003E1F07C1F80001E01F03E000F07E3C0F801F87E3E07E007807C3E0C1F3FFF87CFC3E1F0FCFC3F1E1F0F8F8F8FC787E7E003FFF07FFF0003FFF1F8FFC00FFFC7FFF800FCFC3FFFC007807C3F1E1F1FFF07FF83E1F0FFBC1FFE1F0F8FFF87FF87FDE001FFE03FFE0003FFF1FDFFC00FFFC3FFF000FFFC3FFFC00F803C1FFE1F0FFE03FF83E1F07F3C0FFC1F0F87FF03FF03F9E000FFC01FFC0003FFF0FDFFC007FF81FFE0007FF83FFF800F803E0FFC1F07FC00FE03E1F03F3C07F81F0F83FE01FE01F9E0007F800FF80003FFF07DFFC001FE007FC0003FF03FFF000F003E07F81F0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001F0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001F0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001F0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007F0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007E0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007E00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000078^FS
|
||||
^FO26,176^GFA,2028,2028,52,00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003C0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007E000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000C3000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000C3000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000C3000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000E70000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007E0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007000070000000000000000000000000003C0003C00000000000000000000000000000000038000000003C0000000000000000000F0000F8F81F0000000000000000000000FF000FF000000007E01FF0007FFE0000007FFC00FF00000000FF007FF000000003C0000E0000F8F81F0000000000000000000003FF801FF80000000FE01FF0007FFE0000007FFC01FF80000001FF807FF800000003E0001C000078FC3F0000000000000000000003C3C03C3C0000001FE000700003C0000000700003C3C0000003C3C0707C00000007E00018000000EC37000000000000000000000781E0781E0000007CE000700003C000000070000781C000000781E0701E00000007E00000000000EC37000000000000000000000700E0700E00000078E000700003C000000070000700E000000700E0700E00000006E00000000000EE7700180000C000180000000700E0700E00000060E000700003C000000070000700E000000700E0700E0000000E700038000000E66700FF0077F800FF0000000700E0700E00000040E000700003C0000000700007000000000700E0700E0000000E7000FF007FF8E66701FF8077FC01FF8000000700E0700E00000000E000700003C0000000700007000000000700E0700E0000000E7001FF807FF8E6E703C3C07C3C03C3C000000700E0700E00000000E000700003C0000000700003800000000381C0701C0000000E700381C00078E7C70781E0781E0781E000000700E0700E00000000E000700003C00000007FF003E000000003C3C0703C0000001C380700E00078E3C70700E0700E0700E000000718E0700E00000000E000700003C00000007FF801FE00000000FF007FF80000001C380700E00078E3C70700E0700E0700E00000073CE0700E00000000E000700003C00000003FFC00FF80000000FF007FF00000001C380700E00078E3870700E0700E0700E00000073CE0700E00000000E000700003C0000000001E001FC0000003FFC07FFC0000001C380700E00078E0070700E0700E0700E000000718E0700E00000000E000700003C0000000000E0003E000000781E0701C000000381C07FFE00078E0070700E0700E0700E000000700E0700E00000000E000700003C0000000000E0000E000000700E0700E000000381C07FFE00078E0070700E0700E0700E000000700E0700E00000000E000700003C0000000000E0000E000000E0070700E0000003FFC07FFE00078E0070700E0700E0700E000000700E0700E00000000E000700003C0000000000E00007000000E007070070000003FFC0700000078E0070700E0700E0700E000000700E0700E00000000E000700003C0000000600E06007000000E007070070000007FFE0700000078E0070700E0700E0700E000000700E0700E00000000E000700003C0000000700E07006000000E0070700F000000700E0700000078E0070700E0700E0700E000000700E0700E00000000E000700003C0000000700E0700E000000700E0700E000000700E0700E00078E0070381C0700E0381C000000781E0381C00000000E000380003C0000000781E0781E000000781E0701E000000700E0381E00078E00703E7C0700E03E7C0000003C3C03E7C0000007FFF003FF03FFC0000003C7C03E7C0000003E7C07FFC000000E00703C3C00078E00701FF80700E01FF80000001FF801FF80000007FFF001FF07FFE0000001FF801FF80000001FF807FF8000000E00701FF800070E00700FF00700E00FF00000000FF000FF00000007FFF000FF07FFE0000000FF000FF00000000FF007FF0000000E00700FF0000700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000070000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000F000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007FE000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007FC000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007F00^FS
|
||||
^FO27,225^GFA,2028,2028,52,0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FC000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001FE000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001860000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000018600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000186000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001CE000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FC000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000780001F0000F00000000000000000000000000078000780000000000000000000000000000000007000000000780000000000000000001E0001F0FFE0000003FF000003C0000001FE001FE00000000FC03FF000FFFC000000FFFC03FE00000001FE00FFE00000000FC0003C0001F8FFF8000003FF000003C0000007FF807FF80000001FC03FF000FFFC000000FFFC07FF80000007FF80FFF80000000FC000780000F0FFFC000003FF000003C000000FFFC07FF80000007FC03FF000FFFC000000FFFC0FFF8000000FFFC0FFFC0000000FC00070000060F07C0000000F000003C000000F87C0F87C000000FFC000F0000780000000F0000F87C000000F87C0F07C0000001FE00000000000F03E0000000F000003C000000F03C0F03C000000FBC000F0000780000000F0000F03C000000F03C0F03E0000001FE00000000000F03E0078000F0000C3C000001E01E0F03C000000F3C000F0000780000000F0001F03C000000F03E0F03E0000001FE00030000000F03E01FE000F0003FBC000001E01E1F03E000000C3C000F0000780000000F0001F000000000F03E0F03E0000001CE001FE00FFF0F03C07FF800F0007FFC000001E01E1F03E00000083C000F0000780000000F0000F000000000F03C0F03C0000003CF007FF80FFF0F03C0FFFC00F000FFFC000001E01E1F03E00000003C000F0000780000000FE000F800000000F03C0F03C0000003CF007FF80FFF0F0F80F87C00F000F87C000001E01E1F03E00000003C000F0000780000000FFE00FF000000007CF80F0F80000003CF00F03C000F0FFF00F03C00F000F03C000001E79E1F03E00000003C000F0000780000000FFF807FE00000003FF00FFF000000038700F03C000F0FFF01F03E00F001F03C000001E79E1F03E00000003C000F0000780000000FFFC03FF80000003FF00FFF000000078781E01E000F0FFF81F03E00F001F03C000001E79E1F03E00000003C000F0000780000000007C00FFC0000007FF80FFF800000078781E01E000F0F07C1F03E00F001F03C000001E31E1F03E00000003C000F0000780000000003C001FC000000F87C0F07C00000078781FFFE000F0F03E1F03E00F001F03C000001E01E1F03E00000003C000F0000780000000003E0003E000001F03E0F03E00000078781FFFE000F0F01E1F03E00F001F03C000001E01E1F03E00000003C000F0000780000000003E0001E000001E01E0F01E000000FFFC1FFFE000F0F01E1F03E00F001F03C000001E01E1F03E00000003C000F0000780000000003E0001E000001E01E0F01E000000FFFC1E000000F0F01E1F03E00F001F03C000001E01E1F03E00000003C000F0000780000001E03E1E01E000001E01E0F01E000000FFFC1E000000F0F01E1F03E00F001F03C000001E01E0F03C00000003C000F0000780000001E03E1E01E000001E01E0F01E000000F03C1F000000F0F03E0F03C00F800F03C000000F03C0F03C00000003C000F8000780000001F03C1F03E000001F03E0F03E000001E01E0F03C000F0F1FC0FCFC00FFE0FCFC000000FCFC0FCFC000000FFFE00FFE0FFFC000000FCFC0FCFC000000F87C0F1FC000001E01E0F87C000F0FFFC07FF8007FF0FFFC0000007FF807FF8000000FFFE007FF0FFFC000000FFF80FFFC000000FFFC0FFFC000001E01E07FF8000F0FFF803FF0003FF07FBC0000003FF003FF0000000FFFE003FF0FFFC0000007FF007FF80000007FF80FFF8000001E01E03FF0000F0FFE001FE0001FF03F3C0000001FE001FE0000000FFFE001FF0FFFC0000001FE001FE00000001FE00FFE0000003E01F01FE0000F0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000F0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001F00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FFE00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FFE00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FFC00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FF00^FS
|
||||
^XZ
|
||||
|
After Width: | Height: | Size: 7.4 KiB |
@@ -0,0 +1,7 @@
|
||||
^XA^PW1200^LL600^LH0,0^CI28
|
||||
^FO51,52^GFA,6318,6318,81,000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003E0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FF8000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001FFC000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003E3E00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000780F00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000780F00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000780F00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000780F00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000780F00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000780F00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000780F000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003C1E000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003FFC000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001FF80000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007F0000000003800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007E00000000000000000000000000000000000000000000000000000000000000000000000000000000003E00000000000000000000000000000000000000000000000000000000000000000000000000007C0000F800000000000000000000000000000000000000000000000000000000000000000000000000003E0000000000000000000000000000000000000000000000000000000000000000000000000000FC0001F8003FFE0000000000000000000000000000000000000FFC00000001FFE000000000000FF000003E003FFFF00000003FFFFFC00003FFE00000000007FF800003FFFF80000000000007F000000000F80001FC00FFFF8000000000000000000000000000000000007FFF8000000FFFF800000000001FF000003E003FFFF80000007FFFFFC0000FFFF8000000003FFFF00007FFFFFC00000000000FF000000001F00001FC03FFFFE00000000000000000000000000000000000FFFFC000003FFFFE00000000003FF000003E003FFFF80000007FFFFFC0003FFFFE000000007FFFF80007FFFFFF00000000000FF800000001F00001F807FFFFF00000000000000000000000000000000001FFFFE000007FFFFF0000000000FFF000003E003FFFF80000007FFFFFC0007FFFFF00000001FFFFFE0007FFFFFF80000000000FF800000003E00000F80FFFFFFC0000000000000000000000000000000003FFFFF00000FFFFFFC000000001FFF000003E003FFFF80000007FFFFFC000FFFFFFC0000001FFFFFE0007FFFFFFC0000000001FF800000003C00000201FE003FE0000000000000000000000000000000007F807F80001FF007FE000000003F9F000003E0000FC000000007C00000001FE003FE0000003FC00FF0007E0007FE0000000001FFC00000007C00000001F8000FE0000000000000000000000000000000007E001F80003FC001FE000000007F1F000003E00007C000000007C00000001F8000FE0000007F0003F8007E0000FE0000000001F7C00000007800000003F00007F000000000000000000000000000000000FC000FC0007F00007F00000000FE1F000003E00007C000000007C00000003F00007F0000007E0001F8007E00007F0000000003E7C00000001000000003F00003C000000000000000000000000000000000F80007C000FE00003F80000001FC1F000003E00007C000000007C00000003F00003C0000007E0001F8007E00003F0000000003E7E00000000000000003E000018000000000000000000000000000000001F80007E000FC00001F80000003F81F000003E00007C000000007800000003E000018000000FC0000FC007E00003F0000000003E3E00000000000000007E000000000000000000000000000000000000001F00003E001F800000FC0000007F01F000003E00007C000000007800000007E000000000000FC0000FC007E00001F8000000007C3E00000000000000007E000000000000000000000000000000000000003F00003E001F800000FC000000FE01F000003E00007C000000007800000007E000000000000FC0000FC007E00001F8000000007C3F00000000000000007E0000000000FFC0000000FF000000FFC00000003F00003F001F0000007E000000FC01F000003E00007C00000000F800000007E0000000000Line truncated
|
||||
^FO53,149^GFA,6640,6640,83,0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003F80000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FFE0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001FFF0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003FFF8000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007FFFC000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007F1FE00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FE0FE00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FC07E00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FC07E00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FC07E00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FC07E00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FC07E00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FE0FE000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007F0FE000000003000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007FFFC000000007C00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003FFF8000000007F00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001FFF000000000FFC0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FFE000000000FFC0007C00000000000000000000003FE00000001FF8000000000000000000000000000000000000000003FE0000000000000000000000000000000000000000000000000000000000000000003F8000000001FF8001FF00000000000000000000003FE00000001FF8000000000000000000000000000000000000000003FE000000000000000000000000000000000000000000000000000000000000000000000000000003FF0003FF807FFFFE000000000000003FE00000001FF800000000FFF00000000FFF000000000001FFF80003FE007FFFFC0000007FFFFFF000007FFC00000000007FF800007FFFFE000000000000FFF000000003FE0003FF80FFFFFFF00000000000003FE00000001FF800000003FFFC0000007FFFE00000000003FFFC0003FE00FFFFFC000000FFFFFFF80003FFFF8000000003FFFF0000FFFFFFF00000000001FFF800000007FE0003FF80FFFFFFFC0000000000003FE00000001FF80000000FFFFF000001FFFFF80000000007FFFC0003FE00FFFFFC000000FFFFFFF8000FFFFFE00000000FFFFFC000FFFFFFFC0000000001FFF800000007FC0003FF80FFFFFFFE0000000000003FE00000001FF80000001FFFFF800007FFFFFC000000000FFFFC0003FE00FFFFFC000000FFFFFFF8001FFFFFF80000001FFFFFE000FFFFFFFE0000000001FFF80000000FF80003FF80FFFFFFFF0000000000003FE00000001FF80000003FFFFFC0000FFFFFFE000000001FFFFC0003FE00FFFFFC000000FFFFFFF8003FFFFFFC0000003FFFFFF000FFFFFFFF0000000003FFFC0000000FF00003FF80FFFFFFFF8000000000003FE00000001FF80000007FFFFFE0001FFFFFFF000000001FFFFC0003FE00FFFFFC000000FFFFFFF8007FFFFFFE0000007FFFFFF800FFFFFFFF8000000003FFFC0000000FE00003FF80FFFFFFFFC000000000003FE00000001FF8000000FFFFFFF0003FFFFFFF800000003FFFFC0003FE00FFFFFC000000FFFFFFF800FFFFFFFE000000FFFFFFFC00FFFFFFFFC000000003FFFC00000007E00001FF00FFFFFFFFC000000000003FE00000001FF8000000FFFFFFF0003FFFFFFFC00000007FFFFC0003FE00FFFFFC000000FFFFFFF801FFFFFFFC000000FFF87FFC00FFFFFFFFC000000007FFFC00000001C00000FE00FFFFFFFFC000000000003FE00000001FF8000001FFF0FFF8007FFFFFFFE0000000FFEFFC0003FE007FFFFC000000FFFFFFF801FFFFFFF8000000FFE01FFC00FFFFFFFFC000000007FFFE000000000000000000FFC001FFE000000000003FE00000001FF8000001FFC03FF800FFFC03FFE0000000FFCFFC0003FE0003FF00000000FF00000001FFC00FF0000001FFC00FFE00FFC001FFE000000007FFFE0000000000Line truncated
|
||||
^FO51,248^GFA,7840,7840,98,0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003F80000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FFE0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001FFF0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003FFF8000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007FFFC000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007E0FC00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FC07E00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FC07E00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FC07E00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FC07E00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FC07E00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FC07E00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FC07E000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007E0FC0000000C0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007FFFC0000000F8000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003FFF80000001FE000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001FFF80000001FF000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FFE00000003FF0003F0000000000000000000000000000000007FE0000000000000000000000000000000000000000FFC00000000000000000000000000000000000007FC000000000000000000000000000000000000000000000000000000000003F800000003FE0007F8000000000000000000000000000000007FE0000000000000000000000000000000000000000FFC00000000000000000000000000000000000007FC0000000000000000000000000000000000000000000000000000000000000000000007FC000FFC0007FF800000000000000000000000007FE0000000000000000000000000000000000000000FFC0000000FFF0000001FFE0000000000FFF80007FC03FFFFC000003FFFFFE0000FFF8000000001FFE0000FFFFF80000000000FFE00000007FC000FFC003FFFE00000000000000000000000007FE0000000000000000000000000000000000000000FFC0000003FFFC000007FFFC000000001FFFC0007FC03FFFFC000007FFFFFE0003FFFF00000000FFFFC000FFFFFF8000000001FFF00000007F8000FFC00FFFFF80000000000000000000000007FE0000000000000000000000000000000000000000FFC000000FFFFF00001FFFFF000000003FFFC0007FC03FFFFC000007FFFFFE000FFFFFC0000001FFFFE000FFFFFFE000000001FFF0000000FF0000FFC01FFFFFC0000000000000000000000007FE0000000000000000000000000000000000000000FFC000001FFFFF80003FFFFF800000007FFFC0007FC03FFFFC000007FFFFFE001FFFFFE0000007FFFFF800FFFFFFF000000001FFF8000000FF0000FFC03FFFFFE0000000000000000000000007FE0000000000000000000000000000000000000000FFC000003FFFFFC0007FFFFFC00000007FFFC0007FC03FFFFC000007FFFFFE003FFFFFF8000007FFFFF800FFFFFFF800000003FFF8000001FE0000FFC07FFFFFF0000000000000000000000007FE0000000000000000000000000000000000000Line truncated
|
||||
^FO53,349^GFA,8112,8112,104,000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001FE00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003FF00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007FF8000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000F87C000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001F03E000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001E01E000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001E01E000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001E01E000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001E01E000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001F03E000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000F03C000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FFFC0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007FF80000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003FF00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FC0000000003F000000003C00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007E000000007F0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FC00000000FF80FF8007FC000000000000000000000000000000000000000000003FF00000003FF000000000000000000FF00007FFFE0000003FFFFFF00000000000003FFFFFE000003FF000000000000000003FF000007FFF8000000000000000001FE000000001FC00000000FF80FF8007FC00000000000000000000000000000000000000000001FFFE000001FFFE00000000000000003FF00007FFFF0000003FFFFFF00000000000003FFFFFF00001FFFC0000000000000001FFFE00007FFFFC00000000000000001FE000000001F800000000FF80FF8007FC00000000000000000000000000000000000000000003FFFF000003FFFF00000000000000007FF00007FFFF0000003FFFFFF00000000000003FFFFFF00003FFFF0000000000000003FFFF00007FFFFF00000000000000003FF000000003F000000000FF80FFC00FFC0000000000000000000000000000000000000000000FFFFFC00007FFFF8000000000000000FFF00007FFFF0000003FFFFFF00000000000003FFFFFF00007FFFF800000000000000FFFFFC0007FFFFF80000000000000003FF000000007E000000000FF80FFC00FFC0000000000000000000000000000000000000000000FFFFFC0000FFFFFC000000000000003FFF00007FFFF0000003FFFFFF00000000000003FFFFFF0000FFFFFC00000000000000FFFFFC0007FFFFFC0000000000000003FF00000000FC0000000007F00FBC00F7C0000000000000000000000000000000000000000001FE01FE0001FF03FE000000000000007FFF00007FFFF0000003FFFFFF00000000000003FFFFFE0001FF03FE00000000000001FF03FE0007FFFFFE0000000000000003FF00000000FC0000000001C00FBC01F7C0000000000000000000000000000000000000000003FC00FF0001FC00FE00000000000000FFBF00000003F000000000FC0000000000000003E000000003FC00FE00000000000003FC00FF0007E001FF0000000000000007FF80Line truncated
|
||||
^FO53,447^GFA,8216,8216,104,0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003FF00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007FF8000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FFFC000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001FCFE000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001F03E000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003E01F000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003E01F000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003E01F000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003E01F000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003F03F000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001F87E000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001FFFE000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FFFC0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007FF80000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001FE000000000FF800000007F0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FF00000000FF8000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001FF00000000FFC0FFFF8000000000000007FFFF800000000003FC000000000000007FF80000007FF800000000000000000FF80007FFFF8000007FFFFFF80000000000007FFFFFF000007FF800000000000000007FF80000FFFF8000000000000000003FF000000003FE00000001FFC0FFFFFC00000000000007FFFF800000000003FC00000000000001FFFE000001FFFE00000000000000003FFC0007FFFF8000007FFFFFF80000000000007FFFFFF80001FFFE0000000000000001FFFE0000FFFFFC00000000000000007FF800000007FC00000001FFC0FFFFFF00000000000007FFFF800000000003FC00000000000007FFFF800007FFFF80000000000000007FFC0007FFFF8000007FFFFFF80000000000007FFFFFF80007FFFF8000000000000007FFFF8000FFFFFF00000000000000007FF800000007F800000001FFC0FFFFFFC0000000000007FFFF800000000003FC0000000000000FFFFFC0000FFFFFC000000000000001FFFC0007FFFF8000007FFFFFF80000000000007FFFFFF8000FFFFFC00000000000000FFFFFC000FFFFFFC0000000000000007FF80000000FF000000000FFC0FFFFFFE0000000000007FFFF800000000003FC0000000000001FFFFFE0001FFFFFE000000000000003FFFC0007FFFF8000007FFFFFF80000000000007FFFFFF8001FFFFFE00000000000001FFFFFE000FFFFFFE0000000000000007FF80000001FF000000000FF80FFFFFFF0000000000007FFFF800000000003FC0000000000003FFFFFF0003FFFFFF000000000000007FFFC0007FFFF8000007FFFFFF80000000000007FFFFFF8003FFFFFF00000000000003FFFFFF000FFFFFFF000000000000000FFFC0Line truncated
|
||||
^XZ
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
@@ -0,0 +1,3 @@
|
||||
^XA^PW406^LL203^LH0,0^CI28
|
||||
^FO16,16^GFA,8037,8037,47,FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE000001FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC8000000000000000000000000000000000000000000200000100000000000000000000000000000000000000000004800000000000000000000000000000000000000000020000010000000000000000000000000000000000000000000480000000000000000000000000000000000000000002000001000000000000000000000000000000000000000000048000000000000000000000000000000000000000000200000100000000000000000000000000000000000000000004800000000000000000000000000000000000000000020000010000000000000000000000000000000000000000000480000000000000000000000000000000000000000002000001000000000000000000000000000000000000000000048000000000000000000000000000000000000000000200000100000000000000000000000000000000000000000004800000000000000000000000000000000000000000020000010000000000000000000000000000000000000000000480000000000000000000000000000000000000000002000001000000000000000000000000000000000000F000000480F0000FC03FFF0007E0003C00F8001F003F001F80020000010000000000000000000000000FF0003FFC00F000000480F0003FF03FFF800FF8007E03FE003E007F003F80020000010000000000000000000000003FF8003FFC00F000000480F0007FF83FFF801FFC00FE07FF007C00FF007F80020000010000000000000000000000003FFC003FFC00F00001E480F000FFFC3FFF803FFE00FE0FFF80F801FF00FF80020000010000000000000000000000007C7C003C0000F00001E480F000F87E01E0007C3E01FE0F0781F001EF00F78002000001000000000000000000000000783C00380000F00003E480F001F03E01E000381E03FE1F0783E003CF01E78002000001000000000000000000000000783C00380000F0F0FF0480F001E01E01E000001E03BE1F07C3C0078F03C78002000001000000000000000000000000783E00380000F1F1FF8480F001E01E01E000003E07BE1E07C7CE038F01C78002000001000000000000000000000000783E0039F000F3E1E7C480F001E01F01E000003E0F3E1E07C7BF800F000780020000010000000000000000000000007C3E003BF800F3C3C3C480F003E01F01E000007C0E3E1E07C7FFC00F000780020000010000000000000000000000007FFC003BFC00F783C3C480F003E01F01E00000F81E3E1E07CFFFC00F000780020000010000000000000000000000003FFC003E7C00F783C3C480F003E01F01E00001F01C3E1E07CF83E00F000780020000010000000000000000000000001FBC00083E00FF83E3C480F001E01F01E00003E03C3E1E07CF83E00F000780020000010000000000000000000000000E7C00003E00FF81FF8480F001E01E01E00007C03FFF9F07CF81E00F00078002000001000000000000000000000000007800003E00FFC0FF0480F001F01E01E0000F803FFF9F07CF83E00F0007800200000100000000000000000000000000F800003E00FBC0FC0480F001F03E01E0003E003FFF8F078783E00F0007800200000100000000000000000000000001F018383E00F3E1800480FFF0FFFC01E0003FFE003E0F8F87C7C00F8007C00200000100000000000000000000000001F03C7C7C00F1E3C00480FFF07FFC01E0003FFF003E07FF03FFC1FFF0FFF80200000100000000000000000000000003E03C3FFC00F1F3FFC480FFF03FF801E0003FFF003E07FE03FF81FFF0FFF8020000010000000000000000000000000FC03C1FF800F0F1FFE480FFF01FE001E0003FFF003E01FC00FE01FFF0FFF8020000010000000000000000000000001F803C0FF000F0F8FFE480000000000000000000000000000000000000000002000001000000000000000000000000000000000000000181E480000000000000000000000000000000000000000002000001000000000000000000000000000000000000000380F480000000000000000000000000000000000000000002000001000000000000000000000000000000000000000381E4800000000000000000000000000000000000000000020000010000000000000000000000000000000000000003FFE4800000000000000000000000000000000000000000020000010000000000000000000000000000000000000003FFC4800000000000000000000000000000000000000000020000010000000000000000000000000000000000000000FF8480000000000000000000000000000000000000000002000001000000000000000000000000000000000000000000048000000000000000000000000000000000000000000200000100000000000000000000000000000000000000000004FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE000001FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE00000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000800000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000808000000C000000000000000000000000000000000000000000020000003000000000000000000000000002000000838000000E0000000000000000000000000000000000000000000E00000038000000000000000000000000020000008783E01E07007C0F80F80F03E07F80E0FF0387FE1F03E03E0FF01E0781F01C07003E00E0F80380F00F01CLine truncated
|
||||
^XZ
|
||||
|
After Width: | Height: | Size: 3.4 KiB |
@@ -0,0 +1,3 @@
|
||||
^XA^PW600^LL300^LH0,0^CI28
|
||||
^FO24,24^GFA,17388,17388,69,FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE000000007FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8000000000000000000000000000000000000000000000000000000000000000200000000400000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000000200000000400000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000000200000000400000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000000200000000400000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000000200000000400000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000000200000000400000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000000200000000400000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000000200000000400000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000000200000000400000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000000200000000400000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000000200000000400000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000000200000000400000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000000200000000400000000000000000000000000000000000000000000000000001F8000000001800000000000000000000000000000000000000000000000000000000000000020000000040000000000000000000000000000000000003F000000000000001F800000000180000000000300000000000000180000000000020000000000000000000000002000000004000000000000000000000000000000000001FFE000003FFFE0001F80000000018007F000007FF001FFFFF80001FFC00007F8007FF00003FE000FFC0007FE00002000000004000000000000000000000000000000000003FFF000007FFFE0001F80000000018007F00000FFFC01FFFFF80003FFE00007F800FFF80007FC001FFC000FFE00002000000004000000000000000000000000000000000007FFF800007FFFE0001F80000000018007F00003FFFE01FFFFF8000FFFF0000FF801FFFC000FF0003FFC001FFE0000200000000400000000000000000000000000000000000FFFFC00007FFFE0001F80000001F18007F00007FFFF01FFFFF8001FFFF8001FF803FFFE001FE0003FFC001FFE0000200000000400000000000000000000000000000000001FF3FC00007FFFE0001F80000003F18007F00007FFFF81FFFFF8001FFFFC001FF807FFFF003FC0007FFC003FFE0000200000000400000000000000000000000000000000001FC0FE00007E0000001F80000003F18007F0000FF07FC0FFFFF8003FC3FC003FF807F07F007F8000FEFC007F7E0000200000000400000000000000000000000000000000001F807E00007C0000001F80000003F18007F0000FE01FC001F800003F01FC007FF807E03F007F0001FCFC00FE7E0000200000000400000000000000000000000000000000003F807E00007C0000001F80FC03FFF18007F0001FC00FE001F800001F00FC007DF80FE03F80FE0001F8FC00FC7E0000200000000400000000000000000000000000000000003F807E00007C0000001F81FC0FFF018007F0001F800FE001F800000200FC00F9F80FC01F81FC0003F0FC01F87E0000200000000400000000000000000000000000000000003F807F00007C0000001F81FC1FFF818007F0001F8007E001F800000000FC00F9F80FC01F81F80003F0FC01F87E0000200000000400000000000000000000000000000000003F807F00007C0000001F83F83FFFC18007F0003F8007E001F800000001FC01F1F80FC01F83F8F001E0FC00F07E0000200000000400000000000000000000000000000000003F807F00007C7F00001F87F07F0FC18007F0003F8007E001F800000001FC03F1F81FC01FC3F3FE0040FC00207E0000200000000400000000000000000000000000000000003F807F00007CFFC0001F8FE07E07E18007F0003F8007F001F800000003F803E1F81FC01FC3F7FF0000FC00007E0000200000000400000000000000000000000000000000001FC0FF00007DFFE0001F8FC07E07E18007F0003F8007F001F800000003F807C1F81FC01FC7FFFF8000FC00007E0000200000000400000000000000000000000000000000001FF3FE00007DFFF0001F9FC07E07E18007F0003F8007F001F800000007F00FC1F81FC01FC7FFFF8000FC00007E0000200000000400000000000000000000000000000000001FFF7E00007FFFF0001F9F807E07E18007F0003F8007F001F80000001FE00F81F81FC01FC7F83FC000FC00007E0000200000000400000000000000000000000000000000000FFF7E00007F07F0001FBF007E07E18007F0003F8007F001F80000003FC01F81F81FC01FC7F01FC000FC00007E00002000000004000000000000000000000000000000000007FEFE00007C03F8001FFF007E07E18007F0003F8007E001F80000007F801F01F81FC01FC7F00FC000FC00007E00002000000004000000000000000000000000000000000003FCFE00000003F8001FFF803F9FE18007F0003F8007E001F8000000FF003F01F80FC01F87E00FE000FC00007E00002000000004000000000000000000000000000000000000F0FC00000003F8001FFF803FFFC18007F0001F8007E001F8000001FE003FFFFF8FC01F87E00FE000FC00007E0000200000000400000000000000000000000000000000000001FC00000003F8001FFFC01FFF81800Line truncated
|
||||
^XZ
|
||||
|
After Width: | Height: | Size: 8.4 KiB |
@@ -0,0 +1,5 @@
|
||||
^XA^PW1200^LL600^LH0,0^CI28
|
||||
^FO48,48^GFA,16402,16402,139,FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE00000000000000003FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE00000000000000003FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80C0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060000000000000000300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000180C0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060000000000000000300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000180C0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060000000000000000300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000180C0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060000000000000000300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000180C0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060000000000000000300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000180C0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060000000000000000300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000180C0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060000000000000000300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000180C0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060000000000000000300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000180C0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060000000000000000300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000180C0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060000000000000000300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000180C0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060000000000000000300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000180C0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060000000000000000300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000180C0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060000000000000000300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000180C0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060000000000000000300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000180C0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060000000000000000300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000180C000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006000000000000000030000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000Line truncated
|
||||
^FO48,178^GFA,12220,12220,130,FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEC0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006C0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006C0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006C0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006C0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006C0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006C0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006C0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006C0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006C0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006C0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006C0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006C0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006C0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006C0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006C0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006C0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006C0000000000000000000000000000Line truncated
|
||||
^FO48,284^GFA,28783,28783,107,FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0C0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000C0C0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000C0C0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000C0C0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000C0C0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000C0C0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000C0C0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000C0C0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000C0C0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000C0C0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000C0C0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000C0C0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000C0C0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000C0C0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000C0C0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000C0C0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000C0C0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000C0C0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000C0C0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000C0C0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000C0C0000001800000000000000000000600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060000000000000000000018000000000000000000000000C0C0000007800000000000000000000780000000000000000Line truncated
|
||||
^XZ
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,3 @@
|
||||
^XA^PW406^LL203^LH0,0^CI28
|
||||
^FO26,26^GFA,6795,6795,45,0000300000000000000003C03FE00003C07C01E03E03E0001FF07FE1E01E01E0000000000000000000000000000000780000000000000003C0FFF00003C03C01E03E07E0003FF87FE1E01E01E0000000000000000000000001C00000780000000000000003C0FFF80003C03C03E07E07E0007FFE7FE1E01E01E0000000000000000000000001C00000780000000000000003C1FDF80003C03E03C07F07F000FEFC1F01E01E01E0000000000000000000000001C00000780000000000000003C1F07C0003C01E03C07F07F000F8180F01E01E01E0000000000000000000000001C00000780000000000000003C1E07C0003C01FFFC07F07F000F0000F01E01E01E0000000000000000000000001C0FFFFF80000000000000003C1E03C0003C01FFF80F70FF000F0000F01E01E01E000000000000000000007FFFFC0FFFFF800000000000003FFC1E03C0003C00FFF80F78E7000F8000F01E01E01E000000000000000000007FFFFC0FFFFF800000000000007FFC1E03C0007C00F0F80F78E7800FFC00F01FFFE01E000000000000000000007FFFFC00000780000000000000FFFC3E03C0007E00F8F00E38E78007FF00F01FFFE01E0000000000000000000078001C00000780000000000001FFFC3E03C000FF0078F00E39E78003FF80F01FFFE01E000000000000000000003C001C00000780000000000001E03C3E03C000FF0078F01E3DC78000FFC0F01FFFE01E000000000000000000001C001C00000780000000000001E03C3E03C001EF8079E01E3DC3C00007C0F01E01E01E000000000000000000001E001C00000780000000000001E03C3E03C003E7803DE01E3DC3C00003C0F01E01E01E000000000000000000000F001C00000300000000000001E03C3E03C003C7C03DE01C1FC3C00003C0F01E01E01E0000000000000000000007001C00000000000000000001F03C3E03C007C3C03FC03C1FC3C00307C0F01E01E01E0000000000000000000000000000000000000000000001FFFC3E03C00783E01FC03C1F83C007FFC7FC1E01E7FFF000000000000000000000000000000000000000000000FFFC3E03C00F81F01FC03C1F81E00FFF87FE1E01E7FFF00000000000000000000000000FFFFF000000000000007FFC3E03C00F01F01F803C0F81E003FF07FE1E01E7FFF00000000000000000000000000FFFFF800000000000001FFC1C03C01F00F00F80380F01E000FC07FC1E01E3FFF000000000000000000007FFE00FFFFF8000000000000000000000000000000000000000000000000000000000000000000000000000001FFFF00FFFFF8000000000000000000000000000000000000000000000000000000000000000000000000000003FFFF80E07078000000000000000000000000000000000000000000000000000000000000000000000000000003C007C0E070780000000000000000000000000000000000000000000000000000000000000000000000000000078001C0E070780000000000000000000000000000000000000000000000000000000000000000000000000000070381C0E070780000000000000000000000000000000000000000000000000000000000000000000000000000070381C0E070780000000000000000000000000000000000000000000000000000000000000000000000000000070381C0E070780000000000000000000000000000000000000000000000000000000000000000000000000000078001C0E07078000000000000000000000000000000000000000000000000000000000000000000000000000003C007C0E07078000000000000000000000000000000000000000000000000000000000000000000000000000003FFFF80E00030000000000000000000000000000000000000000000000000000000000000000000000000000001FFFF000000000000000000000000000000000000000000000000000000000000000000000000000000000000007FFE000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001C07800000000000000000000000000000000000000000000000000000000000000000000000000000007FFE007C1FE0000000000000000000000000000000000000000000000000000000000000000000000000000001FFFF007C3FF0000000000000000000000000000000000000000000000000000000000000000000000000000003FFFF80F03FF0000003FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8000000000003C007C0E07878000003FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80000000000078001C0E07838000003FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80000000000070381C0E07038000003FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80000000000070381C0E07038000003FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80000000000070381C0E0F078000003FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80000000000078001C0F1F1F0000003FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8000000000003C007C07FE1F0000003FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8000000000003FFFF807FC1E0000003FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8000000000001FFFF001F8180000003FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80000000000007FFE00000000000003FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8000000000000000000000000000003FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8000000000000000000000000000003FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8000000000000000000000030000003FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8000000000000000000000078000003FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80000000000007FFE00000078000003FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8000000000001FFFF00000078000003FFFFFFFFFFFFFFFFFFFFE03FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8000000000003FFFF80000078000003FFFFFFFFFFFLine truncated
|
||||
^XZ
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
@@ -0,0 +1,3 @@
|
||||
^XA^PW600^LL300^LH0,0^CI28
|
||||
^FO40,39^GFA,14430,14430,65,000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001F0000003C000000000000000000000003F8007FF00000007F003F8001F803FC00FE000000FFE00FFFC0FC007F000FC000000000000000000000000000000000001F0000003C000000000000000000000003F801FFFC0000007F001F8003F803FC00FF000003FFF80FFFC0FC007F000FC000000000000000000000000000000000001F0000003C000000000000000000000003F803FFFE0000007F001FC003F003FC01FF000007FFFE0FFFC0FC007F000FC000000000000000000000000000000000001F0000003C000000000000000000000003F807FFFF0000007F001FC003F003FC01FF00000FFFFF0FFFC0FC007F000FC000000000000000000000000000000000001F0000003C000000000000000000000003F80FFFFF8000007F000FC007F007FE01FF00001FFFFF0FFFC0FC007F000FC000000000000000000000000000000000001F0000003C000000000000000000000003F80FF07F8000007F000FE007E007FE01FF00001FC0FE00FC00FC007F000FC000000000000000000000000000003FFFFFFF0000003C000000000000000000000003F80FE03F8000007F000FE007E007FE03FF80003F803C00FC00FC007F000FC000000000000000000000000000003FFFFFFF0000003C000000000000000000000003F80FC01FC000007F0007FFFFE007FE03FF80003F801800FC00FC007F000FC000000000000000000000000000003FFFFFFFFFFFFFFC000000000000000000000003F81FC01FC000007F0007FFFFC00FFF03EF80003F000000FC00FC007F000FC000000000000000000000000000003FFFFFFFFFFFFFFC000000000000000000000003F81F800FC000007F0007FFFFC00FDF03EF80003F800000FC00FC007F000FC000000000000000000000000000003FFFFFFFFFFFFFFC0000000000000000000000FFF81F800FC000007F0003FFFFC00F9F03CFC0003F800000FC00FC007F000FC000000000000000000000000000003FFFFFFFFFFFFFFC000000000000000000001FFFF81F800FC000007F0003FFFF800F9F07CFC0003FE00000FC00FC007F000FC000000000000000000000000000003E00001FFFFFFFFC000000000000000000007FFFF81F800FC000007F0003F81F800F9F87CFC0001FFF8000FC00FFFFFF000FC000000000000000000000000000003F00001F0000003C00000000000000000000FFFFF81F800FC00000FF8001F81F801F8F87CFC0001FFFE000FC00FFFFFF000FC000000000000000000000000000001F80001F0000003C00000000000000000000FFFFF81F800FC00000FF8001F81F001F8F8787C0000FFFF800FC00FFFFFF000FC000000000000000000000000000000F80001F0000003C00000000000000000001FFFFF81F800FC00001FFC001F83F001F0F8F87E00007FFFC00FC00FFFFFF000FC000000000000000000000000000000FC0001F0000003C00000000000000000001FC03F81F800FC00003FFE000FC3F001F0F8F87E00001FFFE00FC00FFFFFF000FC0000000000000000000000000000007E0001F0000003C00000000000000000003F803F81F800FC00003FFE000FC3E003F0FCF87E000001FFE00FC00FFFFFF000FC0000000000000000000000000000003F0001F0000003C00000000000000000003F803F81F800FC00007F7F000FC7E003F07CF87E0000001FE00FC00FC007F000FC0000000000000000000000000000001F0000F0000003C00000000000000000003F803F81F800FC00007E7F0007E7E003F07DF03F00000007E00FC00FC007F000FC00000000000000000000000000000000000000000003C00000000000000000003F803F81F800FC0000FE3F8007E7C003E07DF03F00000007F00FC00FC007F000FC00000000000000000000000000000000000000000000000000000000000000003F803F81F800FC0000FC3F8007EFC007E07FF03F00000007E00FC00FC007F000FC00000000000000000000000000000000000000000000000000000000000000001F803F81F800FC0001FC1FC003FFC007E03FF03F00003007E00FC00FC007F000FC00000000000000000000000000000000000000000000000000000000000000001FE03F81F800FC0003F80FC003FF8007E03FE03F80007C0FE00FC00FC007F0FFFFF8000000000000000000000000000000000000000000000000000000000000001FFFFF81F800FC0003F80FE003FF8007E03FE01F8000FFFFE0FFFC0FC007F0FFFFFC000000000000000000000000000000000000000000000000000000000000000FFFFF81F800FC0007F007F001FF8007C03FE01F8001FFFFC0FFFC0FC007F0FFFFFC00000000000000000000000000001FFFFE000000000000000000000000000007FFFF81F800FC0007F007F001FF000FC01FE01F8000FFFF80FFFC0FC007F0FFFFFC00000000000000000000000000007FFFFF8FFFFFFFC000000000000000000003FFFF81F800FC000FE003F801FF000FC01FC01F80007FFF00FFFC0FC007F0FFFFFC0000000000000000000000000000FFFFFFCFFFFFFFC000000000000000000000FFFF81F800FC000FE003F800FF000FC01FC01FC0001FFC00FFFC0FC007F0FFFFFC0000000000000000000000000001FFFFFFEFFFFFFFC00000000000000000000000000000000000000000000000000000000000000001C0000000000000000000000000000000000000000000000003FFFFFFEFFFFFFFC0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003F00003FFFFFFFFC0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003E00001FF807C03C0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007C01E00FF807C03C0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007C03E00FF807C03C0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007C03E00FF807C03C0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007C03E00FF807C03C0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007C01E00FF807C03C00000000000000000000000Line truncated
|
||||
^XZ
|
||||
|
After Width: | Height: | Size: 5.2 KiB |
@@ -0,0 +1,5 @@
|
||||
^XA^PW1200^LL600^LH0,0^CI28
|
||||
^FO81,79^GFA,11830,11830,130,0000000000001FF80000000000000000000000000000000000000000000000000000000007FFC000000000000000000000000000000000000000000000000000000000000000000007FF80000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001FF800000000000000000000000000000000000000000000000FFF800000FFFFFC000000000000007FF800000FFF8000000FFF80001FFF800003FFF80000000000007FFFF80000FFFFFFF003FFE000007FFC000003FFE0000000000000000000000000000000000000000000000000000000000000000000000003FE0000000000001FF800000000000000000000000000000000000000000000000FFF800003FFFFFF000000000000007FF800000FFF8000000FFF80003FFFC00003FFF8000000000003FFFFFE0000FFFFFFF003FFE000007FFC000003FFE0000000000000000000000000000000000000000000000000000000000000000000000003FE0000000000001FF800000000000000000000000000000000000000000000000FFF80000FFFFFFFC00000000000007FF800000FFFC000000FFF80003FFFC00007FFF800000000000FFFFFFF8000FFFFFFF003FFE000007FFC000003FFE0000000000000000000000000000000000000000000000000000000000000000000000003FE0000000000001FF800000000000000000000000000000000000000000000000FFF80001FFFFFFFE00000000000007FF8000007FFC000001FFF00003FFFC00007FFF800000000001FFFFFFFE000FFFFFFF003FFE000007FFC000003FFE0000000000000000000000000000000000000000000000000000000000000000000000003FE0000000000001FF800000000000000000000000000000000000000000000000FFF80003FFFFFFFF00000000000007FF8000007FFC000001FFF00003FFFC00007FFF800000000003FFFFFFFF000FFFFFFF003FFE000007FFC000003FFE0000000000000000000000000000000000000000000000000000000000000000000000003FE0000000000001FF800000000000000000000000000000000000000000000000FFF80007FFFFFFFF80000000000007FF8000007FFC000001FFF00003FFFE00007FFFC00000000007FFFFFFFFC00FFFFFFF003FFE000007FFC000003FFE0000000000000000000000000000000000000000000000000000000000000000000000003FE0000000000001FF800000000000000000000000000000000000000000000000FFF8000FFFFFFFFFC0000000000007FF8000003FFE000001FFE00007FFFE0000FFFFC0000000000FFFFFFFFFE00FFFFFFF003FFE000007FFC000003FFE0000000000000000000000000000000000000000000000000000000000000000000000003FE0000000000001FF800000000000000000000000000000000000000000000000FFF8000FFFFFFFFFE0000000000007FF8000003FFE000003FFE00007FFFE0000FFFFC0000000001FFFFFFFFFF00FFFFFFF003FFE000007FFC000003FFE0000000000000000000000000000000000000000000000000000000000000000000000003FE0000000000001FF800000000000000000000000000000000000000000000000FFF8001FFFFFFFFFE0000000000007FF8000003FFE000003FFE00007FFFE0000FFFFC0000000003FFFFFFFFFF80FFFFFFF003FFE000007FFC000003FFE0000000000000000000000000000000000000000000000000000000000000000000000003FE0000000000001FF800000000000000000000000000000000000000000000000FFF8001FFFFFFFFFF0000000000007FF8000001FFF000003FFC00007FFFE0000FFFFE0000000003FFFFFFFFFF00FFFFFFF003FFE000007FFC000003FFE0000000000000000000000000000000000000000000000000000000000000000000000003FE0000000000001FF800000000000000000000000000000000000000000000000FFF8003FFFF03FFFF0000000000007FF8000001FFF000007FFC0000FFFFF0001FFFFE0000000003FFFE03FFFE0000FFF00003FFE000007FFC000003FFE0000000000000000000000000000000000000000000000000000000000000000000000003FE0000000000001FF800000000000000000000000000000000000000000000000FFF8003FFF8007FFF0000000000007FF8000001FFF000007FFC0000FFFFF0001FFFFE0000000007FFF0007FFC0000FFF00003FFE000007FFC000003FFE0000000000000000000000000000000000000000000000000000000000000000000000003FE0000000000001FF800000000000000000000000000000000000000000000000FFF8003FFF0003FFF8000000000007FF8000000FFF800007FF80000FFFFF0001FFFFE0000000007FFE0001FF80000FFF00003FFE000007FFC000003FFE0000000000000000000000000000000000000000000000000000000000000000000000007FE0000000000001FF800000000000000000000000000000000000000000000000FFF8007FFE0001FFF8000000000007FF8000000FFF80000FFF80000FFFFF0001FFFFE0000000007FFC00007F00000FFF00003FFE000007FFC000003FFE000000000000000000000000000000000000000000000000000000000003FFFFFFFFFFFFFFE0000000000001FF800000000000000000000000000000000000000000000000FFF8007FFC0000FFF8000000000007FF8000000FFFFFFFFFFF80001FFFFF8001FFFFF000000000FFF800003E00000FFF00003FFE000007FFC000003FFE000000000000000000000000000000000000000000000000000000000003FFFFFFFFFFFFFFEFFFFFFFFFFFFFFF800000000000000000000000000000000000000000000000FFF8007FFC0000FFF8000000000007FF80000007FFFFFFFFFF00001FFDFF8003FF7FF000000000FFF800001C00000FFF00003FFE000007FFC000003FFE000000000000000000000000000000000000000000000000000000000003FFFFFFFFFFFFFFEFFFFFFFFFFFFFFF800000000000000000000000000000000000000000000000FFF8007FF800007FF8000000000007FF80000007FFFFFFFFFF00001FFDFF8003FF7FF000000000FFF800000800000FFF00003FFE000007FFC000003FFE000000000000000000000000000000000000000000000000000000000003FFFFFFFFFFFFFFEFFFFFFFFFFFFFFF800000000000000000000000000000000000000000000000FFF8007FF800007FFC000000000007FF80000007FFFFFFFFFF00001FF9FF8003FE7FF000000000FFF800000000000FFF00003FFE000007FFC000003FFE000000000000000000000000000000000000000000000000000000000003FFFFFFFFFFFFFFEFFFFFFFFFFFFFFF800000000000000Line truncated
|
||||
^FO80,180^GFA,31571,31571,131,0007C00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003FC000003FC00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FFC00001FFF8000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001FFFFFFFFF8000001FFC00007FFFE00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FFFFFFFFFFE000003FFC0001FFFFF00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003FFFFFFFFFFF800007FFC0003FFFFF80000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007FFFFFFFFFFFC0000FFFC0007FFFFFC000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FFFFFFFFFFFFE0001FFFC000FFFFFFE000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001FFFFFFFFFFFFF0001FFFC000FFFFFFF000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003FFFFFFFFFFFFF8003FFFC001FFFFFFF000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007FFFFFFFFFFFFFC003FFE0003FFFFFFF8000000000000000007FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF800000000000000000000000000000000007FFFFFFFFFFFFFC003FF80003FFF0FFF8000000000000000007FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80000000000000000000000000000000000FFFFFFFFFFFFFFE007FF00007FFC03FFC000000000000000007FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80000000000000000000000000000000000FFF000000001FFE007FE00007FF800FFC000000000000000007FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80000000000000000000000000000000001FFE0000000007FF007FE00007FF000FFC000000000000000007FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80000000000000000000000000000000001FF80000000003FF007FC0000FFE0007FC000000000000000007FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80000000000000000000000000000000001FF80000000003FF00FFC0000FFE0007FC000000000000000007FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80000000000000000000000000000000001FF00003F80001FF00FFC0000FFE0007FE000000000000000007FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80000000000000000000000000000000001FF00007FC0001FF80FF80000FFC0003FE000000000000000007FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80000000000000000000000000000000003FF00007FLine truncated
|
||||
^FO80,431^GFA,11830,11830,130,000FFFFFFFFFC0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003FFFFFFFFFF8000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001FF00000000001FF00FFFFFFFFFFFE000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001FF00000000001FF01FFFFFFFFFFFF000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001FF00000000001FF03FFFFFFFFFFFF800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001FF0000FF80001FF07FFFFFFFFFFFFC00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001FF0000FF80001FF0FFFFFFFFFFFFFE00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001FF0000FF80001FF1FFFFFFFFFFFFFF00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001FF0000FF80001FF1FFFFFFFFFFFFFF00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001FF0000FF80001FF3FFFFFFFFFFFFFF80000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001FF0000FF80001FF3FFC000000007FF80000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001FF0000FF80001FF7FF0000000001FFC0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001FF0000FF80001FF7FE0000000000FFC0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001FF0000FF80001FF7FE0000000000FFC0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001FF0000FF80001FF7FC0000FE00007FC0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001FF0000FF80001FFFFC0001FF00007FC0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001FF0000FF80001FFFF80001FF00007FE0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001FF0000FF80001FFFF80001FF80003FE0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001FF0000FF80001FFFF80003FF80003FE0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001FF0000FF80001FFFF80003FF80003FE0000000000000Line truncated
|
||||
^XZ
|
||||