diff --git a/.editorconfig b/.editorconfig
index f579ff5d..9592a0c0 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -19,6 +19,7 @@ indent_size = 4
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
+max_line_length = 160
##########################################
# File Extension Settings
@@ -134,6 +135,10 @@ csharp_style_prefer_null_check_over_type_check = true:warning
# C# Style Rules
# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/language-rules#c-style-rules
[*.{cs,csx,cake}]
+# Report lines exceeding the shared editor limit during analysis and builds.
+roslynator_max_line_length = 160
+dotnet_diagnostic.RCS0056.severity = warning
+
# 'var' preferences
csharp_style_var_for_built_in_types = false:error
csharp_style_var_when_type_is_apparent = false:error
diff --git a/.gitattributes b/.gitattributes
index 4b946243..e594c0e9 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -140,6 +140,8 @@
*.avif filter=lfs diff=lfs merge=lfs -text
*.hvc1 filter=lfs diff=lfs merge=lfs -text
*.hvcc filter=lfs diff=lfs merge=lfs -text
+*.ivf filter=lfs diff=lfs merge=lfs -text
+*.y4m filter=lfs diff=lfs merge=lfs -text
*.yuv filter=lfs diff=lfs merge=lfs -text
*.bit filter=lfs diff=lfs merge=lfs -text
###############################################################################
diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml
index b0457857..c2e5a814 100644
--- a/.github/workflows/build-and-test.yml
+++ b/.github/workflows/build-and-test.yml
@@ -81,7 +81,7 @@ jobs:
sdk-preview: true
runtime: -x64
codecov: false
- - os: ubuntu-22.04-arm
+ - os: ubuntu-24.04-arm
framework: net11.0
sdk: 11.0.x
sdk-preview: true
@@ -103,7 +103,7 @@ jobs:
sdk: 10.0.x
runtime: -x64
codecov: false
- - os: ubuntu-22.04-arm
+ - os: ubuntu-24.04-arm
framework: net10.0
sdk: 10.0.x
runtime: -x64
diff --git a/.gitignore b/.gitignore
index 4e61c3c8..81d70e71 100644
--- a/.gitignore
+++ b/.gitignore
@@ -234,3 +234,7 @@ SixLabors.Shapes.Coverage.xml
/plans/
samples/Avalonia**
+
+# Specifications are licensed documents and stay on the local machine only.
+/src/ImageSharp.Drawing/Barcodes/specs/
+/src/ImageSharp.Drawing.FontGenerator/specs/
diff --git a/AGENTS.md b/AGENTS.md
index 814e3ce2..38cc8eae 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -4,9 +4,12 @@ These instructions apply to the entire repository. More-specific `AGENTS.md` fil
## Working Practices
-- Inspect the relevant implementation, tests, benchmarks, project files, and nearby code before proposing or making changes. Do not infer current behavior when the source is available.
+- Inspect only the source and supporting files needed for the task before proposing or making changes. Do not infer current behavior when the source is available.
- Make the smallest complete change that solves the requested problem. Avoid unrelated cleanup, speculative abstractions, and formatting churn.
-- Match established architecture, naming, formatting, documentation, and test patterns. Treat `.editorconfig`, analyzers, and repository build settings as authoritative.
+- Trace the existing execution path before changing it. Extend and optimize the existing implementation within the requested behavior and affected path.
+- Before implementing an operation, find and inspect the existing APIs that provide it. Use or extend those APIs instead of writing equivalent logic elsewhere. This includes color conversion, pixel conversion, memory management, and vectorized operations. If an API lacks required behavior, improve its owning implementation rather than bypassing it with a separate implementation.
+- Follow existing naming, formatting, documentation, and test patterns. Treat `.editorconfig`, analyzers, and repository build settings as authoritative.
+- Use simple technical English in all communication, documentation, and comments. Use short sentences, active voice, and existing code terminology.
- Preserve public API and observable behavior unless the task explicitly requires a change. Public API documentation must describe observable behavior, not implementation details.
- Do not use reflection against built assemblies, ad hoc assembly loading, or temporary probe projects unless explicitly requested.
- Build .NET projects in Release configuration unless explicitly instructed otherwise.
@@ -15,27 +18,26 @@ These instructions apply to the entire repository. More-specific `AGENTS.md` fil
- Treat throughput, latency, memory use, and binary size as design constraints, especially in pixel-processing, drawing, parsing, encoding, and other hot paths.
- Avoid unnecessary allocations, copies, boxing, closures, interface dispatch, repeated enumeration, and extra passes over data.
-- Reuse the repository's existing memory ownership, pooling, span, vectorization, and parallelization patterns. Do not introduce a new mechanism when an established one fits.
-- Keep hot loops simple and bounds-check-friendly. Hoist invariant work, preserve locality, and use the narrowest suitable data types without sacrificing correctness.
-- Do not trade correctness or maintainability for assumed speed. Support non-obvious optimizations with measurements or clear evidence, and add or update benchmarks when performance is the purpose of the change.
+- Reuse existing memory ownership, pooling, span, vectorization, and parallelization mechanisms.
+- Use a SIMD-first design for suitable data processing code. Build on existing vectorized operations, dispatch mechanisms, and scalar fallbacks. Keep hot loops simple, hoist invariant work, and preserve memory locality.
+- Preserve correctness and maintainability during optimization. Distinguish source-based reasoning from measured results. Claim a speedup only with measurements. Use existing benchmarks first. Add or update cases only when they measure relevant behavior that existing benchmarks miss.
- Consider all supported target frameworks and runtime capabilities. Do not regress fallback paths while optimizing newer runtimes.
## C# Conventions
-- Follow the existing code around the change; local patterns take precedence over generic preferences.
+- Follow local patterns when they comply with the explicit rules.
- Do not use `record` or `record struct` types.
- Prefer established invariants over redundant guards. Validate at real external boundaries and do not add defensive checks for internally controlled states.
-- Do not extract single-use helpers merely to name a block. Extract only for genuine reuse, an established local pattern, or meaningful complexity reduction.
+- Do not extract single-use helpers merely to name a block. Extract only for genuine reuse, an established local pattern, or independently complex logic.
- Add vertical whitespace after multi-line statements and declarations and between distinct logical stages. Never add trailing whitespace.
-- Document every method, constructor, and property, regardless of whether it is public, internal, protected, or private. Keep public API documentation limited to observable behavior; use private and internal documentation to capture the contract and intent needed to maintain the code.
-- Add inline comments throughout complex code. Explain algorithms, formulas, invariants, ownership, compatibility behavior, and performance tradeoffs at the operations and decisions they govern. Explain why the code is shaped that way rather than narrating the syntax.
-- Document SIMD code especially thoroughly. Explain the vector layout, lane meaning, widening or narrowing, masks, shuffles, constants, alignment or remainder handling, supported instruction paths, scalar equivalence, and the reason each non-obvious operation is correct.
-- Write algorithm and SIMD comments for a maintainer who is unfamiliar with the implementation. The reader should not need to reconstruct intent from external documentation, issue history, or benchmark results.
+- Document every added method, constructor, and property, regardless of visibility. Update documentation when a member's behavior or contract changes. Keep public API documentation limited to observable behavior. Use private and internal documentation to explain the contract and intent.
+- Add inline comments that explain why complex code works. Explain algorithms, formulas, invariants, memory ownership, compatibility behavior, and performance decisions where they apply.
+- For SIMD, explain lane layouts, operations, alignment, remainders, and scalar equivalence so an unfamiliar maintainer can follow the code.
## Verification
-- Add or update focused tests when behavior changes, following the test framework and conventions already used by the project.
-- Never hack, weaken, skip, conditionally bypass, or otherwise manipulate a test to make it pass. Fix the production defect or the genuine test defect while preserving the test's intended coverage and sensitivity.
-- Do not update golden files, reference images, snapshots, baselines, or expected-output artifacts to resolve a test failure. Treat a mismatch as evidence to investigate and correct the implementation.
-- Run the narrowest relevant formatting, test, and Release build commands, then expand verification in proportion to the risk and scope of the change.
+- Add or update tests only when they prove a required behavior or expose a real defect. Derive expectations from established contracts, explicit requirements, or defect evidence. Use source to identify contracts and integration points, not to copy implementation results into expectations. Prefer extending existing tests. Avoid redundant cases and assertions that merely repeat the implementation.
+- Never weaken, skip, or bypass a valid test to make it pass. Fix the production defect or the genuine test defect. Replace or remove a test only with evidence that its expectation is invalid or its coverage is redundant.
+- Do not update golden files, reference images, snapshots, baselines, or expected-output artifacts merely to silence a failure. Investigate mismatches. Update expected results only for an established requirement change or an incorrect reference result proved by independent contract or defect evidence.
+- Test the actual behavior through the existing execution path. Synthetic inputs must exercise an established requirement or reproduce a real defect. Do not invent requirements or substitute internal structure and helper calls for assertions about required behavior. Run the narrowest relevant formatting, test, and Release build commands.
- Report what changed, the verification performed, and any remaining risks or unverified assumptions.
diff --git a/ImageSharp.Drawing.All.slnx b/ImageSharp.Drawing.All.slnx
index 210a9644..60f1ffed 100644
--- a/ImageSharp.Drawing.All.slnx
+++ b/ImageSharp.Drawing.All.slnx
@@ -18,6 +18,9 @@
+
+
+
diff --git a/ImageSharp.Drawing.slnx b/ImageSharp.Drawing.slnx
index 8ba71a6c..e2cada87 100644
--- a/ImageSharp.Drawing.slnx
+++ b/ImageSharp.Drawing.slnx
@@ -23,6 +23,9 @@
+
+
+
diff --git a/SECURITY.md b/SECURITY.md
index 8b2e1df8..2e060f62 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -2,16 +2,23 @@
## Supported Versions
-Six Labors provides security fixes only for the latest major version of each library.
+Six Labors provides security updates for the latest major version of each library.
-Older major versions are end-of-life and do not receive security fixes.
+Each major version remains eligible for security updates for 12 months after the first stable release of the next major version of that library.
+After this period, that major version is end-of-life and does not receive security updates.
-Users must upgrade to the latest major version to receive security fixes.
+Users must install the latest available patch or minor release within a supported major version to receive security fixes.
+Older releases within that major version are not maintained separately.
-| Version | Supported |
-| -------------------- | --------- |
-| Latest major version | Yes |
-| Older major versions | No |
+| Version | Supported |
+| ------- | --------- |
+| Latest major version | Yes |
+| Superseded major version within 12 months of the next major's first stable release | Yes |
+| Superseded major version after that 12-month period | No |
+
+Security updates may require a license key under the existing license terms. This does not change the license terms.
+
+This policy covers security fixes only. It does not include feature backports or extend support for underlying .NET versions.
Security fixes, if any, are provided at Six Labors' discretion.
diff --git a/THIRD-PARTY-NOTICES b/THIRD-PARTY-NOTICES
index 29b7b5c7..261aa50a 100644
--- a/THIRD-PARTY-NOTICES
+++ b/THIRD-PARTY-NOTICES
@@ -38,3 +38,77 @@ MIT License. The full license texts are available in the Vello repository
LICENSE-APACHE and LICENSE-MIT files.
================================================================================
+
+.NET Runtime (https://github.com/dotnet/runtime)
+License: MIT
+
+Portions of this software were developed with reference to the .NET runtime
+project, including the pooled string and list building buffers.
+
+Copyright (c) .NET Foundation and Contributors
+
+The .NET runtime project is distributed under the MIT License. The full license
+text is available in the .NET runtime repository LICENSE.TXT file.
+
+================================================================================
+
+Barcode Writer in Pure PostScript (https://github.com/bwipp/postscriptbarcode)
+License: MIT
+
+Portions of this software were developed with reference to the Barcode Writer
+in Pure PostScript project, including barcode symbology encodation.
+
+Copyright (c) 2004-2024 Terry Burton
+
+Barcode Writer in Pure PostScript is distributed under the MIT License. The
+full license text is available in the project repository LICENSE file.
+
+================================================================================
+
+bwip-js (https://github.com/metafloor/bwip-js)
+License: MIT
+
+Portions of this software were developed with reference to the bwip-js
+project, including barcode symbology encodation.
+
+Copyright (c) 2011-2026 Mark Warren
+
+The bwip-js project is distributed under the MIT License. The full license text
+is available in the bwip-js repository LICENSE file.
+
+================================================================================
+
+Zint (https://github.com/zint/zint)
+License: BSD-3-Clause
+
+Portions of this software were developed with reference to the Zint barcode
+library backend, including barcode symbology encodation.
+
+Copyright (C) 2008-2026 Robin Stuart and the Zint contributors
+
+The Zint backend is distributed under the BSD 3-Clause License. The full
+license text is available in the Zint repository LICENSE file.
+
+The GS1 DataBar element width routine derives from the combins and getRSSwidths
+functions of ISO/IEC 24724 Annex B, which the Zint backend carries with this
+notice: "Copyright (C) 2006 BSI ... This does not preclude the free use, in the
+course of implementing the standard, of necessary details such as symbols, and
+size, type or grade designations."
+
+================================================================================
+
+GS1 Barcode Syntax Dictionary (https://github.com/gs1/gs1-syntax-dictionary)
+License: Apache-2.0
+
+Portions of this software incorporate the GS1 Barcode Syntax Dictionary and
+were developed with reference to its Syntax Tests.
+
+Copyright (c) 2020-2021 BWIPP project
+Copyright (c) 2020-2021 Zint Project
+Copyright (c) 2021-2026 GS1 AISBL
+
+The GS1 Barcode Syntax Dictionary is distributed under the Apache License,
+Version 2.0. The full license text is available in the gs1-syntax-dictionary
+repository LICENSE file.
+
+================================================================================
diff --git a/shared-infrastructure b/shared-infrastructure
index 03471c6b..32a45b2d 160000
--- a/shared-infrastructure
+++ b/shared-infrastructure
@@ -1 +1 @@
-Subproject commit 03471c6b458a2c11a0b1df1f5bf2777c8c773b99
+Subproject commit 32a45b2d369ff8628606a19c8e72b7e2dc5f8d76
diff --git a/src/ImageSharp.Drawing.FontGenerator/FontDesign.cs b/src/ImageSharp.Drawing.FontGenerator/FontDesign.cs
new file mode 100644
index 00000000..57f3a412
--- /dev/null
+++ b/src/ImageSharp.Drawing.FontGenerator/FontDesign.cs
@@ -0,0 +1,164 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Drawing.FontGenerator;
+
+///
+/// One font design fed through the generator: the glyph skeletons, their stroke styling, the naming
+/// and the acceptance expectations.
+///
+internal sealed class FontDesign
+{
+ ///
+ /// Gets the short name that prefixes every output file, for example OcrA for OcrA.ttf,
+ /// OcrAFontData.cs and OcrA-proof.png.
+ ///
+ public required string Name { get; init; }
+
+ ///
+ /// Gets the font family name written to the name table.
+ ///
+ public required string FamilyName { get; init; }
+
+ ///
+ /// Gets the sentence naming this design's source standard, written into the generated data
+ /// file's XML documentation.
+ ///
+ public required string DataSummary { get; init; }
+
+ ///
+ /// Gets the design units per em.
+ ///
+ public required ushort UnitsPerEm { get; init; }
+
+ ///
+ /// Gets the nominal centerline height H in design units.
+ ///
+ public required float H { get; init; }
+
+ ///
+ /// Gets the nominal centerline width W in design units.
+ ///
+ public required float W { get; init; }
+
+ ///
+ /// Gets the descender centerline depth in design units, negative below the baseline.
+ ///
+ public required float Descender { get; init; }
+
+ ///
+ /// Gets the default stroke width T in design units.
+ ///
+ public required float DefaultStrokeWidth { get; init; }
+
+ ///
+ /// Gets the ink height of a small letter above the baseline, in design units, as the standard
+ /// dimensions the nominal printed image.
+ ///
+ public required float SmallLetterHeight { get; init; }
+
+ ///
+ /// Gets the ink height of a capital letter and of a digit above the baseline, in design units.
+ ///
+ public required float CapitalHeight { get; init; }
+
+ ///
+ /// Gets the ink height of an ascender above the baseline, in design units.
+ ///
+ public required float AscenderHeight { get; init; }
+
+ ///
+ /// Gets the ink depth of a descender below the baseline, in design units, as a positive distance.
+ ///
+ public required float DescenderDepth { get; init; }
+
+ ///
+ /// Gets the characters the standard dimensions on their own rather than on a nominal line. Those
+ /// keep the size they are drawn at, because normalizing them onto a line they were never meant to
+ /// touch would change a shape the standard states.
+ ///
+ public required string NormalizationExceptions { get; init; }
+
+ ///
+ /// Gets the factor that maps the design grid onto the em. The grid carries the 0.1 inch character
+ /// pitch, which makes the ink far taller per em than any font convention, so the same point size
+ /// would render far larger glyphs than an established digitization of the same standard. The factor
+ /// lands the capital ink at the height those digitizations use, so a point size means the same
+ /// thing across them.
+ ///
+ public required float EmScale { get; init; }
+
+ ///
+ /// Gets the ink height a small letter is drawn at, in design units, before normalization.
+ ///
+ public required float DrawnSmallLetterHeight { get; init; }
+
+ ///
+ /// Gets the ink height an ascender is drawn at, in design units, before normalization.
+ ///
+ public required float DrawnAscenderHeight { get; init; }
+
+ ///
+ /// Gets the ink depth a descender is drawn at, in design units, as a positive distance, before
+ /// normalization.
+ ///
+ public required float DrawnDescenderDepth { get; init; }
+
+ ///
+ /// Gets the ink height a capital is drawn at, in design units, before normalization. A glyph whose
+ /// drawn ink reaches this line is a full height glyph and is normalized onto
+ /// . One that does not reach it, such as a hyphen or a comma, keeps its
+ /// place against the capitals instead.
+ ///
+ public required float DrawnCapitalHeight { get; init; }
+
+ ///
+ /// Gets a value indicating whether open stroke ends project half a stroke width along their
+ /// tangent and cut flat, as the printed OCR-B terminals do, instead of ending in the round pen
+ /// sweep.
+ ///
+ public required bool CutTerminals { get; init; }
+
+ ///
+ /// Gets the glyph skeletons keyed by character, in the stroke token format that
+ /// BuildStroke in Program.cs documents.
+ ///
+ public required IReadOnlyDictionary Skeletons { get; init; }
+
+ ///
+ /// Gets, per character, the indices of strokes drawn with projecting square caps.
+ ///
+ public required IReadOnlyDictionary SquareStrokes { get; init; }
+
+ ///
+ /// Gets, per character, the indices of strokes cut off exactly at their endpoints.
+ ///
+ public required IReadOnlyDictionary ButtStrokes { get; init; }
+
+ ///
+ /// Gets, per character, the indices of strokes drawn with butt ends and sharp miter joins.
+ ///
+ public required IReadOnlyDictionary MiterStrokes { get; init; }
+
+ ///
+ /// Gets, per character, the indices of strokes drawn with round caps and sharp miter joins.
+ ///
+ public required IReadOnlyDictionary MiterRoundStrokes { get; init; }
+
+ ///
+ /// Gets per-character stroke width overrides for characters whose drawings use a thickness other
+ /// than throughout, such as the OCR-B small letters.
+ ///
+ public required IReadOnlyDictionary StrokeWidths { get; init; }
+
+ ///
+ /// Gets per-stroke width overrides keyed by character and stroke index. The value zero marks a
+ /// stroke that is already a filled ink polygon rather than a centerline to stroke.
+ ///
+ public required IReadOnlyDictionary<(char Character, int Stroke), float> StrokeWidthOverrides { get; init; }
+
+ ///
+ /// Gets the per-glyph acceptance expectations that enforces.
+ ///
+ public required IReadOnlyDictionary Expectations { get; init; }
+}
diff --git a/src/ImageSharp.Drawing.FontGenerator/GlyphClass.cs b/src/ImageSharp.Drawing.FontGenerator/GlyphClass.cs
new file mode 100644
index 00000000..3bb25959
--- /dev/null
+++ b/src/ImageSharp.Drawing.FontGenerator/GlyphClass.cs
@@ -0,0 +1,43 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Drawing.FontGenerator;
+
+///
+/// The class of character a glyph belongs to. The standards dimension the nominal printed image per
+/// class, so a glyph is normalized onto the height its class stands at.
+///
+internal enum GlyphClass
+{
+ ///
+ /// A capital letter, and the default for a character the standards draw against the capitals, such
+ /// as a bracket, a brace or an arithmetic sign.
+ ///
+ Capital,
+
+ ///
+ /// A digit.
+ ///
+ Digit,
+
+ ///
+ /// A small letter that rises to the ascender line.
+ ///
+ Ascender,
+
+ ///
+ /// A small letter that stands on the small letter line.
+ ///
+ SmallLetter,
+
+ ///
+ /// A small letter that hangs to the descender line.
+ ///
+ Descender,
+
+ ///
+ /// A character the standards dimension in millimetres rather than draw, such as the vertical line
+ /// and the character erase. Those carry their own absolute size and are not normalized.
+ ///
+ Absolute,
+}
diff --git a/src/ImageSharp.Drawing.FontGenerator/ImageSharp.Drawing.FontGenerator.csproj b/src/ImageSharp.Drawing.FontGenerator/ImageSharp.Drawing.FontGenerator.csproj
new file mode 100644
index 00000000..527d8bd3
--- /dev/null
+++ b/src/ImageSharp.Drawing.FontGenerator/ImageSharp.Drawing.FontGenerator.csproj
@@ -0,0 +1,27 @@
+
+
+
+ Exe
+ enable
+ false
+ ImageSharp.Drawing.FontGenerator
+ SixLabors.ImageSharp.Drawing.FontGenerator
+
+
+
+
+
+ net10.0;net11.0
+
+
+
+
+ net10.0
+
+
+
+
+
+
+
+
diff --git a/src/ImageSharp.Drawing.FontGenerator/OcrAGlyphs.cs b/src/ImageSharp.Drawing.FontGenerator/OcrAGlyphs.cs
new file mode 100644
index 00000000..4e0a6b4d
--- /dev/null
+++ b/src/ImageSharp.Drawing.FontGenerator/OcrAGlyphs.cs
@@ -0,0 +1,1239 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Drawing.FontGenerator;
+
+///
+/// The OCR-A glyph skeletons transcribed from the character drawings of FIPS PUB 32 (1974), Figures II-11
+/// through II-96, using the Size I dimensions of Table II-3. The drawings dimension stroke centerlines
+/// directly on the 0.0, W and H reference lines, so ink projects a half stroke beyond them at every
+/// terminal: round terminals get round caps and square terminals get projecting square caps, both ending
+/// T/2 past the centerline endpoint. Labelled corner radii are outer ink radii, so the centerline radii
+/// used here are those values less T/2.
+///
+internal static class OcrAGlyphs
+{
+ ///
+ /// Design units per em. One em equals the 10 characters-per-inch pitch of 0.1 inch, so one unit is
+ /// 0.0001 inch and Table II-3 dimensions map directly: W = 550, H = 940, T = 140.
+ ///
+ public const ushort UnitsPerEm = 1000;
+
+ ///
+ /// The nominal centerline width W of Table II-3 in design units.
+ ///
+ public const float W = 550;
+
+ ///
+ /// The nominal centerline height H of Table II-3 in design units.
+ ///
+ public const float H = 940;
+
+ ///
+ /// The nominal stroke width T of Table II-3 in design units.
+ ///
+ public const float T = 140;
+
+ ///
+ /// The centerline radius of the r1 vertex arcs of Table II-3 at the C, D and O sides; the callouts
+ /// point at the dashed centerlines, so the table values are centerline radii.
+ ///
+ public const float R1 = 248;
+
+ ///
+ /// The centerline radius of the r2 vertex arcs of Table II-3 at the O top and bottom.
+ ///
+ public const float R2 = 111;
+
+ ///
+ /// The centerline radius of the r3 corner arcs of Table II-3 at the Q bowl corners.
+ ///
+ public const float R3 = 100;
+
+ ///
+ /// The centerline radius of the r4 corner arcs of Table II-3 joining the S bars to its diagonal.
+ ///
+ public const float R4 = 87;
+
+ ///
+ /// The centerline radius of corners the drawings label 3/2 T.
+ ///
+ public const float Corner3T2 = 210;
+
+ ///
+ /// The centerline radius of corners the drawings label T.
+ ///
+ public const float CornerT = T;
+
+ ///
+ /// The centerline radius of corners the drawings label 1/2 T.
+ ///
+ public const float CornerHalfT = T / 2;
+
+ ///
+ /// The lower case x height of Figures II-68 through II-93, whose dimensions are given directly in
+ /// thousandths of an inch.
+ ///
+ public const float X = 646;
+
+ ///
+ /// The lower case descender tail centerline of Figures II-68 through II-93; the ink reaches -294.
+ ///
+ public const float D = -224;
+
+ ///
+ /// The ink height of a capital and of a digit above the baseline. Section 2.7 of FIPS PUB 32 gives
+ /// upper case dimensions in terms of W and H, so both reach the nominal centerline height of
+ /// Table II-1 plus half a stroke.
+ ///
+ public const float CapitalHeight = H + (T / 2);
+
+ ///
+ /// The ink height of a small letter above the baseline, from the lower case drawings of Figures
+ /// II-68 through II-93.
+ ///
+ public const float SmallLetterHeight = X + (T / 2);
+
+ ///
+ /// The ink height of an ascender above the baseline. The lower case drawings carry the ascenders to
+ /// the same line as the capitals.
+ ///
+ public const float AscenderHeight = CapitalHeight;
+
+ ///
+ /// The ink depth of a descender below the baseline, as a positive distance. The descending letters
+ /// carry their tail centerline to 294 below the baseline.
+ ///
+ public const float DescenderDepth = 294 + (T / 2);
+
+ ///
+ /// The centerline radius of the 13.8 corner arcs of the lower case drawings, measured on the outer
+ /// ink.
+ ///
+ public const float LowerR = 138 - (T / 2);
+
+ ///
+ /// Gets the glyph skeletons keyed by character. Each glyph is a list of strokes; a stroke is a point
+ /// list in design units with the y axis up, closed when the first and last points are equal. A
+ /// in an x position starts an arc segment: the next three values are the arc
+ /// radius (negative to flip the sweep direction; positive sweeps counter-clockwise) and the end point.
+ /// Two consecutive values mark a rounded corner: the next three values are the
+ /// centerline corner radius and the corner vertex, and the generator replaces the vertex with a
+ /// tangent arc computed from the neighbouring segments.
+ ///
+ public static IReadOnlyDictionary Skeletons { get; } = new Dictionary
+ {
+ // Figure II-20. A rounded rectangle with 1/2 T corners.
+ ['0'] =
+ [
+ [
+ W / 2, H,
+ float.NaN, float.NaN, CornerHalfT, W, H,
+ float.NaN, float.NaN, CornerHalfT, W, 0,
+ float.NaN, float.NaN, CornerHalfT, 0, 0,
+ float.NaN, float.NaN, CornerHalfT, 0, H,
+ W / 2, H,
+ ],
+ ],
+
+ // Figure II-11. A flag to 1/2 W with a square stem corner, a full-width base bar with a round
+ // join into the riser, and the riser cap at 3/8 H.
+ ['1'] =
+ [
+ [0, H, W / 2, H, W / 2, 0],
+ [0, 0, W, 0, W, 3 * H / 8],
+ ],
+
+ // Figure II-12. Three 1/2 T centerline bends, a square corner at the base, and round caps at the
+ // top-left and bottom-right.
+ ['2'] =
+ [
+ [
+ 0, H,
+ float.NaN, float.NaN, CornerHalfT, W, H,
+ float.NaN, float.NaN, CornerHalfT, W, H / 2,
+ float.NaN, float.NaN, CornerHalfT, 0, H / 2,
+ 0, 0,
+ W, 0,
+ ],
+ ],
+
+ // Figure II-13. Two runs sharing the middle arm at 1/2 H: 1/2 T bends at the outer right corners
+ // and T bends where the arm turns, whose outer arcs pinch at the right notch.
+ ['3'] =
+ [
+ [
+ 0, H,
+ float.NaN, float.NaN, CornerHalfT, W, H,
+ float.NaN, float.NaN, CornerT, W, H / 2,
+ W / 4, H / 2,
+ ],
+ [
+ W / 4, H / 2,
+ float.NaN, float.NaN, CornerT, W, H / 2,
+ float.NaN, float.NaN, CornerHalfT, W, 0,
+ 0, 0,
+ ],
+ ],
+
+ // Figure II-14. A vertical left arm at 1/8 W to the bar at 3/8 H, and a right stem at 7/8 W ink,
+ // topping out at 7/8 H; the spacing reference is corrected by delta X of +1/16 W.
+ ['4'] =
+ [
+ [W / 8, H, W / 8, 3 * H / 8, W, 3 * H / 8],
+ [7 * W / 8, 0, 7 * W / 8, 7 * H / 8],
+ ],
+
+ // Figure II-15. Bars on H and 1/2 H off a stem at 1/4 W with sharp corners, 1/2 T corners onto
+ // the right side, and the base run bending at 1/4 W through a T corner up to the 1/16 H tail cap.
+ ['5'] =
+ [
+ [
+ W, H, W / 4, H, W / 4, H / 2,
+ float.NaN, float.NaN, CornerHalfT, W, H / 2,
+ float.NaN, float.NaN, CornerHalfT, W, 0,
+ float.NaN, float.NaN, CornerT, W / 4, 0,
+ 0, H / 16,
+ ],
+ ],
+
+ // Figure II-16. A spine on the 1/8 W line with the top bar capped at 1/2 W and a closed bowl
+ // between the ink reference lines up to 3/8 H.
+ ['6'] =
+ [
+ [0, 290, 0, H, W / 8, H],
+ [W / 2, 0, W, 0, W, 3 * H / 8, 0, 3 * H / 8, 0, 0, W / 2, 0],
+ ],
+
+ // Figure II-17. A hook cap at 15/16 H arcing onto the top bar, a square corner at the top right,
+ // and the diagonal stepping through 5/8 H and 3/8 H onto the lower stem.
+ ['7'] =
+ [
+ [0, 15 * H / 16, 0, H, W / 2, H],
+ [W / 4, H, W, H, W, 5 * H / 8],
+ [W, 5 * H / 8, W / 2, 3 * H / 8, W / 2, 0],
+ ],
+
+ // Figure II-18. Two closed boxes: the narrow upper bowl between 1/4 W and 3/4 W and the full-width
+ // lower bowl.
+ ['8'] =
+ [
+ [
+ W / 2, H / 2, W / 4, H / 2, W / 4, H, 3 * W / 4, H, 3 * W / 4, H / 2, W / 2, H / 2,
+ ],
+ [
+ W / 2, H / 2,
+ float.NaN, float.NaN, CornerHalfT, W, H / 2,
+ float.NaN, float.NaN, CornerHalfT, W, 0,
+ float.NaN, float.NaN, CornerHalfT, 0, 0,
+ float.NaN, float.NaN, CornerHalfT, 0, H / 2,
+ W / 2, H / 2,
+ ],
+ ],
+
+ // Figure II-19. The digit six rotated a half turn: a spine on the 7/8 W line with the base bar on
+ // the baseline capped at 1/2 W, and a closed bowl down to 5/8 H.
+ ['9'] =
+ [
+ [W, 650, W, 0, 7 * W / 8, 0],
+ [W / 2, H, 0, H, 0, 5 * H / 8, W, 5 * H / 8, W, H, W / 2, H],
+ ],
+
+ // Figure II-25. Diagonals meeting at the apex with a crossbar at 1/4 H ending on the legs.
+ ['A'] =
+ [
+ [0, 0, W / 2, H, W, 0],
+ [W / 8, H / 4, 7 * W / 8, H / 4],
+ ],
+
+ // Figure II-26. A square-cut stem and two bowls whose right corners round at 3/2 T.
+ ['B'] =
+ [
+ [0, 0, 0, H],
+ [
+ 0, H,
+ float.NaN, float.NaN, Corner3T2, W, H,
+ float.NaN, float.NaN, Corner3T2, W, H / 2,
+ 0, H / 2,
+ ],
+ [
+ 0, H / 2,
+ float.NaN, float.NaN, Corner3T2, W, H / 2,
+ float.NaN, float.NaN, Corner3T2, W, 0,
+ 0, 0,
+ ],
+ ],
+
+ // Figure II-27. Bars turning at 3/8 W through T corners into 1:2 diagonals tangent to the r1
+ // vertex arc, whose leftmost centerline point rides the left reference line at 1/2 H.
+ ['C'] =
+ [
+ [
+ W, H,
+ float.NaN, float.NaN, CornerT, 3 * W / 8, H,
+ 26.6f, 580.7f,
+ float.NaN, R1, 26.6f, 359.3f,
+ float.NaN, float.NaN, CornerT, 3 * W / 8, 0,
+ W, 0,
+ ],
+ ],
+
+ // Figure II-28. Overhanging bars turning at 5/8 W into the r1 vertex arc on the right reference
+ // line, with an inset stem at 1/4 W.
+ ['D'] =
+ [
+ [
+ 0, H,
+ float.NaN, float.NaN, CornerT, 5 * W / 8, H,
+ 523.4f, 580.7f,
+ float.NaN, -R1, 523.4f, 359.3f,
+ float.NaN, float.NaN, CornerT, 5 * W / 8, 0,
+ 0, 0,
+ ],
+ [W / 4, 0, W / 4, H],
+ ],
+
+ // Figure II-29. One mitered run from the top bar cap around the square stem corners to the bottom
+ // bar cap, with the middle bar to 1/2 W.
+ ['E'] =
+ [
+ [W, H, 0, H, 0, 0, W, 0],
+ [0, H / 2, W / 2, H / 2],
+ ],
+
+ // Figure II-30. A mitered run from the top bar cap through the square top corner down to the stem
+ // base cap; the middle bar sits at 5/8 H and ends at 3/4 W.
+ ['F'] =
+ [
+ [W, H, 0, H, 0, 0],
+ [0, 5 * H / 8, 3 * W / 4, 5 * H / 8],
+ ],
+
+ // Figure II-31. One stroke from the top-right cap through 3/2 T bends into the straight left side
+ // between 3/8 H and 5/8 H, around the base through T corners, up the right side to 3/8 H, and
+ // inward to the spur cap at 5/8 W.
+ ['G'] =
+ [
+ [
+ W, H,
+ float.NaN, float.NaN, CornerT, W / 2, H,
+ float.NaN, float.NaN, Corner3T2, 0, 5 * H / 8,
+ float.NaN, float.NaN, CornerT, 0, 0,
+ float.NaN, float.NaN, CornerT, W, 0,
+ W, 3 * H / 8,
+ 5 * W / 8, 3 * H / 8,
+ ],
+ ],
+
+ // Figure II-32.
+ ['H'] =
+ [
+ [0, 0, 0, H],
+ [W, 0, W, H],
+ [0, H / 2, W, H / 2],
+ ],
+
+ // Figure II-33.
+ ['I'] =
+ [
+ [0, H, W, H],
+ [W / 2, H, W / 2, 0],
+ [0, 0, W, 0],
+ ],
+
+ // Figure II-34. The stem on the right turns through a semicircular bowl into a riser ending at
+ // 3/8 H on the left.
+ ['J'] =
+ [
+ [
+ 7 * W / 8, H,
+ float.NaN, float.NaN, CornerT, 7 * W / 8, 0,
+ float.NaN, float.NaN, CornerT, W / 8, 0,
+ W / 8, 3 * H / 8,
+ ],
+ ],
+
+ // Figure II-35. Both diagonals spring from the stem at 1/2 H toward the corners.
+ ['K'] =
+ [
+ [0, 0, 0, H],
+ [0, H / 2, W, H],
+ [0, H / 2, W, 0],
+ ],
+
+ // Figure II-36. Round caps on the stem top and bar end; the base corner miters square.
+ ['L'] =
+ [
+ [0, H, 0, 0, W, 0],
+ ],
+
+ // Figure II-37. Edge stems, square at the top and round at the base; each vee arm leaves the stem
+ // with a butt face whose upper corner sits on the stem's inner top corner and ends in the round
+ // bulb cap at 11/16 H.
+ ['M'] =
+ [
+ [0, 0, 0, H / 2],
+ [0, H / 4, 0, H],
+ [W, 0, W, H / 2],
+ [W, H / 4, W, H],
+ [0, H, W / 8, H, W / 2, 11 * H / 16, 7 * W / 8, H, W, H],
+ [W / 2, 11 * H / 16, W / 2, 5 * H / 8],
+ ],
+
+ // Figure II-38. One run: round cap up the left stem, a short top bar to 1/8 W, the diagonal to
+ // 7/8 W on the baseline, a short bottom bar, and the right stem to a round cap; the two bar
+ // corners miter square.
+ ['N'] =
+ [
+ [0, 0, 0, H, W / 8, H, 7 * W / 8, 0, W, 0, W, H],
+ ],
+
+ // Figure II-39. Four diagonals joined by the r2 vertex arcs at the top and bottom, tangent at
+ // 3/8 W and 5/8 W, and the r1 vertex arcs on the sides, tangent at 7/16 H and 9/16 H.
+ ['O'] =
+ [
+ [
+ 206.25f, 916.2f,
+ float.NaN, -R2, 343.75f, 916.2f,
+ W - 7.1f, 9 * H / 16,
+ float.NaN, -R1, W - 7.1f, 7 * H / 16,
+ 343.75f, 23.8f,
+ float.NaN, -R2, 206.25f, 23.8f,
+ 7.1f, 7 * H / 16,
+ float.NaN, -R1, 7.1f, 9 * H / 16,
+ 206.25f, 916.2f,
+ ],
+ ],
+
+ // Figure II-40. A stem with a square bowl closing at 7/16 H, its right corners rounded at T.
+ ['P'] =
+ [
+ [0, 0, 0, H / 2],
+ [0, H / 4, 0, H],
+ [0, H, W / 2, H],
+ [
+ W / 4, H,
+ float.NaN, float.NaN, CornerT, W, H,
+ float.NaN, float.NaN, CornerT, W, 7 * H / 16,
+ 0, 7 * H / 16,
+ ],
+ ],
+
+ // Figure II-41. A slanted bowl: straight sides on the reference lines between 3/8 H and 5/8 H,
+ // slanted top and bottom runs, r3 corners top-right and bottom-left, 3/2 T corners at the other
+ // two bends, and a tail from 3/8 H through 3/4 W on the baseline to a foot cap at the right edge.
+ ['Q'] =
+ [
+ [
+ 0, 5 * H / 8,
+ float.NaN, float.NaN, R3, W, 1053.3f,
+ W, 3 * H / 8,
+ 157.2f, 25.2f,
+ float.NaN, -97, 0, 97,
+ 0, 5 * H / 8,
+ ],
+ [W / 2, 3 * H / 8, 415, 0, W, 0],
+ ],
+
+ // Figure II-42. The P bowl closing at 5/8 H with a leg from 3/8 W on the bowl to the corner.
+ ['R'] =
+ [
+ [0, 0, 0, H / 2],
+ [0, H / 4, 0, H],
+ [0, H, W / 2, H],
+ [
+ W / 4, H,
+ float.NaN, float.NaN, CornerT, W, H,
+ float.NaN, float.NaN, CornerT, W, 5 * H / 8,
+ 0, 5 * H / 8,
+ ],
+ [3 * W / 8, 5 * H / 8, W, 0],
+ ],
+
+ // Figure II-43. Point-symmetric: terminal curls to 7/8 H and 1/8 H, runs along the top and bottom
+ // reference lines, short sides on the vertical reference lines, and r4 corners onto the middle
+ // diagonal.
+ ['S'] =
+ [
+ [
+ W, 7 * H / 8,
+ float.NaN, H / 8, W - (H / 8), H,
+ float.NaN, float.NaN, R4, -91.7f, H,
+ float.NaN, float.NaN, R4, 641.7f, 0,
+ H / 8, 0,
+ float.NaN, -(H / 8), 0, H / 8,
+ ],
+ ],
+
+ // Figure II-44. The bar carries short drops to caps at 7/8 H at both ends.
+ ['T'] =
+ [
+ [0, 7 * H / 8, 0, H],
+ [0, H, W, H],
+ [W, H, W, 7 * H / 8],
+ [W / 2, H, W / 2, 0],
+ ],
+
+ // Figure II-45. A flat bottom run with T corners.
+ ['U'] =
+ [
+ [
+ 0, H,
+ float.NaN, float.NaN, CornerT, 0, 0,
+ float.NaN, float.NaN, CornerT, W, 0,
+ W, H,
+ ],
+ ],
+
+ // Figure II-46. Vertical drops to 3/4 H before the diagonals meet at the rounded base vertex.
+ ['V'] =
+ [
+ [0, H, 0, 3 * H / 4, W / 2, 0, W, 3 * H / 4, W, H],
+ ],
+
+ // Figure II-47. Three verticals: outer stems to H and a middle stroke capped at 5/8 H, joined by
+ // U valleys whose 1/2 T corners put their tangents on the 1/8 W and 3/8 W ticks.
+ ['W'] =
+ [
+ [0, H, 0, H / 8, W / 8, 0, (W / 2) - (T / 2), 0],
+ [W, H, W, H / 8, 7 * W / 8, 0, (W / 2) + (T / 2), 0],
+ [W / 2, 0, W / 2, H / 2],
+ [W / 2, 3 * H / 8, W / 2, 5 * H / 8],
+ ],
+
+ // Figure II-48. Diagonals corner to corner.
+ ['X'] =
+ [
+ [0, 0, W, H],
+ [0, H, W, 0],
+ ],
+
+ // Figure II-49. Arm stubs dropping to 7/8 H, the junction at 1/2 H, and the stem to the baseline.
+ ['Y'] =
+ [
+ [0, H, 0, 7 * H / 8, W / 2, H / 2, W / 2, 0],
+ [W, H, W, 7 * H / 8, W / 2, H / 2],
+ ],
+
+ // Figure II-50. The top bar has a round cap on the left and a square end projecting 1/2 T past
+ // the diagonal vertex on the right; the bottom bar mirrors it. The diagonal runs vertex to vertex
+ // with its caps buried under the square ends.
+ ['Z'] =
+ [
+ [
+ 0, 1010,
+ 620, 1010,
+ 620, 921,
+ 122, 70,
+ W, 70,
+ float.NaN, -70, 620, 0,
+ float.NaN, -70, W, -70,
+ -70, -70,
+ -70, 19,
+ 427.9f, 870,
+ 0, 870,
+ float.NaN, -70, -70, H,
+ float.NaN, -70, 0, 1010,
+ ],
+ ],
+
+ // Figure II-51. A square centerline between 3/16 H and 5/16 H; the drawing's Y value keeps the
+ // ink clear of the baseline.
+ ['.'] = [.. SquareMark(3 * W / 8, 3 * H / 16, 5 * W / 8, 5 * H / 16)],
+
+ // Figure II-52. A bar at 3/8 H from 1/4 W turning down at 3/4 W to a foot on the baseline.
+ [','] =
+ [
+ [W / 4, 3 * H / 8, 3 * W / 4, 3 * H / 8, 3 * W / 4, 0],
+ ],
+
+ // Figure II-53. Squares between 3/16 H and 5/16 H and between 11/16 H and 13/16 H.
+ [':'] =
+ [
+ .. SquareMark(3 * W / 8, 3 * H / 16, 5 * W / 8, 5 * H / 16),
+ .. SquareMark(3 * W / 8, 11 * H / 16, 5 * W / 8, 13 * H / 16),
+ ],
+
+ // Figure II-54. The comma elbow with a square between 3/4 H and 7/8 H.
+ [';'] =
+ [
+ [W / 4, 3 * H / 8, 3 * W / 4, 3 * H / 8, 3 * W / 4, 0],
+ .. SquareMark(W / 4, 3 * H / 4, W / 2, 7 * H / 8),
+ ],
+
+ // Figure II-55. Bars at Y = 3/8 H and 11/16 H.
+ ['='] =
+ [
+ [0, 3 * H / 8, W, 3 * H / 8],
+ [0, 11 * H / 16, W, 11 * H / 16],
+ ],
+
+ // Figure II-56. A cross centered at 1/2 H whose vertical bar spans W in total.
+ ['+'] =
+ [
+ [0, H / 2, W, H / 2],
+ [W / 2, (H / 2) - (W / 2), W / 2, (H / 2) + (W / 2)],
+ ],
+
+ // Figure II-66. A closed centerline loop between Y = 7/16 H and 1/2 H across the full width with
+ // 1/2 T corners.
+ ['-'] =
+ [
+ [0, 7 * H / 16, 0, H / 2, W, H / 2, W, 7 * H / 16, 0, 7 * H / 16],
+ [80, 15 * H / 32, W - 80, 15 * H / 32],
+ ],
+
+ // Figure II-57. A diagonal corner to corner.
+ ['/'] =
+ [
+ [0, 0, W, H],
+ ],
+
+ // Figure II-58. A vertical arm between 1/8 H and 7/8 H crossed by two diagonals ending at 1/4 H
+ // and 3/4 H on the reference lines.
+ ['*'] =
+ [
+ [W / 2, H / 8, W / 2, 7 * H / 8],
+ [0, H / 4, W, 3 * H / 4],
+ [0, 3 * H / 4, W, H / 4],
+ ],
+
+ // Figure II-59. Single ink outline: flat top spanning the full width plus the 1/2 T side
+ // overhangs, tapered sides parallel to the vee centerlines, round prong tips at 5/8 H, and the
+ // centre notch floor T below the top ink.
+ ['"'] =
+ [
+ [
+ -70, 1010, 620, 1010, 620, 933.2f, 549.96f, 574.1f,
+ float.NaN, -70, 412.54f, 574.1f,
+ 354.83f, 870, 195.17f, 870, 137.46f, 574.1f,
+ float.NaN, -70, 0.04f, 574.1f,
+ -70, 933.2f, -70, 1010,
+ ],
+ ],
+
+ // Figure II-65. A vertical left arm to a round vertex at 1/2 H with the right arm rising back to
+ // the top line, capped flat by a short bar.
+ ['\''] =
+ [
+ [3 * W / 8, H, 5 * W / 8, H],
+ [3 * W / 8, 900, 3 * W / 8, H / 2],
+ [3 * W / 8, H / 2, 332.05f, 900],
+ ],
+
+ // Figure II-62. Centerline squares top-left and bottom-right with the slash from 1/8 H to 7/8 H.
+ ['%'] =
+ [
+ [0, H / 8, W, 7 * H / 8],
+ .. SquareMark(0, 7 * H / 8, W / 4, H),
+ .. SquareMark(3 * W / 4, 0, W, H / 8),
+ ],
+
+ // Figure II-67. A squared S body with bars at 1/4 H, 1/2 H and 3/4 H crossed by a centre stem
+ // spanning the full character height.
+ ['$'] =
+ [
+ [W, 3 * H / 4, 0, 3 * H / 4, 0, H / 2, W, H / 2, W, H / 4, 0, H / 4],
+ [W / 2, 3 * H / 4, W / 2, H],
+ [W / 2, H / 4, W / 2, 0],
+ ],
+
+ // Figure II-60. A spine at 1/2 W with bars reaching the right edge and a spur to the left edge at
+ // 1/2 H.
+ ['('] =
+ [
+ [
+ W, H,
+ float.NaN, float.NaN, CornerHalfT, W / 2, H,
+ W / 2, 610,
+ float.NaN, -CornerT, 135, H / 2,
+ 0, H / 2,
+ ],
+ [
+ W, 0,
+ float.NaN, float.NaN, CornerHalfT, W / 2, 0,
+ W / 2, 330,
+ float.NaN, CornerT, 135, H / 2,
+ 0, H / 2,
+ ],
+ ],
+
+ // Figure II-61. The mirrored closing form.
+ [')'] =
+ [
+ [
+ 0, H,
+ float.NaN, float.NaN, CornerHalfT, W / 2, H,
+ W / 2, 610,
+ float.NaN, CornerT, 415, H / 2,
+ W, H / 2,
+ ],
+ [
+ 0, 0,
+ float.NaN, float.NaN, CornerHalfT, W / 2, 0,
+ W / 2, 330,
+ float.NaN, -CornerT, 415, H / 2,
+ W, H / 2,
+ ],
+ ],
+
+ // Figure II-63. A slanted hairpin from a cap at 5/8 H around the top-right, descending to a drop
+ // cap at 1/4 H, with the dot at 1/4 W on the baseline.
+ ['?'] =
+ [
+ [0, 5 * H / 8, 3 * W / 4, H, W, 7 * H / 8, W, 3 * H / 4, W / 4, 3 * H / 8, W / 4, H / 4],
+ [W / 4, 0, (W / 4) + 0.1f, 0],
+ ],
+
+ // Figure II-64. One run: from the cap at (W, 1/4 H) down to the baseline vertex at 1/2 W, around
+ // the T fillet at the origin, up and diagonally through the self crossing to 3/4 W at 5/8 H, over
+ // the top bar at 7/8 H between two T fillets, and down the left side into the closing diagonal to
+ // the cap at (W, 0).
+ ['&'] =
+ [
+ [
+ W, H / 4,
+ W / 2, 0,
+ float.NaN, float.NaN, CornerT, 0, 0,
+ 0, H / 4,
+ 3 * W / 4, 5 * H / 8,
+ float.NaN, float.NaN, CornerT, 3 * W / 4, 7 * H / 8,
+ float.NaN, float.NaN, CornerT, 0, 7 * H / 8,
+ 0, 5 * H / 8,
+ W, 0,
+ ],
+ ],
+
+ // Figure II-24. The Long Vertical Mark: length L = 1460 with square-cut ends, tied to no
+ // baseline, rising from the descender depth through the line box. The break is real ink, centred
+ // on the bar, splitting it into equal segments.
+ ['|'] =
+ [
+ [W / 2, D - (T / 2), W / 2, 410.75f],
+ [W / 2, 461.25f, W / 2, D - (T / 2) + 1460],
+ ],
+
+ // Figure II-21. Symbol Hook: one run with square corners from the 3/8 H cap on the left, along
+ // the baseline, up the 1/2 W stem, across the top, and down to the 5/8 H cap on the right.
+ ['⑀'] =
+ [
+ [0, 3 * H / 8, 0, 0, W / 2, 0, W / 2, H, W, H, W, 5 * H / 8],
+ ],
+
+ // Figure II-23. Symbol Chair: a full-height right stroke, a seat at 1/2 H turning square down the
+ // left front leg to the baseline.
+ ['⑁'] =
+ [
+ [W, 0, W, H],
+ [W, H / 2, 0, H / 2, 0, 0],
+ ],
+
+ // Figure II-22. Symbol Fork: prongs to 1/2 H joined by a square-ended bar, with the stem
+ // continuing to the baseline.
+ ['⑂'] =
+ [
+ [0, H, 0, H / 2],
+ [W, H, W, H / 2],
+ [0, H / 2, W, H / 2],
+ [W / 2, H / 2, W / 2, 0],
+ ],
+
+ // Figure II-95. Character Erase: solid ink covering the character cell, inside the figure's MIN
+ // and MAX rectangles.
+ ['█'] =
+ [
+ [-70, -70, 620, -70, 620, 1010, -70, 1010, -70, -70],
+ ],
+
+ // Figure II-96. Group Erase: a thin continuous line spanning the cell so adjacent marks join.
+ ['―'] =
+ [
+ [-225, H / 2, W + 225, H / 2],
+ ],
+
+ // Figure II-68. An arm capped at 13.8 turning through a 13.8 corner into the 2 degree right side
+ // ending in the 7.0 foot, with a low octagonal bowl to 35.3 whose bottom flat ends at 34.4 in a
+ // cut up to the side.
+ ['a'] =
+ [
+ [138, X, float.NaN, float.NaN, 138, 527.4f, X, W, 0],
+ [
+ 537.7f, 353,
+ float.NaN, float.NaN, 138, 0, 353,
+ float.NaN, float.NaN, 138, 0, 0,
+ float.NaN, float.NaN, 138, 344, 0,
+ 546, 118,
+ ],
+ ],
+
+ // Figure II-69. An ascender stem with the bowl attached on its right.
+ ['b'] =
+ [
+ [0, 0, 0, H],
+ [
+ 0, 380,
+ float.NaN, float.NaN, 138, 0, 470,
+ float.NaN, float.NaN, 138, 206, X,
+ float.NaN, float.NaN, 138, 413, X,
+ float.NaN, float.NaN, 138, W, 529,
+ float.NaN, float.NaN, 138, W, 118,
+ float.NaN, float.NaN, 138, 413, 0,
+ float.NaN, float.NaN, 138, 206, 0,
+ float.NaN, float.NaN, 138, 0, 176,
+ 0, 266,
+ ],
+ ],
+
+ // Figure II-70. The open bowl with caps at 3/4 of the width.
+ ['c'] =
+ [
+ [
+ W, X,
+ float.NaN, float.NaN, 138, 206, X,
+ float.NaN, float.NaN, 138, 0, 470,
+ float.NaN, float.NaN, 138, 0, 176,
+ float.NaN, float.NaN, 138, 206, 0,
+ W, 0,
+ ],
+ ],
+
+ // Figure II-71. The mirrored b.
+ ['d'] =
+ [
+ [W, 0, W, H],
+ [
+ W, 380,
+ float.NaN, float.NaN, 138, W, 470,
+ float.NaN, float.NaN, 138, 344, X,
+ float.NaN, float.NaN, 138, 138, X,
+ float.NaN, float.NaN, 138, 0, 529,
+ float.NaN, float.NaN, 138, 0, 118,
+ float.NaN, float.NaN, 138, 138, 0,
+ float.NaN, float.NaN, 138, 344, 0,
+ float.NaN, float.NaN, 138, W, 176,
+ W, 266,
+ ],
+ ],
+
+ // Figure II-72. A full-width bar at 29.4 with the shell opening bottom-right.
+ ['e'] =
+ [
+ [0, 294, W, 294],
+ [
+ W, 294,
+ float.NaN, float.NaN, 138, W, 529,
+ float.NaN, float.NaN, 138, 413, X,
+ float.NaN, float.NaN, 138, 138, X,
+ float.NaN, float.NaN, 138, 0, 529,
+ float.NaN, float.NaN, 138, 0, 118,
+ float.NaN, float.NaN, 138, 138, 0,
+ 300, 0,
+ ],
+ [200, 0, W, 0],
+ ],
+
+ // Figure II-73. A stem leaning 2 degrees from 30.9 at the base, hooking right through the 17.6
+ // corner to a cap at 48.1, crossed at the x line; the spacing reference is corrected by delta X
+ // of +3.4.
+ ['f'] =
+ [
+ [206, 0, float.NaN, float.NaN, 176, 206, H, W, H],
+ [69, X, 413, X],
+ ],
+
+ // Figure II-74. The bowl with a right stem descending to a hooked tail ending at 13.8.
+ ['g'] =
+ [
+ [
+ W, X,
+ float.NaN, float.NaN, 138, W, -176,
+ float.NaN, float.NaN, 138, 413, -294,
+ 69, -294,
+ ],
+ [
+ W, 380,
+ float.NaN, float.NaN, 138, W, 470,
+ float.NaN, float.NaN, 138, 344, X,
+ float.NaN, float.NaN, 138, 138, X,
+ float.NaN, float.NaN, 138, 0, 529,
+ float.NaN, float.NaN, 138, 0, 176,
+ float.NaN, float.NaN, 138, 138, 59,
+ float.NaN, float.NaN, 138, 344, 59,
+ float.NaN, float.NaN, 138, W, 176,
+ W, 266,
+ ],
+ ],
+
+ // Figure II-75. An ascender stem with the arch springing at 47.0; the arch geometry repeats
+ // Figure II-81, so the shoulder is the same tangent arc into the 2 degree leg.
+ ['h'] =
+ [
+ [0, 0, 0, H],
+ [
+ 0, 470,
+ float.NaN, float.NaN, 138, W / 2, X,
+ 394.1f, X,
+ float.NaN, -138, 532.1f, 512.8f,
+ W, 0,
+ ],
+ ],
+
+ // Figure II-76. A flag capped at 6.9 into the stem at 27.5, a base bar from 6.9 to 48.1, and the
+ // dot: a closed centerline square between 20.6 and 27.5 and between 94.0 and 99.9.
+ ['i'] =
+ [
+ [69, X, W / 2, X, W / 2, 0],
+ [69, 0, 481, 0],
+ .. SquareMark(206, 940, W / 2, 999),
+ ],
+
+ // Figure II-77. A flag capped at 13.8 into the stem at 41.3, a hook with 13.8 corners whose run
+ // sits at -22.4 rising to a terminal on the left reference line, and the dot square between 34.4
+ // and 41.3; the spacing reference is corrected by delta X of -6.9.
+ ['j'] =
+ [
+ [138, X, 413, X],
+ [
+ 413, X,
+ float.NaN, float.NaN, 138, 413, -294,
+ 138, -294,
+ float.NaN, -138, 0, -176,
+ ],
+ .. SquareMark(344, 940, 413, 999),
+ ],
+
+ // Figure II-78. Diagonals from the stem at 35.3 toward the x line and the baseline corner.
+ ['k'] =
+ [
+ [0, 0, 0, H],
+ [0, 235, 481, X],
+ [138, 353, W, 0],
+ ],
+
+ // Figure II-79. The ascender stem with a base bar.
+ ['l'] =
+ [
+ [69, H, W / 2, H, W / 2, 0],
+ [69, 0, 481, 0],
+ ],
+
+ // Figure II-80. Stems lean two degrees from baseline centres 1.5 and 56.5; the r100 and r97
+ // arches crest at 13.8 and 41.3 on the x line and run down straight tangents into the next leg
+ // at 52.9.
+ ['m'] =
+ [
+ [-15, 0, -15, X],
+ [-15, 529, 112.4f, 631.1f, float.NaN, -100, W / 2, 553.1f],
+ [W / 2, 553.1f, W / 2, 0],
+ [W / 2, 529, 383.4f, 627.4f, float.NaN, -97, 545.5f, 559.1f, 565, 0],
+ ],
+
+ // Figure II-81. The left stem with the arch springing at 47.0.
+ ['n'] =
+ [
+ [0, 0, 0, X],
+ [
+ 0, 470,
+ float.NaN, float.NaN, 138, W / 2, X,
+ 394.1f, X,
+ float.NaN, -138, 532.1f, 512.8f,
+ W, 0,
+ ],
+ ],
+
+ // Figure II-82. An octagonal bowl: the centerline carries the drawing's corner cuts between 13.8
+ // and 41.3 horizontally and 11.8 and 52.9 vertically, leaving the counter angled while the joins
+ // round the outer corners.
+ ['o'] =
+ [
+ [
+ W / 2, X,
+ float.NaN, float.NaN, 138, 413, X,
+ float.NaN, float.NaN, 138, W, 529,
+ float.NaN, float.NaN, 138, W, 118,
+ float.NaN, float.NaN, 138, 413, 0,
+ float.NaN, float.NaN, 138, 138, 0,
+ float.NaN, float.NaN, 138, 0, 118,
+ float.NaN, float.NaN, 138, 0, 529,
+ float.NaN, float.NaN, 138, 138, X,
+ W / 2, X,
+ ],
+ ],
+
+ // Figure II-83. The bowl with a left stem rising to 67.55 and descending to the tail depth.
+ ['p'] =
+ [
+ [0, -294, 0, 675.5f],
+ [
+ 0, 470,
+ float.NaN, float.NaN, 138, 206, 675.5f,
+ float.NaN, float.NaN, 138, 344, 675.5f,
+ float.NaN, float.NaN, 138, W, 498,
+ float.NaN, float.NaN, 138, W, 148,
+ float.NaN, float.NaN, 138, 344, -29.5f,
+ float.NaN, float.NaN, 138, 206, -29.5f,
+ 0, 176,
+ ],
+ ],
+
+ // Figure II-84. The mirrored p.
+ ['q'] =
+ [
+ [W, -294, W, X],
+ [
+ W, 470,
+ float.NaN, float.NaN, 138, 344, X,
+ float.NaN, float.NaN, 138, 138, X,
+ float.NaN, float.NaN, 138, 0, 529,
+ float.NaN, float.NaN, 138, 0, 118,
+ float.NaN, float.NaN, 138, 138, 0,
+ float.NaN, float.NaN, 138, 344, 0,
+ W, 176,
+ ],
+ ],
+
+ // Figure II-85. The stem with an arm curling down to a cap at 41.1.
+ ['r'] =
+ [
+ [0, 0, 0, X],
+ [
+ 0, 411,
+ float.NaN, float.NaN, 138, W / 2, X,
+ 412, X,
+ float.NaN, -138, W, 508,
+ W, 470,
+ ],
+ ],
+
+ // Figure II-86. The scaled S form: terminal curls of 13.8 radius, sides on the vertical reference
+ // lines, and the middle diagonal.
+ ['s'] =
+ [
+ [
+ 535, 588,
+ float.NaN, 138, 422.6f, X,
+ 147.7f, X,
+ float.NaN, 138, 15, 470,
+ 465.7f, 271,
+ float.NaN, -141.6f, 408.4f, 0,
+ 127.4f, 0,
+ float.NaN, -138, 15, 59,
+ ],
+ ],
+
+ // Figure II-87. A stem rising to 88.1, crossed at the x line, with the base turning through a
+ // U-arc dipping to -13.8 ink and rising to a tip at 11.8.
+ ['t'] =
+ [
+ [0, X, 481, X],
+ [
+ 138, 881, 138, 138,
+ float.NaN, 138, 276, 0,
+ 413.5f, 0,
+ float.NaN, 138, W, 118,
+ ],
+ ],
+
+ // Figure II-88. The n form flipped, with the right stem running to its own base cap.
+ ['u'] =
+ [
+ [W, 0, W, X],
+ [
+ W, 176,
+ float.NaN, float.NaN, 138, W / 2, 0,
+ 155.9f, 0,
+ float.NaN, -138, 17.9f, 133.2f,
+ 0, X,
+ ],
+ ],
+
+ // Figure II-89. Legs to a short base flat between 23.2 and 31.8.
+ ['v'] =
+ [
+ [0, X, 0, 529, 232, 0, 318, 0, W, 529, W, X],
+ ],
+
+ // Figure II-90. Outer stems leaning 2 degrees bending at 23.5 into flat valley bottoms between
+ // 6.9 and 13.8 and between 41.3 and 48.1, with the centre peak at 41.1.
+ ['w'] =
+ [
+ [-14, X, 0, 235, 69, 0, 138, 0, W / 2, 235, 413, 0, 481, 0, W, 235, W + 14, X],
+ [W / 2, 235, W / 2, 411],
+ ],
+
+ // Figure II-91. Diagonals between 1.5 and 53.5.
+ ['x'] =
+ [
+ [15, 0, 535, X],
+ [15, X, 535, 0],
+ ],
+
+ // Figure II-92. The left arm meets the descending right diagonal, which hooks left at the tail.
+ ['y'] =
+ [
+ [0, X, 0, 529, 232, 0],
+ [W, X, W, 529, 189, -294, 0, -294],
+ ],
+
+ // Figure II-93. The Z form at x height with round caps on all bar ends.
+ ['z'] =
+ [
+ [34, X, W, X, W, 588, 0, 59, 0, 0, W, 0],
+ ],
+ };
+
+ ///
+ /// Gets, per character, the indices of strokes whose terminals are square: they receive projecting
+ /// square caps that extend T/2 past the centerline endpoint, mirroring how round caps project. Mixed
+ /// terminals on one path are drawn as two overlapping strokes, one per cap style.
+ ///
+ public static IReadOnlyDictionary SquareStrokes { get; } = new Dictionary
+ {
+ ['B'] = [0],
+ ['M'] = [1, 3],
+ ['T'] = [1],
+ ['P'] = [1, 2],
+ ['R'] = [1, 2],
+ ['⑂'] = [2],
+ };
+
+ ///
+ /// Gets, per character, the indices of strokes cut off exactly at their endpoints with no cap
+ /// projection: the attachment strokes whose butt faces sit on another stroke's ink corners.
+ ///
+ public static IReadOnlyDictionary ButtStrokes { get; } = new Dictionary
+ {
+ ['W'] = [2],
+ ['|'] = [0, 1],
+ ['―'] = [0],
+ ['%'] = [1, 2, 7, 8],
+ ['.'] = [0, 1],
+ [':'] = [0, 1, 6, 7],
+ [';'] = [1, 2],
+ ['a'] = [1],
+ ['b'] = [1],
+ ['d'] = [1],
+ ['e'] = [1],
+ ['g'] = [1],
+ ['i'] = [2, 3],
+ ['j'] = [2, 3],
+ };
+
+ ///
+ /// Gets, per character, the indices of strokes drawn with butt ends and sharp miter joins: the
+ /// vees whose drawings dimension the outer vertex at the exact miter point.
+ ///
+ public static IReadOnlyDictionary MiterStrokes { get; } = new Dictionary
+ {
+ ['M'] = [4],
+ };
+
+ ///
+ /// Gets, per character, the indices of strokes drawn with round terminal caps and sharp miter
+ /// joins: paths whose drawings show square outer corners between straight runs.
+ ///
+ public static IReadOnlyDictionary MiterRoundStrokes { get; } = new Dictionary
+ {
+ ['1'] = [0],
+ ['2'] = [0],
+ ['4'] = [0],
+ ['5'] = [0],
+ ['7'] = [1],
+ ['E'] = [0],
+ ['F'] = [0],
+ ['G'] = [0],
+ ['L'] = [0],
+ ['N'] = [0],
+ ['⑀'] = [0],
+ ['⑁'] = [1],
+ };
+
+ ///
+ /// Gets per-stroke width overrides keyed by character and stroke index, used for the dots and marks
+ /// whose ink is shallower than a full stroke. The value zero marks a stroke that is already a filled
+ /// ink polygon rather than a centerline to stroke.
+ ///
+ public static IReadOnlyDictionary<(char Character, int Stroke), float> StrokeWidthOverrides { get; } =
+ new Dictionary<(char Character, int Stroke), float>
+ {
+ [('.', 0)] = 257.5f,
+ [('.', 1)] = 277.5f,
+ [(':', 0)] = 257.5f,
+ [(':', 1)] = 277.5f,
+ [(':', 6)] = 257.5f,
+ [(':', 7)] = 277.5f,
+ [(';', 1)] = 257.5f,
+ [(';', 2)] = 277.5f,
+ [('%', 1)] = 257.5f,
+ [('%', 2)] = 277.5f,
+ [('%', 7)] = 257.5f,
+ [('%', 8)] = 277.5f,
+ [('█', 0)] = 0,
+ [('Z', 0)] = 0,
+ [('"', 0)] = 0,
+ [('i', 2)] = 199,
+ [('i', 3)] = 209,
+ [('j', 2)] = 199,
+ [('j', 3)] = 209,
+ };
+
+ ///
+ /// Gets the design definition the generator consumes.
+ ///
+ public static FontDesign Design { get; } = new()
+ {
+ Name = "OcrA",
+ FamilyName = "SixLabors OCRA",
+ DataSummary = "Built clean-room from the dimensioned character drawings of FIPS PUB 32 (1974).",
+ UnitsPerEm = UnitsPerEm,
+ H = H,
+ W = W,
+ Descender = D,
+ DefaultStrokeWidth = T,
+ SmallLetterHeight = SmallLetterHeight,
+ CapitalHeight = CapitalHeight,
+ AscenderHeight = AscenderHeight,
+ DescenderDepth = DescenderDepth,
+
+ // Section 2.1 of FIPS PUB 32 states that i, j, m, p and w were designed to exceed the nominal
+ // values of Table II-1, and Figure II-88 dimensions t at its own height rather than on a line.
+ NormalizationExceptions = "ijmpwt",
+ DrawnCapitalHeight = CapitalHeight,
+
+ // The reference OCR-A digitization prints its capital ink 780 units above the baseline per em, so
+ // the scale lands ours there and a point size means the same glyph size in both.
+ EmScale = 780F / CapitalHeight,
+ DrawnSmallLetterHeight = SmallLetterHeight,
+ DrawnAscenderHeight = AscenderHeight,
+ DrawnDescenderDepth = DescenderDepth,
+ CutTerminals = false,
+ Skeletons = Skeletons,
+ SquareStrokes = SquareStrokes,
+ ButtStrokes = ButtStrokes,
+ MiterStrokes = MiterStrokes,
+ MiterRoundStrokes = MiterRoundStrokes,
+ StrokeWidths = new Dictionary(),
+ StrokeWidthOverrides = StrokeWidthOverrides,
+ Expectations = SpecChecks.OcrA,
+ };
+
+ ///
+ /// Builds the six strokes of a rounded square mark of full stroke ink: two butt-cut bars crossing to
+ /// form the body and four round dots supplying the corner radii. Loop-free so the outline union never
+ /// sees a degenerate inner offset.
+ ///
+ /// The centerline left edge.
+ /// The centerline bottom edge.
+ /// The centerline right edge.
+ /// The centerline top edge.
+ /// The strokes.
+ private static float[][] SquareMark(float minX, float minY, float maxX, float maxY) =>
+ [
+ [minX, (minY + maxY) / 2, maxX, (minY + maxY) / 2],
+ [(minX + maxX) / 2, minY, (minX + maxX) / 2, maxY],
+ [minX, minY, minX + 0.1f, minY],
+ [maxX, minY, maxX + 0.1f, minY],
+ [minX, maxY, minX + 0.1f, maxY],
+ [maxX, maxY, maxX + 0.1f, maxY],
+ ];
+}
diff --git a/src/ImageSharp.Drawing.FontGenerator/OcrBGlyphs.cs b/src/ImageSharp.Drawing.FontGenerator/OcrBGlyphs.cs
new file mode 100644
index 00000000..382200f1
--- /dev/null
+++ b/src/ImageSharp.Drawing.FontGenerator/OcrBGlyphs.cs
@@ -0,0 +1,1942 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Drawing.FontGenerator;
+
+///
+/// The OCR-B glyph outlines, set against ECMA-11, the origin standard for OCR-B: each outline is
+/// placed on the printed 4:1 character sheet of the standard and squared to the dimensions its
+/// tables give. Coordinates are on the 0.0001 inch design grid shared with OCR-A: x = 275 at the
+/// cell centre, baseline at zero. The vertical line, the pre-printed long vertical mark, the
+/// continuous underline and the character erase are exact primitives, because the standard
+/// dimensions those four in millimetres instead of drawing them. Entries whose stroke width is zero
+/// are filled ink outlines; primitives are centerline strokes.
+///
+internal static class OcrBGlyphs
+{
+ ///
+ /// Design units per em before the em scale, equal to the 0.1 inch pitch.
+ ///
+ public const ushort UnitsPerEm = 1000;
+
+ ///
+ /// The nominal centerline height of digit eight, the tallest character: 0.094 inch.
+ ///
+ public const float H = 940;
+
+ ///
+ /// The nominal centerline width of digit zero, the widest character: 0.055 inch.
+ ///
+ public const float W = 550;
+
+ ///
+ /// The nominal stroke width for digits, capitals and most symbols: 0.014 inch.
+ ///
+ public const float T = 140;
+
+ ///
+ /// The nominal stroke width for all small letters plus #, % and @: 0.012 inch.
+ ///
+ public const float TSmall = 120;
+
+ ///
+ /// The descender centerline depth. ECMA-11 section 4.1 gives 0,60 mm below the baseline as
+ /// the typical descender depth.
+ ///
+ public const float D = -236;
+
+ ///
+ /// One grid square of the reference drawings in design units: 0.02 mm.
+ ///
+ public const float Square = 7.874f;
+
+ ///
+ /// Design units per millimetre. The design grid is the 0,0001 inch grid.
+ ///
+ public const float PerMillimetre = 10000F / 25.4F;
+
+ ///
+ /// Dimension C of ECMA-11 section 4.1: a small letter stands 1,83 mm above the baseline.
+ ///
+ public const float SmallLetterHeight = 1.83F * PerMillimetre;
+
+ ///
+ /// Dimension B of ECMA-11 section 4.1: a capital letter and a digit both stand 2,46 mm above the
+ /// baseline. The standard gives the two one dimension.
+ ///
+ public const float CapitalHeight = 2.46F * PerMillimetre;
+
+ ///
+ /// Dimension A of ECMA-11 section 4.1: an ascender stands 2,60 mm above the baseline.
+ ///
+ public const float AscenderHeight = 2.60F * PerMillimetre;
+
+ ///
+ /// Dimension D of ECMA-11 section 4.1: a descender hangs 0,60 mm below the baseline.
+ ///
+ public const float DescenderDepth = 0.60F * PerMillimetre;
+
+ ///
+ /// Gets the glyph outlines and the dimensional primitives keyed by character, in the
+ /// stroke token format that BuildStroke in Program.cs documents.
+ ///
+ public static IReadOnlyDictionary Skeletons { get; } = new Dictionary
+ {
+ ['!'] =
+ [
+ [
+ 188.8f, 1237.5f, 213.2f, 462.4f, 360.6f, 462.4f, 389.2f, 1237.5f, 188.8f, 1237.5f,
+ ],
+ [
+ 174.8f, 197.5f, 174.8f, 7, 375.2f, 7, 375.2f, 197.5f, 174.8f, 197.5f,
+ ],
+ ],
+
+ ['"'] =
+ [
+ [
+ 100.3f, 745.6f, 156.2f, 1212.3f, -38.6f, 1212.3f, -38.6f, 745.6f, 100.3f, 745.6f,
+ ],
+ [
+ 534.4f, 745.6f, 588.6f, 1212.3f, 393.8f, 1212.3f, 393.8f, 745.6f, 534.4f, 745.6f,
+ ],
+ ],
+
+ ['#'] =
+ [
+ [
+ 218.7f, 831.5f, 288.3f, 1168.5f, 171.3f, 1168.5f, 103.1f, 831.5f, -12.5f, 831.5f,
+ -42.1f, 719.1f, 82.4f, 719.1f, 23.1f, 436.6f, -42.1f, 436.6f, -70.2f, 324.2f,
+ -0.6f, 324.2f, -70.2f, 0, 51.3f, 0, 120.9f, 324.2f, 329.8f, 324.2f,
+ 264.6f, 0, 387.6f, 0, 451.3f, 324.2f, 562.5f, 324.2f, 590.6f, 436.6f,
+ 470.6f, 436.6f, 529.9f, 719.1f, 590.6f, 719.1f, 620.2f, 831.5f, 552.1f, 831.5f,
+ 620.2f, 1168.5f, 498.7f, 1168.5f, 430.6f, 831.5f, 218.7f, 831.5f,
+ ],
+ [
+ 140.2f, 437.5f, 197.9f, 711.5f, 408.4f, 711.5f, 349.1f, 437.5f, 140.2f, 437.5f,
+ ],
+ ],
+
+ ['$'] =
+ [
+ [
+ 213, 691, float.NaN, float.NaN, float.NaN, float.NaN, 143.2f, 717.1f, 56.3f, 730.1f,
+ 56.3f, 829.4f, float.NaN, float.NaN, float.NaN, float.NaN, 56.3f, 923.9f, 141.6f, 938.5f,
+ 213, 938.5f, 213, 691,
+ ],
+ [
+ 338.6f, 490.7f, float.NaN, float.NaN, float.NaN, float.NaN, 406.8f, 466.3f, 493.7f, 440.3f,
+ 493.7f, 347.5f, float.NaN, float.NaN, float.NaN, float.NaN, 493.7f, 254.7f, 413, 228.6f,
+ 338.6f, 222.1f, 338.6f, 490.7f,
+ ],
+ [
+ 336.7f, 1150.8f, 214.8f, 1150.8f, 214.8f, 1073.4f, float.NaN, float.NaN, float.NaN, float.NaN,
+ 64.2f, 1060.7f, -92.4f, 1003.8f, -92.4f, 817.3f, float.NaN, float.NaN, float.NaN, float.NaN,
+ -92.4f, 641.8f, 82.3f, 578.6f, 214.8f, 535.9f, 214.8f, 229.2f, 193.7f, 229.2f,
+ float.NaN, float.NaN, float.NaN, float.NaN, 124.4f, 229.2f, 59.7f, 251.3f, 62.7f, 336.7f,
+ -92.4f, 336.7f, -92.4f, 306.7f, float.NaN, float.NaN, float.NaN, float.NaN, -92.4f, 161.2f,
+ 65.7f, 80.6f, 186.2f, 80.6f, 214.8f, 80.6f, 214.8f, 0, 336.7f, 0,
+ 336.7f, 80.6f, float.NaN, float.NaN, float.NaN, float.NaN, 481.3f, 93.3f, 642.4f, 188.1f,
+ 642.4f, 358.8f, float.NaN, float.NaN, float.NaN, float.NaN, 642.4f, 540.6f, 476.7f, 602.3f,
+ 336.7f, 651.3f, 336.7f, 924.8f, float.NaN, float.NaN, float.NaN, float.NaN, 413.5f, 924.8f,
+ 463.2f, 913.7f, 458.7f, 817.3f, 613.7f, 817.3f, 613.7f, 844.1f, float.NaN, float.NaN,
+ float.NaN, float.NaN, 613.7f, 988, 449.6f, 1062.3f, 336.7f, 1073.4f, 336.7f, 1150.8f,
+ ],
+ ],
+
+ ['%'] =
+ [
+ [
+ 97.4f, 1124.8f, float.NaN, float.NaN, float.NaN, float.NaN, -0.9f, 1124.8f, -70.5f, 1046.9f,
+ -70.5f, 938.5f, float.NaN, float.NaN, float.NaN, float.NaN, -70.5f, 839.2f, -3.9f, 756.7f,
+ 97.4f, 756.7f, float.NaN, float.NaN, float.NaN, float.NaN, 198.7f, 756.7f, 265.2f, 839.2f,
+ 265.2f, 938.5f, float.NaN, float.NaN, float.NaN, float.NaN, 265.2f, 1046.9f, 195.6f, 1124.8f,
+ 97.4f, 1124.8f,
+ ],
+ [
+ 95.6f, 1036.2f, float.NaN, float.NaN, float.NaN, float.NaN, 157.2f, 1036.2f, 157.2f, 988.9f,
+ 157.2f, 938.5f, float.NaN, float.NaN, float.NaN, float.NaN, 157.2f, 897.2f, 157.2f, 845.3f,
+ 95.6f, 845.3f, float.NaN, float.NaN, float.NaN, float.NaN, 34, 845.3f, 34, 897.2f,
+ 34, 938.5f, float.NaN, float.NaN, float.NaN, float.NaN, 34, 988.9f, 34, 1036.2f,
+ 95.6f, 1036.2f,
+ ],
+ [
+ 460.7f, 365, float.NaN, float.NaN, float.NaN, float.NaN, 360.9f, 365, 292.8f, 284.8f,
+ 292.8f, 173.1f, float.NaN, float.NaN, float.NaN, float.NaN, 292.8f, 70.8f, 359.3f, -14.2f,
+ 460.7f, -14.2f, float.NaN, float.NaN, float.NaN, float.NaN, 562, -14.2f, 628.5f, 70.8f,
+ 628.5f, 173.1f, float.NaN, float.NaN, float.NaN, float.NaN, 628.5f, 284.8f, 558.9f, 365,
+ 460.7f, 365,
+ ],
+ [
+ 455.4f, 271.8f, float.NaN, float.NaN, float.NaN, float.NaN, 517, 271.8f, 517, 225.9f,
+ 517, 177, float.NaN, float.NaN, float.NaN, float.NaN, 517, 137, 517, 86.6f,
+ 455.4f, 86.6f, float.NaN, float.NaN, float.NaN, float.NaN, 393.8f, 86.6f, 393.8f, 137,
+ 393.8f, 177, float.NaN, float.NaN, float.NaN, float.NaN, 393.8f, 225.9f, 393.8f, 271.8f,
+ 455.4f, 271.8f,
+ ],
+ [
+ 550, 1145.3f, -60.5f, 85, -60.5f, 0, 23.6f, 0, 626.3f, 1054.1f,
+ 626.3f, 1145.3f, 550, 1145.3f,
+ ],
+ ],
+
+ ['&'] =
+ [
+ [
+ 159.7f, 698, float.NaN, float.NaN, float.NaN, float.NaN, 125.4f, 747.2f, 78.3f, 820.3f,
+ 78.3f, 883.8f, float.NaN, float.NaN, float.NaN, float.NaN, 78.3f, 972.8f, 134, 1034.7f,
+ 212.5f, 1034.7f, float.NaN, float.NaN, float.NaN, float.NaN, 279.6f, 1034.7f, 345.2f, 1001.4f,
+ 345.2f, 917.2f, float.NaN, float.NaN, float.NaN, float.NaN, 345.2f, 829.8f, 209.7f, 747.2f,
+ 159.7f, 698,
+ ],
+ [
+ 355.6f, 156.2f, float.NaN, float.NaN, float.NaN, float.NaN, 323.7f, 134.9f, 285.9f, 125.1f,
+ 249.5f, 125.1f, float.NaN, float.NaN, float.NaN, float.NaN, 143.4f, 125.1f, 51, 195.4f,
+ 51, 318.1f, float.NaN, float.NaN, float.NaN, float.NaN, 51, 381.9f, 94.9f, 445.7f,
+ 138.9f, 485, 355.6f, 156.2f,
+ ],
+ [
+ 454.9f, 0, 638.9f, 0, 532.9f, 155.7f, float.NaN, float.NaN, float.NaN, float.NaN,
+ 590.3f, 238.3f, 625.6f, 401.9f, 625.6f, 500.4f, 625.6f, 527.4f, 474.1f, 527.4f,
+ 474.1f, 490.8f, float.NaN, float.NaN, float.NaN, float.NaN, 474.1f, 420.9f, 465.2f, 349.5f,
+ 446.1f, 282.7f, 244.5f, 581.4f, 331.3f, 651.3f, float.NaN, float.NaN, float.NaN, float.NaN,
+ 415.2f, 718, 513.8f, 813.3f, 513.8f, 934, float.NaN, float.NaN, float.NaN, float.NaN,
+ 513.8f, 1092.8f, 365.2f, 1177, 235.7f, 1177, float.NaN, float.NaN, float.NaN, float.NaN,
+ 94.4f, 1177, -64.5f, 1086.5f, -64.5f, 913.3f, float.NaN, float.NaN, float.NaN, float.NaN,
+ -64.5f, 806.9f, 4.7f, 691, 59.1f, 608.4f, float.NaN, float.NaN, float.NaN, float.NaN,
+ -32.1f, 543.2f, -101.2f, 428.9f, -101.2f, 306.6f, float.NaN, float.NaN, float.NaN, float.NaN,
+ -101.2f, 101.7f, 42.9f, -14.3f, 222.5f, -14.3f, float.NaN, float.NaN, float.NaN, float.NaN,
+ 294.6f, -14.3f, 360.8f, -1.6f, 424, 42.9f, 454.9f, 0,
+ ],
+ ],
+
+ ['\''] =
+ [
+ [
+ 198.2f, 628.7f, 351.8f, 628.7f, 383.5f, 1177.6f, 166.6f, 1177.6f, 198.2f, 628.7f,
+ ],
+ ],
+
+ ['('] =
+ [
+ [
+ 362.9f, 1161.8f, float.NaN, float.NaN, float.NaN, float.NaN, 189, 1047.4f, 54.5f, 801.2f,
+ 54.5f, 580.1f, float.NaN, float.NaN, float.NaN, float.NaN, 54.5f, 360.6f, 189, 112.9f,
+ 362.9f, 0, 475.5f, 0, 475.5f, 112.9f, float.NaN, float.NaN, float.NaN, float.NaN,
+ 319.1f, 153.7f, 205.1f, 421.8f, 205.1f, 580.1f, float.NaN, float.NaN, float.NaN, float.NaN,
+ 205.1f, 738.5f, 319.1f, 1006.6f, 475.5f, 1048.9f, 475.5f, 1161.8f, 362.9f, 1161.8f,
+ ],
+ ],
+
+ [')'] =
+ [
+ [
+ 70.5f, 1161.8f, 70.5f, 1048.9f, float.NaN, float.NaN, float.NaN, float.NaN, 224, 1006.6f,
+ 338.4f, 738.5f, 338.4f, 580.1f, float.NaN, float.NaN, float.NaN, float.NaN, 338.4f, 421.8f,
+ 224, 153.7f, 70.5f, 112.9f, 70.5f, 0, 182, 0, float.NaN, float.NaN,
+ float.NaN, float.NaN, 354.3f, 112.9f, 487.5f, 360.6f, 487.5f, 580.1f, float.NaN, float.NaN,
+ float.NaN, float.NaN, 487.5f, 801.2f, 354.3f, 1047.4f, 182, 1161.8f, 70.5f, 1161.8f,
+ ],
+ ],
+
+ ['*'] =
+ [
+ [
+ 329.8f, 590.9f, 351.2f, 886.3f, 195.8f, 886.3f, 209.5f, 590.9f, -35.7f, 700.9f,
+ -98.2f, 543.7f, 165.3f, 457.3f, -6.8f, 232.6f, 116.6f, 139.9f, 276.5f, 381.9f,
+ 425.8f, 139.9f, 564.4f, 232.6f, 380.1f, 457.3f, 648.2f, 534.3f, 581.2f, 700.9f,
+ 329.8f, 590.9f,
+ ],
+ ],
+
+ ['+'] =
+ [
+ [
+ 204.9f, 634.7f, -63.3f, 634.7f, -63.3f, 467.5f, 204.9f, 467.5f, 204.9f, 123.4f,
+ 343.7f, 123.4f, 343.7f, 467.5f, 613.3f, 467.5f, 613.3f, 634.7f, 343.7f, 634.7f,
+ 343.7f, 936.6f, 204.9f, 936.6f, 204.9f, 634.7f,
+ ],
+ ],
+
+ [','] =
+ [
+ [
+ 297.8f, 276.4f, 3.1f, -270.2f, 169.9f, -270.2f, 560.9f, 157.7f, 560.9f, 276.4f,
+ 297.8f, 276.4f,
+ ],
+ ],
+
+ ['-'] =
+ [
+ [
+ -67.7f, 544.6f, 617.7f, 544.6f, 617.7f, 739.3f, -67.7f, 739.3f, -67.7f, 544.6f,
+ ],
+ ],
+
+ ['.'] =
+ [
+ [
+ 119.4f, 269.3f, 119.4f, 0, 430.7f, 0, 430.7f, 269.3f, 119.4f, 269.3f,
+ ],
+ ],
+
+ ['/'] =
+ [
+ [
+ 423.3f, 1195.7f, -24.5f, 0, 128.3f, 0, 580.5f, 1195.7f, 423.3f, 1195.7f,
+ ],
+ ],
+
+ ['0'] =
+ [
+ [
+ 279, 1137, float.NaN, float.NaN, float.NaN, float.NaN, 130.9f, 1137, 11.5f, 1120.1f,
+ -39.9f, 968.2f, float.NaN, float.NaN, float.NaN, float.NaN, -80.7f, 847, -88.3f, 704.3f,
+ -88.3f, 576.9f, float.NaN, float.NaN, float.NaN, float.NaN, -88.3f, 440.4f, -80.7f, 151.9f,
+ 19, 53.7f, float.NaN, float.NaN, float.NaN, float.NaN, 75, -1.5f, 191.3f, -13.8f,
+ 279, -13.8f, float.NaN, float.NaN, float.NaN, float.NaN, 365.2f, -13.8f, 483, -1.5f,
+ 537.5f, 53.7f, float.NaN, float.NaN, float.NaN, float.NaN, 638.7f, 151.9f, 646.3f, 440.4f,
+ 646.3f, 576.9f, float.NaN, float.NaN, float.NaN, float.NaN, 646.3f, 704.3f, 638.7f, 847,
+ 597.9f, 968.2f, float.NaN, float.NaN, float.NaN, float.NaN, 546.5f, 1120.1f, 425.6f, 1137,
+ 279, 1137,
+ ],
+ [
+ 279, 1008.6f, float.NaN, float.NaN, float.NaN, float.NaN, 399.9f, 1008.6f, 454.3f, 1003.8f,
+ 474, 882.1f, float.NaN, float.NaN, float.NaN, float.NaN, 487.6f, 795.2f, 490.6f, 653,
+ 490.6f, 562.9f, float.NaN, float.NaN, float.NaN, float.NaN, 490.6f, 479.1f, 486.1f, 223.1f,
+ 443.7f, 163, float.NaN, float.NaN, float.NaN, float.NaN, 415, 121.9f, 330.4f, 120.3f,
+ 279, 120.3f, float.NaN, float.NaN, float.NaN, float.NaN, 227.6f, 120.3f, 143, 121.9f,
+ 114.3f, 163, float.NaN, float.NaN, float.NaN, float.NaN, 70.4f, 223.1f, 67.4f, 479.1f,
+ 67.4f, 562.9f, float.NaN, float.NaN, float.NaN, float.NaN, 67.4f, 653, 68.9f, 795.2f,
+ 84, 882.1f, float.NaN, float.NaN, float.NaN, float.NaN, 103.7f, 1003.8f, 158.1f, 1008.6f,
+ 279, 1008.6f,
+ ],
+ ],
+
+ ['1'] =
+ [
+ [
+ 323.7f, 956.3f, 323.7f, 0, 470, 0, 470, 1137.6f, 312.3f, 1137.6f,
+ 60.9f, 906.3f, 60.9f, 706.3f, 323.7f, 956.3f,
+ ],
+ ],
+
+ ['2'] =
+ [
+ [
+ 95.9f, 146.6f, 95.9f, 170, float.NaN, float.NaN, float.NaN, float.NaN, 95.9f, 301,
+ 212.4f, 393.1f, 308.1f, 460.1f, float.NaN, float.NaN, float.NaN, float.NaN, 452.5f, 558.4f,
+ 594, 637.9f, 594, 843.8f, float.NaN, float.NaN, float.NaN, float.NaN, 594, 1052.8f,
+ 437.8f, 1155.8f, 253.6f, 1155.8f, float.NaN, float.NaN, float.NaN, float.NaN, 156.4f, 1155.8f,
+ 42.9f, 1124.6f, -36.7f, 1060.6f, -36.7f, 887.5f, float.NaN, float.NaN, float.NaN, float.NaN,
+ 45.8f, 951.4f, 172.6f, 1009.2f, 277.2f, 1009.2f, float.NaN, float.NaN, float.NaN, float.NaN,
+ 362.7f, 1009.2f, 442.2f, 929.6f, 442.2f, 837.6f, float.NaN, float.NaN, float.NaN, float.NaN,
+ 442.2f, 725.3f, 322.9f, 653.5f, 243.3f, 600.5f, float.NaN, float.NaN, float.NaN, float.NaN,
+ 37, 461.7f, -55.8f, 354.1f, -55.8f, 85.8f, -55.8f, 0, 580.7f, 0,
+ 580.7f, 146.6f, 95.9f, 146.6f,
+ ],
+ ],
+
+ ['3'] =
+ [
+ [
+ 580.7f, 1141.7f, -65.2f, 1141.7f, -65.2f, 994.3f, 388.4f, 994.3f, 113.9f, 694.7f,
+ 113.9f, 570.8f, 231.3f, 570.8f, float.NaN, float.NaN, float.NaN, float.NaN, 360.5f, 570.8f,
+ 472.1f, 512.8f, 472.1f, 360.7f, float.NaN, float.NaN, float.NaN, float.NaN, 472.1f, 196,
+ 342.9f, 133.3f, 207.8f, 133.3f, float.NaN, float.NaN, float.NaN, float.NaN, 110.9f, 133.3f,
+ 18.5f, 156.8f, -65.2f, 214.8f, -65.2f, 53.3f, float.NaN, float.NaN, float.NaN, float.NaN,
+ 24.3f, 12.5f, 112.4f, -14.1f, 210.8f, -14.1f, float.NaN, float.NaN, float.NaN, float.NaN,
+ 425.1f, -14.1f, 623.3f, 101.9f, 623.3f, 356, float.NaN, float.NaN, float.NaN, float.NaN,
+ 623.3f, 558.3f, 495.5f, 685.3f, 312.1f, 694.7f, 580.7f, 981.7f, 580.7f, 1141.7f,
+ ],
+ ],
+
+ ['4'] =
+ [
+ [
+ 481, 387.5f, 481, 616.3f, 327.6f, 616.3f, 327.6f, 387.5f, 65.4f, 387.5f,
+ 393.1f, 1110.6f, 235.2f, 1110.6f, -83.5f, 408.8f, -83.5f, 244.1f, 327.6f, 244.1f,
+ 327.6f, 0, 481, 0, 481, 244.1f, 613.5f, 244.1f, 613.5f, 387.5f,
+ 481, 387.5f,
+ ],
+ ],
+
+ ['5'] =
+ [
+ [
+ 137.2f, 994.3f, 552.9f, 994.3f, 552.9f, 1141.7f, 0.2f, 1141.7f, -33, 611.6f,
+ 153.8f, 625.7f, float.NaN, float.NaN, float.NaN, float.NaN, 242.6f, 632, 427.9f, 611.6f,
+ 427.9f, 409.3f, float.NaN, float.NaN, float.NaN, float.NaN, 427.9f, 192.9f, 232.1f, 133.3f,
+ 55.9f, 133.3f, -33, 133.3f, -33, -14.1f, 18.2f, -14.1f, float.NaN, float.NaN,
+ float.NaN, float.NaN, 153.8f, -14.1f, 583, 22, 583, 407.7f, float.NaN, float.NaN,
+ float.NaN, float.NaN, 583, 671.2f, 381.2f, 773.1f, 155.3f, 773.1f, 119.1f, 773.1f,
+ 137.2f, 994.3f,
+ ],
+ ],
+
+ ['6'] =
+ [
+ [
+ 337, 1150.6f, 60.4f, 817.1f, float.NaN, float.NaN, float.NaN, float.NaN, -47, 687.5f,
+ -93.8f, 551.6f, -93.8f, 377.7f, float.NaN, float.NaN, float.NaN, float.NaN, -93.8f, 132.8f,
+ 40.7f, -14.2f, 276.5f, -14.2f, float.NaN, float.NaN, float.NaN, float.NaN, 488.1f, -14.2f,
+ 643.8f, 134.3f, 643.8f, 361.9f, float.NaN, float.NaN, float.NaN, float.NaN, 643.8f, 562.6f,
+ 506.3f, 739.7f, 306.7f, 739.7f, float.NaN, float.NaN, float.NaN, float.NaN, 256.9f, 739.7f,
+ 211.5f, 730.2f, 167.7f, 709.6f, 166.2f, 712.8f, 238.7f, 812.4f, float.NaN, float.NaN,
+ float.NaN, float.NaN, 323.4f, 926.2f, 427.7f, 1049.4f, 532, 1150.6f, 337, 1150.6f,
+ ],
+ [
+ 272, 594.8f, float.NaN, float.NaN, float.NaN, float.NaN, 397.4f, 594.8f, 488.1f, 500.4f,
+ 488.1f, 362, float.NaN, float.NaN, float.NaN, float.NaN, 488.1f, 218.8f, 402, 124.4f,
+ 275, 124.4f, float.NaN, float.NaN, float.NaN, float.NaN, 145, 124.4f, 61.9f, 223.7f,
+ 61.9f, 363.7f, float.NaN, float.NaN, float.NaN, float.NaN, 61.9f, 495.5f, 149.5f, 594.8f,
+ 272, 594.8f,
+ ],
+ ],
+
+ ['7'] =
+ [
+ [
+ 452.6f, 998.5f, float.NaN, float.NaN, float.NaN, float.NaN, 448.1f, 957.5f, 419.4f, 922.9f,
+ 392.3f, 891.4f, 337.9f, 825.3f, float.NaN, float.NaN, float.NaN, float.NaN, 184, 636.3f,
+ 72.3f, 437.8f, 72.3f, 184.3f, 72.3f, 0, 227.8f, 0, 227.8f, 184.3f,
+ float.NaN, float.NaN, float.NaN, float.NaN, 227.8f, 453.6f, 336.4f, 590.6f, 496.4f, 787.5f,
+ float.NaN, float.NaN, float.NaN, float.NaN, 529.6f, 830, 586.9f, 902.4f, 609.6f, 951.2f,
+ float.NaN, float.NaN, float.NaN, float.NaN, 636.7f, 1011.1f, 638.3f, 1061.5f, 638.3f, 1127.6f,
+ 638.3f, 1146.5f, -84.6f, 1146.5f, -84.6f, 998.5f, 452.6f, 998.5f,
+ ],
+ ],
+
+ ['8'] =
+ [
+ [
+ 271, 121.9f, float.NaN, float.NaN, float.NaN, float.NaN, 164.4f, 121.9f, 54.8f, 176.3f,
+ 54.8f, 309, float.NaN, float.NaN, float.NaN, float.NaN, 54.8f, 435.4f, 159.8f, 492.9f,
+ 271, 555.3f, float.NaN, float.NaN, float.NaN, float.NaN, 380.7f, 492.9f, 487.2f, 435.4f,
+ 487.2f, 309, float.NaN, float.NaN, float.NaN, float.NaN, 487.2f, 176.3f, 377.6f, 121.9f,
+ 271, 121.9f,
+ ],
+ [
+ 271, 1150.5f, float.NaN, float.NaN, float.NaN, float.NaN, 118.1f, 1150.5f, -51.4f, 1080.6f,
+ -51.4f, 900.5f, float.NaN, float.NaN, float.NaN, float.NaN, -51.4f, 770.1f, 40.1f, 695.6f,
+ 134.5f, 631.9f, float.NaN, float.NaN, float.NaN, float.NaN, -0.4f, 563.6f, -93.4f, 467.3f,
+ -93.4f, 299.7f, float.NaN, float.NaN, float.NaN, float.NaN, -93.4f, 69.9f, 71.6f, -14,
+ 271, -14, float.NaN, float.NaN, float.NaN, float.NaN, 468.9f, -14, 635.4f, 69.9f,
+ 635.4f, 299.7f, float.NaN, float.NaN, float.NaN, float.NaN, 635.4f, 467.3f, 542.4f, 563.6f,
+ 406, 631.9f, float.NaN, float.NaN, float.NaN, float.NaN, 501.9f, 695.6f, 593.4f, 770.1f,
+ 593.4f, 900.5f, float.NaN, float.NaN, float.NaN, float.NaN, 593.4f, 1080.6f, 423.9f, 1150.5f,
+ 271, 1150.5f,
+ ],
+ [
+ 264, 1004.5f, float.NaN, float.NaN, float.NaN, float.NaN, 339.7f, 1004.5f, 437, 981.2f,
+ 437, 886.5f, float.NaN, float.NaN, float.NaN, float.NaN, 437, 798, 333.5f, 737.5f,
+ 264, 709.5f, float.NaN, float.NaN, float.NaN, float.NaN, 194.5f, 737.5f, 91, 798,
+ 91, 886.5f, float.NaN, float.NaN, float.NaN, float.NaN, 91, 981.2f, 188.3f, 1004.5f,
+ 264, 1004.5f,
+ ],
+ ],
+
+ ['9'] =
+ [
+ [
+ 214.2f, 0, float.NaN, float.NaN, float.NaN, float.NaN, 401.9f, 205.8f, 643.7f, 477.2f,
+ 643.7f, 754.7f, float.NaN, float.NaN, float.NaN, float.NaN, 643.7f, 993.3f, 510.1f, 1155.5f,
+ 278.8f, 1155.5f, float.NaN, float.NaN, float.NaN, float.NaN, 56.5f, 1155.5f, -93.7f, 1005.8f,
+ -93.7f, 775, float.NaN, float.NaN, float.NaN, float.NaN, -93.7f, 569.2f, 50.5f, 410.1f,
+ 248.7f, 410.1f, float.NaN, float.NaN, float.NaN, float.NaN, 293.8f, 410.1f, 340.3f, 421,
+ 376.4f, 439.7f, 379.4f, 436.6f, 380.9f, 438.2f, float.NaN, float.NaN, float.NaN, float.NaN,
+ 278.8f, 276, 143.6f, 135.7f, 8.4f, 0, 214.2f, 0,
+ ],
+ [
+ 278.8f, 1008.9f, float.NaN, float.NaN, float.NaN, float.NaN, 406.4f, 1008.9f, 489, 916.9f,
+ 489, 782.8f, float.NaN, float.NaN, float.NaN, float.NaN, 489, 651.8f, 403.4f, 556.7f,
+ 278.8f, 556.7f, float.NaN, float.NaN, float.NaN, float.NaN, 151.1f, 556.7f, 61, 647.1f,
+ 61, 779.7f, float.NaN, float.NaN, float.NaN, float.NaN, 61, 924.7f, 140.6f, 1008.9f,
+ 278.8f, 1008.9f,
+ ],
+ ],
+
+ [':'] =
+ [
+ [
+ 135, 818, 135, 569.9f, 421.1f, 569.9f, 421.1f, 818, 135, 818,
+ ],
+ [
+ 123.5f, 248.2f, 123.5f, 0, 418.5f, 0, 418.5f, 248.2f, 123.5f, 248.2f,
+ ],
+ ],
+
+ [';'] =
+ [
+ [
+ 231.3f, 238, -5.2f, -272.7f, 180.6f, -272.7f, 546.2f, 238, 231.3f, 238,
+ ],
+ [
+ 243.9f, 789.6f, 243.9f, 550.1f, 508.6f, 550.1f, 508.6f, 789.6f, 243.9f, 789.6f,
+ ],
+ ],
+
+ ['<'] =
+ [
+ [
+ 626.9f, 313, 120.2f, 645.1f, 626.9f, 977.1f, 626.9f, 1174.3f, -104.9f, 664.1f,
+ -104.9f, 620.9f, 626.9f, 112.4f, 626.9f, 313,
+ ],
+ ],
+
+ ['='] =
+ [
+ [
+ -59, 297.1f, 609, 297.1f, 609, 428.6f, -59, 428.6f, -59, 297.1f,
+ ],
+ [
+ -59, 642, 609, 642, 609, 777.6f, -59, 777.6f, -59, 642,
+ ],
+ ],
+
+ ['>'] =
+ [
+ [
+ 653, 661.5f, -67, 1169.7f, -67, 973.3f, 431.6f, 642.6f, -67, 311.8f,
+ -67, 112, 653, 618.5f, 653, 661.5f,
+ ],
+ ],
+
+ ['?'] =
+ [
+ [
+ 385.1f, 396.1f, 385.1f, 425.2f, float.NaN, float.NaN, float.NaN, float.NaN, 385.1f, 478.6f,
+ 397.3f, 509.3f, 432.5f, 541.6f, float.NaN, float.NaN, float.NaN, float.NaN, 521.2f, 628.9f,
+ 576.2f, 701.7f, 576.2f, 835.9f, float.NaN, float.NaN, float.NaN, float.NaN, 576.2f, 1010.5f,
+ 446.2f, 1110.7f, 290.3f, 1110.7f, float.NaN, float.NaN, float.NaN, float.NaN, 84, 1110.7f,
+ -18.5f, 1004, -18.5f, 795.4f, -18.5f, 756.6f, 139, 756.6f, 139, 777.7f,
+ float.NaN, float.NaN, float.NaN, float.NaN, 139, 881.1f, 178.7f, 958.7f, 287.3f, 958.7f,
+ float.NaN, float.NaN, float.NaN, float.NaN, 371.3f, 958.7f, 418.7f, 897.3f, 418.7f, 813.2f,
+ float.NaN, float.NaN, float.NaN, float.NaN, 418.7f, 738.9f, 389.7f, 701.7f, 342.3f, 654.8f,
+ float.NaN, float.NaN, float.NaN, float.NaN, 314.8f, 630.5f, 259.8f, 573.9f, 244.5f, 541.6f,
+ float.NaN, float.NaN, float.NaN, float.NaN, 227.6f, 507.7f, 229.2f, 472.1f, 229.2f, 433.3f,
+ 229.2f, 396.1f, 385.1f, 396.1f,
+ ],
+ [
+ 193.7f, 203.9f, 193.7f, 0, 404.2f, 0, 404.2f, 203.9f, 193.7f, 203.9f,
+ ],
+ ],
+
+ ['@'] =
+ [
+ [
+ 366.5f, 613.4f, 247.5f, 613.4f, 247.5f, 570.6f, 226.7f, 589.6f, float.NaN, float.NaN,
+ float.NaN, float.NaN, 198.4f, 613.4f, 165.7f, 621.3f, 139, 621.3f, float.NaN, float.NaN,
+ float.NaN, float.NaN, -42.5f, 621.3f, -70.7f, 467.6f, -70.7f, 378.8f, -70.7f, 220.3f,
+ float.NaN, float.NaN, float.NaN, float.NaN, -70.7f, 80.8f, 2.1f, -14.3f, 139, -14.3f,
+ float.NaN, float.NaN, float.NaN, float.NaN, 195.5f, -14.3f, 250.5f, 7.9f, 286.2f, 58.6f,
+ 292.1f, 69.7f, 305.5f, 46, float.NaN, float.NaN, float.NaN, float.NaN, 332.3f, -3.2f,
+ 373.9f, -14.3f, 426, -14.3f, float.NaN, float.NaN, float.NaN, float.NaN, 595.5f, -14.3f,
+ 620.8f, 144.2f, 620.8f, 291.6f, 620.8f, 599.1f, float.NaN, float.NaN, float.NaN, float.NaN,
+ 620.8f, 874.9f, 525.6f, 1074.6f, 266.8f, 1074.6f, float.NaN, float.NaN, float.NaN, float.NaN,
+ 72, 1074.6f, -70.7f, 987.4f, -70.7f, 779.8f, -70.7f, 765.5f, 48.2f, 765.5f,
+ 48.2f, 794.1f, float.NaN, float.NaN, float.NaN, float.NaN, 48.2f, 908.2f, 158.3f, 952.5f,
+ 266.8f, 952.5f, float.NaN, float.NaN, float.NaN, float.NaN, 436.4f, 952.5f, 501.8f, 828.9f,
+ 501.8f, 648.2f, 501.8f, 309.1f, float.NaN, float.NaN, float.NaN, float.NaN, 501.8f, 191.8f,
+ 489.9f, 107.8f, 426, 107.8f, float.NaN, float.NaN, float.NaN, float.NaN, 370.9f, 107.8f,
+ 366.5f, 164.8f, 366.5f, 204.5f, 366.5f, 613.4f,
+ ],
+ [
+ 55.1f, 376.2f, float.NaN, float.NaN, float.NaN, float.NaN, 55.1f, 444.7f, 83.9f, 500.2f,
+ 143.1f, 500.2f, float.NaN, float.NaN, float.NaN, float.NaN, 219.5f, 500.2f, 248.4f, 444.7f,
+ 248.4f, 376.2f, 248.4f, 212.9f, float.NaN, float.NaN, float.NaN, float.NaN, 248.4f, 146,
+ 219.5f, 97, 143.1f, 97, float.NaN, float.NaN, float.NaN, float.NaN, 82.5f, 97,
+ 55.1f, 146, 55.1f, 212.9f, 55.1f, 376.2f,
+ ],
+ ],
+
+ ['A'] =
+ [
+ [
+ 386.4f, 451.4f, 163.5f, 451.4f, 274.2f, 903.5f, 277.2f, 903.5f, 386.4f, 451.4f,
+ ],
+ [
+ 420.8f, 309.1f, 494.1f, 0, 649.7f, 0, 373, 1074.9f, 177, 1074.9f,
+ -99.7f, 0, 55.8f, 0, 127.6f, 309.1f, 420.8f, 309.1f,
+ ],
+ ],
+
+ ['B'] =
+ [
+ [
+ 56.4f, 910.4f, 243.7f, 910.4f, float.NaN, float.NaN, float.NaN, float.NaN, 343.5f, 910.4f,
+ 463.2f, 893.3f, 463.2f, 762.3f, float.NaN, float.NaN, float.NaN, float.NaN, 463.2f, 634.5f,
+ 363.4f, 623.6f, 265.2f, 623.6f, 56.4f, 623.6f, 56.4f, 910.4f,
+ ],
+ [
+ 56.4f, 476.3f, 262.1f, 476.3f, float.NaN, float.NaN, float.NaN, float.NaN, 383.4f, 476.3f,
+ 487.8f, 449.1f, 487.8f, 305.5f, float.NaN, float.NaN, float.NaN, float.NaN, 487.8f, 169.4f,
+ 383.4f, 142.1f, 269.8f, 142.1f, 56.4f, 142.1f, 56.4f, 476.3f,
+ ],
+ [
+ 304.7f, 0, float.NaN, float.NaN, float.NaN, float.NaN, 538.7f, 0, 641.6f, 154.3f,
+ 641.6f, 314.9f, float.NaN, float.NaN, float.NaN, float.NaN, 641.6f, 417.8f, 589.4f, 528.5f,
+ 488, 551.9f, 488, 555, float.NaN, float.NaN, float.NaN, float.NaN, 570, 584.6f,
+ 617.7f, 665.7f, 617.7f, 754.5f, float.NaN, float.NaN, float.NaN, float.NaN, 617.7f, 988.4f,
+ 431.4f, 1057, 242.1f, 1057, -84.3f, 1057, -84.3f, 0, 304.7f, 0,
+ ],
+ ],
+
+ ['C'] =
+ [
+ [
+ 572, 837.6f, float.NaN, float.NaN, float.NaN, float.NaN, 572, 992.4f, 446.1f, 1096.1f,
+ 307, 1096.1f, float.NaN, float.NaN, float.NaN, float.NaN, 42, 1096.1f, -30.4f, 786.5f,
+ -30.4f, 537.7f, float.NaN, float.NaN, float.NaN, float.NaN, -30.4f, 301.5f, 42, -14.4f,
+ 307, -14.4f, float.NaN, float.NaN, float.NaN, float.NaN, 446.1f, -14.4f, 572, 92.5f,
+ 572, 253.7f, 422.9f, 253.7f, float.NaN, float.NaN, float.NaN, float.NaN, 418.5f, 183.5f,
+ 363.5f, 135.6f, 301.2f, 135.6f, float.NaN, float.NaN, float.NaN, float.NaN, 140.5f, 135.6f,
+ 118.8f, 416.4f, 118.8f, 545.6f, float.NaN, float.NaN, float.NaN, float.NaN, 118.8f, 673.3f,
+ 143.4f, 946.1f, 301.2f, 946.1f, float.NaN, float.NaN, float.NaN, float.NaN, 363.5f, 946.1f,
+ 418.5f, 903, 422.9f, 837.6f, 572, 837.6f,
+ ],
+ ],
+
+ ['D'] =
+ [
+ [
+ -60f, 0, 51.8f, 0, float.NaN, float.NaN, float.NaN, float.NaN, 361.3f, 0,
+ 586.4f, 186.4f, 586.4f, 509, float.NaN, float.NaN, float.NaN, float.NaN, 586.4f, 830.1f,
+ 347.2f, 1061.9f, 31.4f, 1061.9f, -60f, 1061.9f, -60f, 0,
+ ],
+ [
+ 103.6f, 914.6f, float.NaN, float.NaN, float.NaN, float.NaN, 300.9f, 905.3f, 426.9f, 703.2f,
+ 426.9f, 512.1f, float.NaN, float.NaN, float.NaN, float.NaN, 426.9f, 311.7f, 310, 142.5f,
+ 103.6f, 147.2f, 103.6f, 914.6f,
+ ],
+ ],
+
+ ['E'] =
+ [
+ [
+ 98.9f, 925.5f, 563.6f, 925.5f, 563.6f, 1074.5f, -59.1f, 1074.5f, -59.1f, 0,
+ 582, 0, 582, 149, 98.9f, 149, 98.9f, 486.5f, 503.8f, 486.5f,
+ 503.8f, 635.5f, 98.9f, 635.5f, 98.9f, 925.5f,
+ ],
+ ],
+
+ ['F'] =
+ [
+ [
+ 151.9f, 933.3f, 536.9f, 933.3f, 536.9f, 1083.5f, -4.9f, 1083.5f, -4.9f, 0,
+ 151.9f, 0, 151.9f, 490.6f, 462.3f, 490.6f, 462.3f, 640.9f, 151.9f, 640.9f,
+ 151.9f, 933.3f,
+ ],
+ ],
+
+ ['G'] =
+ [
+ [
+ 469.2f, 429.3f, 469.2f, 161.2f, float.NaN, float.NaN, float.NaN, float.NaN, 429.7f, 145.2f,
+ 388.6f, 135.6f, 343, 135.6f, float.NaN, float.NaN, float.NaN, float.NaN, 114.8f, 135.6f,
+ 88.9f, 367, 88.9f, 556.9f, float.NaN, float.NaN, float.NaN, float.NaN, 88.9f, 761.2f,
+ 125.4f, 946.3f, 344.5f, 946.3f, float.NaN, float.NaN, float.NaN, float.NaN, 412.9f, 946.3f,
+ 454, 928.7f, 469.2f, 864.9f, 625.9f, 864.9f, 625.9f, 877.7f, float.NaN, float.NaN,
+ float.NaN, float.NaN, 625.9f, 987.8f, 495.1f, 1096.3f, 344.5f, 1096.3f, float.NaN, float.NaN,
+ float.NaN, float.NaN, 23.5f, 1096.3f, -67.8f, 833, -67.8f, 542.5f, float.NaN, float.NaN,
+ float.NaN, float.NaN, -67.8f, 264.9f, 17.4f, -14.4f, 329.3f, -14.4f, float.NaN, float.NaN,
+ float.NaN, float.NaN, 428.2f, -14.4f, 534.7f, 11.2f, 625.9f, 51.1f, 625.9f, 579.3f,
+ 256.2f, 579.3f, 256.2f, 429.3f, 469.2f, 429.3f,
+ ],
+ ],
+
+ ['H'] =
+ [
+ [
+ 453.1f, 472.3f, 453.1f, 0, 615.4f, 0, 615.4f, 1049.9f, 453.1f, 1049.9f,
+ 453.1f, 617.9f, 96.9f, 617.9f, 96.9f, 1049.9f, -65.4f, 1049.9f, -65.4f, 0,
+ 96.9f, 0, 96.9f, 472.3f, 453.1f, 472.3f,
+ ],
+ ],
+
+ ['I'] =
+ [
+ [
+ 338.7f, 925.5f, 483.2f, 925.5f, 483.2f, 1074.5f, 45.4f, 1074.5f, 45.4f, 925.5f,
+ 191.4f, 925.5f, 191.4f, 149, 16.8f, 149, 16.8f, 0, 513.3f, 0,
+ 513.3f, 149, 338.7f, 149, 338.7f, 925.5f,
+ ],
+ ],
+
+ ['J'] =
+ [
+ [
+ 396.9f, 1065.9f, 396.9f, 323.8f, float.NaN, float.NaN, float.NaN, float.NaN, 396.9f, 231.1f,
+ 404.5f, 133.6f, 287.1f, 133.6f, float.NaN, float.NaN, float.NaN, float.NaN, 216.4f, 133.6f,
+ 180.3f, 176.1f, 180.3f, 246.8f, 180.3f, 345.9f, 25.4f, 345.9f, 25.4f, 254.7f,
+ float.NaN, float.NaN, float.NaN, float.NaN, 25.4f, 91.2f, 127.7f, -14.1f, 288.6f, -14.1f,
+ float.NaN, float.NaN, float.NaN, float.NaN, 455.6f, -14.1f, 551.9f, 84.9f, 551.9f, 294,
+ 551.9f, 1065.9f, 396.9f, 1065.9f,
+ ],
+ ],
+
+ ['K'] =
+ [
+ [
+ 37.2f, 631.1f, 37.2f, 1067.1f, -117.1f, 1067.1f, -117.1f, 0, 37.2f, 0,
+ 37.2f, 478.5f, 467.1f, 0, 679.9f, 0, 160, 558.7f, 649.9f, 1067.1f,
+ 449.2f, 1067.1f, 37.2f, 631.1f,
+ ],
+ ],
+
+ ['L'] =
+ [
+ [
+ 85.4f, 147.3f, 85.4f, 1062.3f, -65.6f, 1062.3f, -65.6f, 0, 593.8f, 0,
+ 593.8f, 147.3f, 85.4f, 147.3f,
+ ],
+ ],
+
+ ['M'] =
+ [
+ [
+ 283.6f, 648.1f, 280.4f, 648.1f, 157.9f, 1053.8f, -88.8f, 1053.8f, -88.8f, 0,
+ 71, 0, 71, 878.2f, 74.1f, 878.2f, 218.4f, 341.9f, 345.6f, 341.9f,
+ 489.9f, 878.2f, 493, 878.2f, 493, 0, 652.8f, 0, 652.8f, 1053.8f,
+ 406.1f, 1053.8f, 283.6f, 648.1f,
+ ],
+ ],
+
+ ['N'] =
+ [
+ [
+ 475.8f, 290.4f, 472.7f, 290.4f, 114.1f, 1058.4f, -69.9f, 1058.4f, -69.9f, 0,
+ 74.2f, 0, 74.2f, 800.8f, 77.3f, 800.8f, 448.2f, 0, 619.9f, 0,
+ 619.9f, 1058.4f, 475.8f, 1058.4f, 475.8f, 290.4f,
+ ],
+ ],
+
+ ['O'] =
+ [
+ [
+ 279, 1101, float.NaN, float.NaN, float.NaN, float.NaN, 15.7f, 1101, -88.4f, 764.4f,
+ -88.4f, 544.9f, float.NaN, float.NaN, float.NaN, float.NaN, -88.4f, 306.1f, 14.2f, -14.4f,
+ 279, -14.4f, float.NaN, float.NaN, float.NaN, float.NaN, 543.8f, -14.4f, 646.4f, 306.1f,
+ 646.4f, 544.9f, float.NaN, float.NaN, float.NaN, float.NaN, 646.4f, 764.4f, 542.3f, 1101,
+ 279, 1101,
+ ],
+ [
+ 279, 964.9f, float.NaN, float.NaN, float.NaN, float.NaN, 461.2f, 964.9f, 488.7f, 682.6f,
+ 488.7f, 539, float.NaN, float.NaN, float.NaN, float.NaN, 488.7f, 398.7f, 458.1f, 126.3f,
+ 279, 126.3f, float.NaN, float.NaN, float.NaN, float.NaN, 98.4f, 126.3f, 69.3f, 398.7f,
+ 69.3f, 539, float.NaN, float.NaN, float.NaN, float.NaN, 69.3f, 682.6f, 95.3f, 964.9f,
+ 279, 964.9f,
+ ],
+ ],
+
+ ['P'] =
+ [
+ [
+ -91.4f, 0, 67.4f, 0, 67.4f, 438.8f, 250.9f, 438.8f, float.NaN, float.NaN,
+ float.NaN, float.NaN, 445.1f, 438.8f, 616.3f, 525, 616.3f, 746, float.NaN, float.NaN,
+ float.NaN, float.NaN, 616.3f, 967, 469.8f, 1062.6f, 261.7f, 1062.6f, -91.4f, 1062.6f,
+ -91.4f, 0,
+ ],
+ [
+ 74.4f, 579.1f, 74.4f, 908.3f, 262.5f, 908.3f, float.NaN, float.NaN, float.NaN, float.NaN,
+ 381.2f, 908.3f, 464.5f, 881.6f, 464.5f, 742.1f, float.NaN, float.NaN, float.NaN, float.NaN,
+ 464.5f, 613.6f, 370.4f, 579.1f, 261, 579.1f, 74.4f, 579.1f,
+ ],
+ ],
+
+ ['Q'] =
+ [
+ [
+ 501.5f, 0, 690.6f, 0, 510.6f, 238.3f, float.NaN, float.NaN, float.NaN, float.NaN,
+ 570.1f, 331.4f, 579.3f, 452.9f, 579.3f, 561.7f, float.NaN, float.NaN, float.NaN, float.NaN,
+ 579.3f, 789, 504.5f, 1084, 229.9f, 1084, float.NaN, float.NaN, float.NaN, float.NaN,
+ -43.1f, 1084, -119.4f, 789, -119.4f, 561.7f, float.NaN, float.NaN, float.NaN, float.NaN,
+ -119.4f, 329.8f, -34, 55.2f, 229.9f, 55.2f, float.NaN, float.NaN, float.NaN, float.NaN,
+ 297, 55.2f, 364.2f, 78.9f, 411.5f, 119.9f, 501.5f, 0,
+ ],
+ [
+ 324.8f, 224.3f, float.NaN, float.NaN, float.NaN, float.NaN, 299.6f, 197.4f, 261.9f, 189.6f,
+ 228.9f, 189.6f, float.NaN, float.NaN, float.NaN, float.NaN, 56.1f, 189.6f, 30.9f, 437.3f,
+ 30.9f, 565.1f, float.NaN, float.NaN, float.NaN, float.NaN, 30.9f, 696.1f, 54.5f, 921.7f,
+ 228.9f, 921.7f, float.NaN, float.NaN, float.NaN, float.NaN, 403.3f, 921.7f, 426.9f, 696.1f,
+ 426.9f, 565.1f, float.NaN, float.NaN, float.NaN, float.NaN, 426.9f, 487.8f, 426.9f, 426.2f,
+ 409.6f, 369.4f, 299.6f, 508.3f, 107.9f, 508.3f, 324.8f, 224.3f,
+ ],
+ ],
+
+ ['R'] =
+ [
+ [
+ 71.7f, 445.5f, 200.9f, 445.5f, 445.2f, 0, 631.2f, 0, 352.2f, 478.8f,
+ float.NaN, float.NaN, float.NaN, float.NaN, 490.9f, 504.1f, 565, 626.2f, 565, 761,
+ float.NaN, float.NaN, float.NaN, float.NaN, 565, 973.4f, 415.3f, 1074.9f, 219.8f, 1074.9f,
+ -90.6f, 1074.9f, -90.6f, 0, 71.7f, 0, 71.7f, 445.5f,
+ ],
+ [
+ 83.3f, 594.5f, 83.3f, 925.8f, 198, 925.8f, float.NaN, float.NaN, float.NaN, float.NaN,
+ 309.8f, 925.8f, 404.6f, 908.4f, 404.6f, 767.3f, float.NaN, float.NaN, float.NaN, float.NaN,
+ 404.6f, 626.2f, 326.6f, 594.5f, 199.6f, 594.5f, 83.3f, 594.5f,
+ ],
+ ],
+
+ ['S'] =
+ [
+ [
+ 269.5f, -14.5f, float.NaN, float.NaN, float.NaN, float.NaN, 438.6f, -14.5f, 594.1f, 98.1f,
+ 594.1f, 289.5f, float.NaN, float.NaN, float.NaN, float.NaN, 594.1f, 474.5f, 474.8f, 551.7f,
+ 335.9f, 622.5f, 245.3f, 667.5f, float.NaN, float.NaN, float.NaN, float.NaN, 186.4f, 696.5f,
+ 127.6f, 738.3f, 127.6f, 817.1f, float.NaN, float.NaN, float.NaN, float.NaN, 127.6f, 920.1f,
+ 203.1f, 953.8f, 284.6f, 953.8f, float.NaN, float.NaN, float.NaN, float.NaN, 366.1f, 953.8f,
+ 434.1f, 907.2f, 428, 815.5f, 583.5f, 815.5f, 583.5f, 836.4f, float.NaN, float.NaN,
+ float.NaN, float.NaN, 583.5f, 1000.5f, 428, 1105, 287.6f, 1105, float.NaN, float.NaN,
+ float.NaN, float.NaN, 118.5f, 1105, -28, 990.8f, -28, 802.6f, float.NaN, float.NaN,
+ float.NaN, float.NaN, -28, 640.2f, 89.8f, 571, 206.1f, 511.5f, float.NaN, float.NaN,
+ float.NaN, float.NaN, 322.3f, 453.6f, 438.6f, 406.9f, 438.6f, 287.9f, float.NaN, float.NaN,
+ float.NaN, float.NaN, 438.6f, 181.8f, 357.1f, 136.7f, 269.5f, 136.7f, float.NaN, float.NaN,
+ float.NaN, float.NaN, 172.9f, 136.7f, 104.9f, 199.5f, 103.4f, 302.4f, -52.1f, 302.4f,
+ -52.1f, 268.6f, float.NaN, float.NaN, float.NaN, float.NaN, -52.1f, 88.5f, 115.5f, -14.5f,
+ 269.5f, -14.5f,
+ ],
+ ],
+
+ ['T'] =
+ [
+ [
+ 351.2f, 941.1f, 622, 941.1f, 622, 1092.6f, -72, 1092.6f, -72, 941.1f,
+ 198.8f, 941.1f, 198.8f, 0, 351.2f, 0, 351.2f, 941.1f,
+ ],
+ ],
+
+ ['U'] =
+ [
+ [
+ -92.2f, 1087.3f, -92.2f, 364, float.NaN, float.NaN, float.NaN, float.NaN, -92.2f, 128.3f,
+ 17.9f, -14.4f, 268, -14.4f, float.NaN, float.NaN, float.NaN, float.NaN, 516.5f, -14.4f,
+ 628.2f, 128.3f, 628.2f, 364, 628.2f, 1087.3f, 466.2f, 1087.3f, 466.2f, 375.3f,
+ float.NaN, float.NaN, float.NaN, float.NaN, 466.2f, 222.9f, 430, 136.3f, 268, 136.3f,
+ float.NaN, float.NaN, float.NaN, float.NaN, 106, 136.3f, 69.8f, 222.9f, 69.8f, 375.3f,
+ 69.8f, 1087.3f, -92.2f, 1087.3f,
+ ],
+ ],
+
+ ['V'] =
+ [
+ [
+ 277.2f, 159.2f, 274.3f, 159.2f, 52.4f, 1058.4f, -98, 1058.4f, 169.2f, 0,
+ 377.9f, 0, 648.1f, 1058.4f, 497.7f, 1058.4f, 277.2f, 159.2f,
+ ],
+ ],
+
+ ['W'] =
+ [
+ [
+ 219.4f, 660.5f, 112.6f, 279.7f, 109.5f, 279.7f, float.NaN, float.NaN, float.NaN, float.NaN,
+ 80.1f, 518.3f, 60, 774.3f, 60, 982.9f, 60, 1071.4f, -99.4f, 1071.4f,
+ -99.4f, 982.9f, float.NaN, float.NaN, float.NaN, float.NaN, -99.4f, 875.4f, -87, 595.7f,
+ -65.4f, 469.3f, 13.6f, 0, 186.9f, 0, 278.2f, 372.9f, 281.3f, 372.9f,
+ 371.1f, 0, 544.4f, 0, 623.3f, 469.3f, float.NaN, float.NaN, float.NaN, float.NaN,
+ 645, 595.7f, 657.4f, 875.4f, 657.4f, 982.9f, 657.4f, 1071.4f, 498, 1071.4f,
+ 498, 982.9f, float.NaN, float.NaN, float.NaN, float.NaN, 498, 774.3f, 477.8f, 518.3f,
+ 448.4f, 279.7f, 445.3f, 279.7f, 338.6f, 660.5f, 219.4f, 660.5f,
+ ],
+ ],
+
+ ['X'] =
+ [
+ [
+ 265, 691.6f, 75.3f, 1058.4f, -106.8f, 1058.4f, 181.5f, 544.8f, -106.8f, 0,
+ 75.3f, 0, 265, 401.2f, 453.2f, 0, 636.8f, 0, 360.6f, 544.8f,
+ 636.8f, 1058.4f, 453.2f, 1058.4f, 265, 691.6f,
+ ],
+ ],
+
+ ['Y'] =
+ [
+ [
+ 275, 668.4f, 86.4f, 1071.4f, -76.4f, 1071.4f, 201.6f, 500.9f, 201.6f, 0,
+ 349.9f, 0, 349.9f, 500.9f, 626.4f, 1071.4f, 463.6f, 1071.4f, 275, 668.4f,
+ ],
+ ],
+
+ ['Z'] =
+ [
+ [
+ 146.3f, 147.3f, 533.2f, 938.5f, 533.2f, 1062.3f, -19.3f, 1062.3f, -19.3f, 915,
+ 364.6f, 915, -32.9f, 111.2f, -32.9f, 0, 582.8f, 0, 582.8f, 147.3f,
+ 146.3f, 147.3f,
+ ],
+ ],
+
+ ['['] =
+ [
+ [
+ 147.2f, 1064.3f, 543.4f, 1064.3f, 543.4f, 1219, -7.4f, 1219, -7.4f, 0,
+ 543.4f, 0, 543.4f, 154.6f, 147.2f, 154.6f, 147.2f, 1064.3f,
+ ],
+ ],
+
+ ['\\'] =
+ [
+ [
+ -49.6f, 1197.4f, 426.7f, 0, 587.6f, 0, 114.5f, 1197.4f, -49.6f, 1197.4f,
+ ],
+ ],
+
+ [']'] =
+ [
+ [
+ 402.1f, 154.6f, 18.2f, 154.6f, 18.2f, 0, 551.8f, 0, 551.8f, 1219,
+ 18.2f, 1219, 18.2f, 1064.3f, 402.1f, 1064.3f, 402.1f, 154.6f,
+ ],
+ ],
+
+ ['^'] =
+ [
+ [
+ 308.6f, 1180.1f, 239.8f, 1180.1f, -86.2f, 651.4f, 6.5f, 573.3f, 274.2f, 877.5f,
+ 541.9f, 573.3f, 636.2f, 651.4f, 308.6f, 1180.1f,
+ ],
+ ],
+
+ ['_'] =
+ [
+ [
+ -77.7f, -311, 607.7f, -311, 607.7f, -125.8f, -77.7f, -125.8f, -77.7f, -311,
+ ],
+ ],
+
+ ['`'] =
+ [
+ [
+ 296, 1160.8f, 113.4f, 1160.8f, 288.9f, 904.1f, 425.9f, 904.1f, 296, 1160.8f,
+ ],
+ ],
+
+ ['a'] =
+ [
+ [
+ 438.8f, 363.6f, 438.8f, 337.6f, float.NaN, float.NaN, float.NaN, float.NaN, 438.8f, 211.7f,
+ 372.3f, 105.9f, 234.7f, 105.9f, float.NaN, float.NaN, float.NaN, float.NaN, 159.1f, 105.9f,
+ 103.1f, 150.4f, 103.1f, 230.1f, float.NaN, float.NaN, float.NaN, float.NaN, 103.1f, 331.4f,
+ 192.3f, 363.6f, 277, 363.6f, 438.8f, 363.6f,
+ ],
+ [
+ 434.8f, 0, 586, 0, 586, 493.5f, float.NaN, float.NaN, float.NaN, float.NaN,
+ 586, 726, 503.8f, 806.7f, 291, 806.7f, float.NaN, float.NaN, float.NaN, float.NaN,
+ 155.9f, 806.7f, 0.3f, 768.7f, -9.9f, 593.2f, 141.2f, 593.2f, float.NaN, float.NaN,
+ float.NaN, float.NaN, 141.2f, 670.7f, 220.5f, 683.3f, 298.3f, 683.3f, float.NaN, float.NaN,
+ float.NaN, float.NaN, 423.1f, 683.3f, 434.8f, 616.9f, 434.8f, 526.7f, 434.8f, 498.3f,
+ 298.3f, 498.3f, float.NaN, float.NaN, float.NaN, float.NaN, 136.8f, 498.3f, -42.2f, 436.6f,
+ -42.2f, 230.9f, float.NaN, float.NaN, float.NaN, float.NaN, -42.2f, 69.6f, 79.6f, -14.2f,
+ 222, -14.2f, float.NaN, float.NaN, float.NaN, float.NaN, 299.8f, -14.2f, 377.6f, 17.4f,
+ 434.8f, 75.9f, 434.8f, 0,
+ ],
+ ],
+
+ ['b'] =
+ [
+ [
+ 282, 678.7f, float.NaN, float.NaN, float.NaN, float.NaN, 440.5f, 678.7f, 489.6f, 518.6f,
+ 489.6f, 386.3f, float.NaN, float.NaN, float.NaN, float.NaN, 489.6f, 253.9f, 425.8f, 111.8f,
+ 273.8f, 111.8f, float.NaN, float.NaN, float.NaN, float.NaN, 113.6f, 111.8f, 54.7f, 257.2f,
+ 54.7f, 394.4f, float.NaN, float.NaN, float.NaN, float.NaN, 54.7f, 539.8f, 116.9f, 678.7f,
+ 282, 678.7f,
+ ],
+ [
+ -116.9f, 0, 46.6f, 0, 46.6f, 80.9f, float.NaN, float.NaN, float.NaN, float.NaN,
+ 105.3f, 17.4f, 191, -14.3f, 276.7f, -14.3f, float.NaN, float.NaN, float.NaN, float.NaN,
+ 503.7f, -14.3f, 632.2f, 171.3f, 632.2f, 383.8f, float.NaN, float.NaN, float.NaN, float.NaN,
+ 632.2f, 598, 532.2f, 808.9f, 287.8f, 808.9f, float.NaN, float.NaN, float.NaN, float.NaN,
+ 202.1f, 808.9f, 105.3f, 778.8f, 51.4f, 707.4f, 46.6f, 707.4f, 46.6f, 1154.7f,
+ -116.9f, 1154.7f, -116.9f, 0,
+ ],
+ ],
+
+ ['c'] =
+ [
+ [
+ 571.6f, 590.5f, float.NaN, float.NaN, float.NaN, float.NaN, 549.8f, 748.6f, 434.9f, 822.8f,
+ 299.7f, 822.8f, float.NaN, float.NaN, float.NaN, float.NaN, 87.5f, 822.8f, -21.6f, 624.4f,
+ -21.6f, 408.2f, float.NaN, float.NaN, float.NaN, float.NaN, -21.6f, 190.4f, 71.5f, -14.5f,
+ 286.6f, -14.5f, float.NaN, float.NaN, float.NaN, float.NaN, 421.8f, -14.5f, 571.6f, 75.8f,
+ 571.6f, 246.9f, 421.8f, 246.9f, float.NaN, float.NaN, float.NaN, float.NaN, 421.8f, 177.5f,
+ 360.8f, 124.2f, 296.8f, 124.2f, float.NaN, float.NaN, float.NaN, float.NaN, 155.8f, 124.2f,
+ 128.2f, 282.3f, 128.2f, 409.8f, float.NaN, float.NaN, float.NaN, float.NaN, 128.2f, 542.1f,
+ 158.7f, 684.1f, 304.1f, 684.1f, float.NaN, float.NaN, float.NaN, float.NaN, 359.3f, 684.1f,
+ 418.9f, 648.6f, 421.8f, 590.5f, 571.6f, 590.5f,
+ ],
+ ],
+
+ ['d'] =
+ [
+ [
+ 270.6f, 110.7f, float.NaN, float.NaN, float.NaN, float.NaN, 109.8f, 110.7f, 56.1f, 266.2f,
+ 56.1f, 402.3f, float.NaN, float.NaN, float.NaN, float.NaN, 56.1f, 528.7f, 109.8f, 672.8f,
+ 259.2f, 672.8f, float.NaN, float.NaN, float.NaN, float.NaN, 421.7f, 672.8f, 483.5f, 546.5f,
+ 483.5f, 402.3f, float.NaN, float.NaN, float.NaN, float.NaN, 483.5f, 266.2f, 428.2f, 110.7f,
+ 270.6f, 110.7f,
+ ],
+ [
+ 498.7f, 0, 661.2f, 0, 661.2f, 1144.9f, 498.7f, 1144.9f, 498.7f, 704.6f,
+ 495.6f, 704.6f, float.NaN, float.NaN, float.NaN, float.NaN, 443.5f, 773.8f, 348.9f, 802.1f,
+ 262.1f, 802.1f, float.NaN, float.NaN, float.NaN, float.NaN, 25.5f, 802.1f, -78.6f, 603.9f,
+ -78.6f, 391.6f, float.NaN, float.NaN, float.NaN, float.NaN, -78.6f, 176.1f, 36.5f, -14.2f,
+ 273.2f, -14.2f, float.NaN, float.NaN, float.NaN, float.NaN, 353.6f, -14.2f, 443.5f, 26.7f,
+ 498.7f, 81.8f, 498.7f, 0,
+ ],
+ ],
+
+ ['e'] =
+ [
+ [
+ 602.4f, 368.1f, 602.4f, 417.8f, float.NaN, float.NaN, float.NaN, float.NaN, 602.4f, 651.4f,
+ 512.3f, 816.3f, 280.6f, 816.3f, float.NaN, float.NaN, float.NaN, float.NaN, 47.4f, 816.3f,
+ -60.3f, 633.8f, -60.3f, 395.3f, float.NaN, float.NaN, float.NaN, float.NaN, -60.3f, 145.7f,
+ 65.1f, -14.4f, 299.8f, -14.4f, float.NaN, float.NaN, float.NaN, float.NaN, 414.9f, -14.4f,
+ 566.9f, 27.2f, 574.3f, 179.3f, 423.8f, 179.3f, float.NaN, float.NaN, float.NaN, float.NaN,
+ 407.5f, 128, 351.5f, 123.2f, 307.2f, 123.2f, float.NaN, float.NaN, float.NaN, float.NaN,
+ 171.4f, 123.2f, 84.3f, 222.5f, 91.7f, 368.1f, 602.4f, 368.1f,
+ ],
+ [
+ 100.3f, 507, float.NaN, float.NaN, float.NaN, float.NaN, 106.4f, 617.4f, 196.1f, 685,
+ 291.8f, 685, float.NaN, float.NaN, float.NaN, float.NaN, 392.2f, 685, 468.2f, 617.4f,
+ 469.7f, 507, 100.3f, 507,
+ ],
+ ],
+
+ ['f'] =
+ [
+ [
+ 347.1f, 789.6f, 347.1f, 827.4f, float.NaN, float.NaN, float.NaN, float.NaN, 347.1f, 981.9f,
+ 375.6f, 1011.8f, 515.3f, 1011.8f, 548, 1011.8f, 548, 1147.4f, 472.5f, 1147.4f,
+ float.NaN, float.NaN, float.NaN, float.NaN, 270.2f, 1147.4f, 200.4f, 1027.6f, 200.4f, 827.4f,
+ 200.4f, 789.6f, -3.3f, 789.6f, -3.3f, 654.1f, 200.4f, 654.1f, 200.4f, 0,
+ 347.1f, 0, 347.1f, 654.1f, 548, 654.1f, 548, 789.6f, 347.1f, 789.6f,
+ ],
+ ],
+
+ ['g'] =
+ [
+ [
+ 273.5f, 160.8f, float.NaN, float.NaN, float.NaN, float.NaN, 116.8f, 160.8f, 76.5f, 286.3f,
+ 76.5f, 418, float.NaN, float.NaN, float.NaN, float.NaN, 76.5f, 541.9f, 115.2f, 672.1f,
+ 265.4f, 672.1f, float.NaN, float.NaN, float.NaN, float.NaN, 418.8f, 672.1f, 465.7f, 552.9f,
+ 465.7f, 422.7f, float.NaN, float.NaN, float.NaN, float.NaN, 465.7f, 298.8f, 422, 160.8f,
+ 273.5f, 160.8f,
+ ],
+ [
+ 636, 785.7f, 474.5f, 785.7f, 474.5f, 715.1f, 471.3f, 715.1f, 472.9f, 715.1f,
+ float.NaN, float.NaN, float.NaN, float.NaN, 425.9f, 777.9f, 333.4f, 799.8f, 255, 799.8f,
+ float.NaN, float.NaN, float.NaN, float.NaN, 40.2f, 799.8f, -64.9f, 635.2f, -64.9f, 434.4f,
+ float.NaN, float.NaN, float.NaN, float.NaN, -64.9f, 225.8f, 30.8f, 47, 258.1f, 47,
+ float.NaN, float.NaN, float.NaN, float.NaN, 344.3f, 47, 400.8f, 72.1f, 468.2f, 125.5f,
+ 474.5f, 130.2f, 474.5f, 14.1f, float.NaN, float.NaN, float.NaN, float.NaN, 474.5f, -108.2f,
+ 444.7f, -150.6f, 262.8f, -150.6f, float.NaN, float.NaN, float.NaN, float.NaN, 209.5f, -150.6f,
+ 124.8f, -142.7f, 129.5f, -72.1f, -31.9f, -72.1f, -31.9f, -89.4f, float.NaN, float.NaN,
+ float.NaN, float.NaN, -31.9f, -254.1f, 140.5f, -271.3f, 264.4f, -271.3f, float.NaN, float.NaN,
+ float.NaN, float.NaN, 472.9f, -271.3f, 636, -214.9f, 636, 26.7f, 636, 785.7f,
+ ],
+ ],
+
+ ['h'] =
+ [
+ [
+ 110.8f, 1154.3f, -50.3f, 1154.3f, -50.3f, 0, 110.8f, 0, 110.8f, 424.9f,
+ float.NaN, float.NaN, float.NaN, float.NaN, 110.8f, 558.1f, 131.1f, 659.6f, 290.6f, 659.6f,
+ float.NaN, float.NaN, float.NaN, float.NaN, 404.8f, 659.6f, 425.2f, 591.4f, 425.2f, 491.5f,
+ 425.2f, 0, 586.3f, 0, 586.3f, 478.9f, float.NaN, float.NaN, float.NaN, float.NaN,
+ 586.3f, 661.2f, 515.9f, 808.7f, 314.1f, 808.7f, float.NaN, float.NaN, float.NaN, float.NaN,
+ 240.6f, 808.7f, 160.8f, 781.7f, 110.8f, 729.4f, 110.8f, 1154.3f,
+ ],
+ ],
+
+ ['i'] =
+ [
+ [
+ 89.1f, 789.6f, 89.1f, 641.5f, 332.9f, 641.5f, 332.9f, 0, 489, 0,
+ 489, 789.6f, 89.1f, 789.6f,
+ ],
+ [
+ 264.4f, 1175.9f, 264.4f, 969.5f, 466.9f, 969.5f, 466.9f, 1175.9f, 264.4f, 1175.9f,
+ ],
+ ],
+
+ ['j'] =
+ [
+ [
+ 172.1f, 781.3f, 172.1f, 634.7f, 344.3f, 634.7f, 344.3f, 51.5f, float.NaN, float.NaN,
+ float.NaN, float.NaN, 344.3f, -4.7f, 344.3f, -57.7f, 313.9f, -90.4f, float.NaN, float.NaN,
+ float.NaN, float.NaN, 277.7f, -129.4f, 234.3f, -123.2f, 179.3f, -123.2f, 76.6f, -123.2f,
+ 76.6f, -269.8f, 185.1f, -269.8f, float.NaN, float.NaN, float.NaN, float.NaN, 412.2f, -269.8f,
+ 493.3f, -185.6f, 493.3f, 43.7f, 493.3f, 781.3f, 172.1f, 781.3f,
+ ],
+ [
+ 289.9f, 1149.3f, 289.9f, 951.2f, 483.4f, 951.2f, 483.4f, 1149.3f, 289.9f, 1149.3f,
+ ],
+ ],
+
+ ['k'] =
+ [
+ [
+ 71.5f, 1136.5f, -87.4f, 1136.5f, -87.4f, 0, 71.5f, 0, 71.5f, 365.3f,
+ 403.3f, 0, 610.1f, 0, 207.3f, 443.4f, 576.2f, 782.1f, 366.3f, 782.1f,
+ 71.5f, 502.7f, 71.5f, 1136.5f,
+ ],
+ ],
+
+ ['l'] =
+ [
+ [
+ 202.4f, 1182.9f, 52.8f, 1182.9f, 52.8f, 355.8f, float.NaN, float.NaN, float.NaN, float.NaN,
+ 52.8f, 126.7f, 110.9f, 0, 328.8f, 0, 463.9f, 0, 463.9f, 139.7f,
+ 366.6f, 139.7f, float.NaN, float.NaN, float.NaN, float.NaN, 247.5f, 139.7f, 202.4f, 178.7f,
+ 202.4f, 313.6f, 202.4f, 1182.9f,
+ ],
+ ],
+
+ ['m'] =
+ [
+ [
+ 56.7f, 781.7f, -90.7f, 781.7f, -90.7f, 0, 56.7f, 0, 56.7f, 510.2f,
+ float.NaN, float.NaN, float.NaN, float.NaN, 56.7f, 575.8f, 58.1f, 675.6f, 141.2f, 675.6f,
+ float.NaN, float.NaN, float.NaN, float.NaN, 194.1f, 675.6f, 201.3f, 619.5f, 201.3f, 575.8f,
+ 201.3f, 0, 348.7f, 0, 348.7f, 510.2f, float.NaN, float.NaN, float.NaN, float.NaN,
+ 348.7f, 575.8f, 345.8f, 675.6f, 431.7f, 675.6f, float.NaN, float.NaN, float.NaN, float.NaN,
+ 490.4f, 675.6f, 493.3f, 624.1f, 493.3f, 575.8f, 493.3f, 0, 640.7f, 0,
+ 640.7f, 575.8f, float.NaN, float.NaN, float.NaN, float.NaN, 640.7f, 691.2f, 583.4f, 795.8f,
+ 467.5f, 795.8f, float.NaN, float.NaN, float.NaN, float.NaN, 411.7f, 795.8f, 363, 777.1f,
+ 322.9f, 734.9f, float.NaN, float.NaN, float.NaN, float.NaN, 292.9f, 778.6f, 248.5f, 795.8f,
+ 198.4f, 795.8f, float.NaN, float.NaN, float.NaN, float.NaN, 169.8f, 795.8f, 144, 789.5f,
+ 119.7f, 777.1f, float.NaN, float.NaN, float.NaN, float.NaN, 96.8f, 764.6f, 75.3f, 747.4f,
+ 56.7f, 724, 56.7f, 781.7f,
+ ],
+ ],
+
+ ['n'] =
+ [
+ [
+ 78.6f, 778.7f, -82.6f, 778.7f, -82.6f, 0, 78.6f, 0, 78.6f, 405.7f,
+ float.NaN, float.NaN, float.NaN, float.NaN, 78.6f, 551.8f, 144.4f, 659.1f, 302.4f, 659.1f,
+ float.NaN, float.NaN, float.NaN, float.NaN, 410.4f, 659.1f, 457.4f, 603.1f, 457.4f, 500.5f,
+ 457.4f, 0, 618.6f, 0, 618.6f, 489.6f, float.NaN, float.NaN, float.NaN, float.NaN,
+ 618.6f, 669.9f, 505.9f, 792.7f, 319.6f, 792.7f, float.NaN, float.NaN, float.NaN, float.NaN,
+ 233.6f, 792.7f, 131.8f, 757, 81.8f, 691.7f, 78.6f, 691.7f, 78.6f, 778.7f,
+ ],
+ ],
+
+ ['o'] =
+ [
+ [
+ 275, 800.3f, float.NaN, float.NaN, float.NaN, float.NaN, 45.2f, 800.3f, -92.4f, 635.5f,
+ -92.4f, 397, float.NaN, float.NaN, float.NaN, float.NaN, -92.4f, 164.8f, 42.2f, -14.1f,
+ 275, -14.1f, float.NaN, float.NaN, float.NaN, float.NaN, 507.8f, -14.1f, 642.4f, 164.8f,
+ 642.4f, 397, float.NaN, float.NaN, float.NaN, float.NaN, 642.4f, 635.5f, 503.3f, 800.3f,
+ 275, 800.3f,
+ ],
+ [
+ 268, 679.5f, float.NaN, float.NaN, float.NaN, float.NaN, 428.4f, 679.5f, 486, 541.4f,
+ 486, 395.4f, float.NaN, float.NaN, float.NaN, float.NaN, 486, 249.5f, 436.2f, 106.7f,
+ 268, 106.7f, float.NaN, float.NaN, float.NaN, float.NaN, 99.8f, 106.7f, 50, 249.5f,
+ 50, 395.4f, float.NaN, float.NaN, float.NaN, float.NaN, 50, 541.4f, 107.6f, 679.5f,
+ 268, 679.5f,
+ ],
+ ],
+
+ ['p'] =
+ [
+ [
+ 275.2f, 664.6f, float.NaN, float.NaN, float.NaN, float.NaN, 444, 664.6f, 483.8f, 519.2f,
+ 483.8f, 373.8f, float.NaN, float.NaN, float.NaN, float.NaN, 483.8f, 233.3f, 432.9f, 97.8f,
+ 275.2f, 97.8f, float.NaN, float.NaN, float.NaN, float.NaN, 114.4f, 97.8f, 61.9f, 238.2f,
+ 61.9f, 382, float.NaN, float.NaN, float.NaN, float.NaN, 61.9f, 524.1f, 114.4f, 664.6f,
+ 275.2f, 664.6f,
+ ],
+ [
+ 47.9f, 770.7f, -116.1f, 770.7f, -116.1f, -266.1f, 47.9f, -266.1f, 47.9f, 72.3f,
+ float.NaN, float.NaN, float.NaN, float.NaN, 102, 15.4f, 203.9f, -13.8f, 278.7f, -13.8f,
+ float.NaN, float.NaN, float.NaN, float.NaN, 511.2f, -13.8f, 633.8f, 175.4f, 633.8f, 384.6f,
+ float.NaN, float.NaN, float.NaN, float.NaN, 633.8f, 595.3f, 509.6f, 784.5f, 272.4f, 784.5f,
+ float.NaN, float.NaN, float.NaN, float.NaN, 180, 784.5f, 127.5f, 756.8f, 59, 704.5f,
+ 47.9f, 698.4f, 47.9f, 770.7f,
+ ],
+ ],
+
+ ['q'] =
+ [
+ [
+ 277.7f, 667.6f, float.NaN, float.NaN, float.NaN, float.NaN, 430.1f, 667.6f, 486.9f, 529.3f,
+ 486.9f, 397.5f, float.NaN, float.NaN, float.NaN, float.NaN, 486.9f, 248, 438.2f, 109.8f,
+ 266.4f, 109.8f, float.NaN, float.NaN, float.NaN, float.NaN, 94.5f, 109.8f, 58.8f, 252.8f,
+ 58.8f, 394.3f, float.NaN, float.NaN, float.NaN, float.NaN, 58.8f, 535.8f, 114, 667.6f,
+ 277.7f, 667.6f,
+ ],
+ [
+ 667.9f, 781.9f, 500.9f, 781.9f, 500.9f, 703.8f, float.NaN, float.NaN, float.NaN, float.NaN,
+ 437.6f, 763.2f, 355, 795.9f, 265.8f, 795.9f, float.NaN, float.NaN, float.NaN, float.NaN,
+ 21, 795.9f, -94.2f, 593, -94.2f, 377.7f, float.NaN, float.NaN, float.NaN, float.NaN,
+ -94.2f, 163.9f, 35.5f, -14, 267.4f, -14, float.NaN, float.NaN, float.NaN, float.NaN,
+ 355, -14, 442.5f, 15.6f, 500.9f, 78, 500.9f, -270, 667.9f, -270,
+ 667.9f, 781.9f,
+ ],
+ ],
+
+ ['r'] =
+ [
+ [
+ 119.9f, 791.4f, -39.8f, 791.4f, -39.8f, 0, 119.9f, 0, 119.9f, 409.1f,
+ float.NaN, float.NaN, float.NaN, float.NaN, 119.9f, 529.2f, 150.9f, 683.9f, 302.8f, 683.9f,
+ float.NaN, float.NaN, float.NaN, float.NaN, 371.1f, 683.9f, 400.5f, 619.2f, 400.5f, 557.6f,
+ 400.5f, 533.9f, 560.2f, 533.9f, 560.2f, 565.5f, float.NaN, float.NaN, float.NaN, float.NaN,
+ 560.2f, 706.1f, 461, 805.6f, 324.5f, 805.6f, float.NaN, float.NaN, float.NaN, float.NaN,
+ 242.4f, 805.6f, 171.1f, 769.2f, 123, 704.5f, 119.9f, 704.5f, 119.9f, 791.4f,
+ ],
+ ],
+
+ ['s'] =
+ [
+ [
+ 544.8f, 597, 544.8f, 620.8f, float.NaN, float.NaN, float.NaN, float.NaN, 544.8f, 747.9f,
+ 405.2f, 809.8f, 271.4f, 809.8f, float.NaN, float.NaN, float.NaN, float.NaN, 156.5f, 809.8f,
+ -16.6f, 768.5f, -16.6f, 582.7f, float.NaN, float.NaN, float.NaN, float.NaN, -16.6f, 500.2f,
+ 18.3f, 425.5f, 91, 392.2f, float.NaN, float.NaN, float.NaN, float.NaN, 150.6f, 362,
+ 243.7f, 338.2f, 307.7f, 325.5f, float.NaN, float.NaN, float.NaN, float.NaN, 367.4f, 314.4f,
+ 427, 301.7f, 427, 220.7f, float.NaN, float.NaN, float.NaN, float.NaN, 427, 128.6f,
+ 310.6f, 109.6f, 248.1f, 109.6f, float.NaN, float.NaN, float.NaN, float.NaN, 200.1f, 109.6f,
+ 109.9f, 117.5f, 107, 211.2f, -42.8f, 211.2f, float.NaN, float.NaN, float.NaN, float.NaN,
+ -42.8f, 34.9f, 109.9f, -14.3f, 245.2f, -14.3f, float.NaN, float.NaN, float.NaN, float.NaN,
+ 386.3f, -14.3f, 576.8f, 28.6f, 576.8f, 220.7f, float.NaN, float.NaN, float.NaN, float.NaN,
+ 576.8f, 435.1f, 376.1f, 457.3f, 229.2f, 489, float.NaN, float.NaN, float.NaN, float.NaN,
+ 173.9f, 500.2f, 133.2f, 522.4f, 133.2f, 590.7f, float.NaN, float.NaN, float.NaN, float.NaN,
+ 133.2f, 671.6f, 217.5f, 685.9f, 271.4f, 685.9f, float.NaN, float.NaN, float.NaN, float.NaN,
+ 316.5f, 685.9f, 392.1f, 668.5f, 395, 597, 544.8f, 597,
+ ],
+ ],
+
+ ['t'] =
+ [
+ [
+ 301.1f, 1020.3f, 151.6f, 978, 151.6f, 799.3f, 2, 799.3f, 2, 664.5f,
+ 151.6f, 664.5f, 151.6f, 286.8f, float.NaN, float.NaN, float.NaN, float.NaN, 151.6f, 111.3f,
+ 219.8f, -14.1f, 395.5f, -14.1f, float.NaN, float.NaN, float.NaN, float.NaN, 450.6f, -14.1f,
+ 514.5f, 3.1f, 561, 32.9f, 561, 181.8f, float.NaN, float.NaN, float.NaN, float.NaN,
+ 520.3f, 148.9f, 449.2f, 120.7f, 398.4f, 120.7f, float.NaN, float.NaN, float.NaN, float.NaN,
+ 312.7f, 120.7f, 301.1f, 197.5f, 301.1f, 271.1f, 301.1f, 664.5f, 561, 664.5f,
+ 561, 799.3f, 301.1f, 799.3f, 301.1f, 1020.3f,
+ ],
+ ],
+
+ ['u'] =
+ [
+ [
+ 475.4f, 0, 640.1f, 0, 640.1f, 781.7f, 475.4f, 781.7f, 475.4f, 385.4f,
+ float.NaN, float.NaN, float.NaN, float.NaN, 475.4f, 227.8f, 414.7f, 120.1f, 237.2f, 120.1f,
+ float.NaN, float.NaN, float.NaN, float.NaN, 123.7f, 120.1f, 88.6f, 163.8f, 88.6f, 269.9f,
+ 88.6f, 781.7f, -76.1f, 781.7f, -76.1f, 269.9f, float.NaN, float.NaN, float.NaN, float.NaN,
+ -76.1f, 99.9f, 27.8f, -14, 208.5f, -14, float.NaN, float.NaN, float.NaN, float.NaN,
+ 301.2f, -14, 413.1f, 15.6f, 475.4f, 87.4f, 475.4f, 0,
+ ],
+ ],
+
+ ['v'] =
+ [
+ [
+ 66.6f, 772.1f, -101.2f, 772.1f, 166.8f, 0, 383.2f, 0, 651.2f, 772.1f,
+ 483.4f, 772.1f, 275.7f, 131, 272.8f, 131, 66.6f, 772.1f,
+ ],
+ ],
+
+ ['w'] =
+ [
+ [
+ 67.6f, 780.8f, -88, 780.8f, 6.9f, 0, 186.1f, 0, 273.5f, 319.5f,
+ 276.5f, 319.5f, 363.9f, 0, 547.6f, 0, 638, 780.8f, 482.4f, 780.8f,
+ 429.1f, 202.6f, 426.1f, 202.6f, 349.1f, 478.4f, 199.4f, 478.4f, 123.9f, 219.7f,
+ 120.9f, 219.7f, 67.6f, 780.8f,
+ ],
+ ],
+
+ ['x'] =
+ [
+ [
+ 122.2f, 780.8f, -77.2f, 780.8f, 186.8f, 392.7f, -77.2f, 0, 92.1f, 0,
+ 267.1f, 285.2f, 442.1f, 0, 627.1f, 0, 353.2f, 397.4f, 599.9f, 780.8f,
+ 429.2f, 780.8f, 277.1f, 519, 122.2f, 780.8f,
+ ],
+ ],
+
+ ['y'] =
+ [
+ [
+ 291, 347.1f, 74.6f, 790.4f, -108.4f, 790.4f, 214, 189.3f, 83.3f, -60,
+ float.NaN, float.NaN, float.NaN, float.NaN, 58.6f, -105.7f, 23.7f, -137.3f, -30, -137.3f,
+ -53.2f, -137.3f, -53.2f, -272.9f, -19.8f, -272.9f, float.NaN, float.NaN, float.NaN, float.NaN,
+ 109.5f, -272.9f, 158.8f, -224, 219.8f, -101, 664.3f, 790.4f, 507.5f, 790.4f,
+ 291, 347.1f,
+ ],
+ ],
+
+ ['z'] =
+ [
+ [
+ 553.7f, 815.6f, 2.2f, 815.6f, 2.2f, 675.6f, 382.4f, 675.6f, -24.8f, 169.3f,
+ -24.8f, 0, 586.8f, 0, 586.8f, 140, 142, 140, 553.7f, 654.4f,
+ 553.7f, 815.6f,
+ ],
+ ],
+
+ ['{'] =
+ [
+ [
+ -58.8f, 505.3f, 15.6f, 505.3f, float.NaN, float.NaN, float.NaN, float.NaN, 111.4f, 505.3f,
+ 152.7f, 488.1f, 152.7f, 389.5f, 152.7f, 286.3f, float.NaN, float.NaN, float.NaN, float.NaN,
+ 152.7f, 92.3f, 280, 0, 458.4f, 0, 608.8f, 0, 608.8f, 147,
+ 509.6f, 147, float.NaN, float.NaN, float.NaN, float.NaN, 400.6f, 147, 322.9f, 153.3f,
+ 322.9f, 286.3f, 322.9f, 389.5f, float.NaN, float.NaN, float.NaN, float.NaN, 322.9f, 450.5f,
+ 304.7f, 535, 248.6f, 561.6f, 208.9f, 578.8f, 248.6f, 597.6f, float.NaN, float.NaN,
+ float.NaN, float.NaN, 304.7f, 624.2f, 322.9f, 708.6f, 322.9f, 768.1f, 322.9f, 872.9f,
+ float.NaN, float.NaN, float.NaN, float.NaN, 322.9f, 1005.9f, 400.6f, 1012.1f, 509.6f, 1012.1f,
+ 608.8f, 1012.1f, 608.8f, 1159.2f, 458.4f, 1159.2f, float.NaN, float.NaN, float.NaN, float.NaN,
+ 280, 1159.2f, 152.7f, 1065.3f, 152.7f, 872.9f, 152.7f, 768.1f, float.NaN, float.NaN,
+ float.NaN, float.NaN, 152.7f, 671.1f, 111.4f, 652.3f, 15.6f, 652.3f, -58.8f, 652.3f,
+ -58.8f, 505.3f,
+ ],
+ ],
+
+ ['}'] =
+ [
+ [
+ 614.3f, 657.3f, 539.6f, 657.3f, float.NaN, float.NaN, float.NaN, float.NaN, 441.7f, 657.3f,
+ 401.8f, 676.2f, 401.8f, 773.9f, 401.8f, 879.5f, float.NaN, float.NaN, float.NaN, float.NaN,
+ 401.8f, 1073.4f, 274, 1168, 93.1f, 1168, -56.3f, 1168, -56.3f, 1019.8f,
+ 43.3f, 1019.8f, float.NaN, float.NaN, float.NaN, float.NaN, 152.8f, 1019.8f, 230.9f, 1013.5f,
+ 230.9f, 879.5f, 230.9f, 773.9f, float.NaN, float.NaN, float.NaN, float.NaN, 230.9f, 714,
+ 249.1f, 628.9f, 305.6f, 602.1f, 345.4f, 583.2f, 305.6f, 565.9f, float.NaN, float.NaN,
+ float.NaN, float.NaN, 249.1f, 539.1f, 230.9f, 453.9f, 230.9f, 392.5f, 230.9f, 288.4f,
+ float.NaN, float.NaN, float.NaN, float.NaN, 230.9f, 154.5f, 152.8f, 148.2f, 43.3f, 148.2f,
+ -56.3f, 148.2f, -56.3f, 0, 93.1f, 0, float.NaN, float.NaN, float.NaN, float.NaN,
+ 274, 0, 401.8f, 93, 401.8f, 288.4f, 401.8f, 392.5f, float.NaN, float.NaN,
+ float.NaN, float.NaN, 401.8f, 491.8f, 441.7f, 509.1f, 539.6f, 509.1f, 614.3f, 509.1f,
+ 614.3f, 657.3f,
+ ],
+ ],
+
+ ['~'] =
+ [
+ [
+ 109.2f, 910.2f, float.NaN, float.NaN, float.NaN, float.NaN, 109.2f, 943.8f, 119.9f, 994.3f,
+ 161.1f, 994.3f, float.NaN, float.NaN, float.NaN, float.NaN, 190.1f, 994.3f, 220.6f, 969.8f,
+ 242, 954.5f, float.NaN, float.NaN, float.NaN, float.NaN, 287.8f, 919.4f, 338.2f, 896.4f,
+ 397.7f, 896.4f, float.NaN, float.NaN, float.NaN, float.NaN, 516.7f, 896.4f, 574.7f, 1008.1f,
+ 574.7f, 1113.6f, 432.8f, 1113.6f, float.NaN, float.NaN, float.NaN, float.NaN, 432.8f, 1083,
+ 423.6f, 1032.6f, 379.4f, 1032.6f, float.NaN, float.NaN, float.NaN, float.NaN, 347.3f, 1032.6f,
+ 321.4f, 1058.6f, 286.3f, 1083, float.NaN, float.NaN, float.NaN, float.NaN, 254.2f, 1107.5f,
+ 213, 1133.5f, 155, 1133.5f, float.NaN, float.NaN, float.NaN, float.NaN, 32.9f, 1133.5f,
+ -32.7f, 1041.7f, -32.7f, 925.5f, -32.7f, 910.2f, 109.2f, 910.2f,
+ ],
+ ],
+
+ ['\u00a3'] =
+ [
+ [
+ 289.9f, 532.2f, 490.6f, 532.2f, 490.6f, 681.1f, 322.6f, 681.1f, 370.2f, 907.6f,
+ float.NaN, float.NaN, float.NaN, float.NaN, 383.6f, 966.3f, 402.9f, 1024.9f, 468.3f, 1024.9f,
+ float.NaN, float.NaN, float.NaN, float.NaN, 511.4f, 1024.9f, 547.1f, 993.2f, 567.9f, 955.2f,
+ 613.9f, 955.2f, 613.9f, 1108.8f, float.NaN, float.NaN, float.NaN, float.NaN, 573.8f, 1151.6f,
+ 527.7f, 1173.8f, 469.8f, 1173.8f, float.NaN, float.NaN, float.NaN, float.NaN, 307.8f, 1173.8f,
+ 243.8f, 1024.9f, 212.6f, 879.1f, 169.5f, 681.1f, -22.2f, 681.1f, -22.2f, 532.2f,
+ 136.8f, 532.2f, 55.1f, 148.9f, -51.9f, 148.9f, -51.9f, 0, 613.9f, 0,
+ 613.9f, 148.9f, 206.7f, 148.9f, 289.9f, 532.2f,
+ ],
+ ],
+
+ ['\u00a4'] =
+ [
+ [
+ -102.2f, 770.1f, 15.5f, 654.2f, float.NaN, float.NaN, float.NaN, float.NaN, -26.2f, 584.1f,
+ -29.2f, 532.2f, -29.2f, 491, float.NaN, float.NaN, float.NaN, float.NaN, -29.2f, 448.3f,
+ -26.2f, 396.5f, 15.5f, 324.8f, -102.2f, 208.9f, -0.9f, 108.3f, 110.8f, 228.7f,
+ float.NaN, float.NaN, float.NaN, float.NaN, 139.1f, 199.8f, 224.1f, 178.4f, 271.7f, 178.4f,
+ float.NaN, float.NaN, float.NaN, float.NaN, 317.9f, 178.4f, 402.8f, 199.8f, 431.1f, 228.7f,
+ 542.9f, 108.3f, 644.2f, 208.9f, 526.5f, 324.8f, float.NaN, float.NaN, float.NaN, float.NaN,
+ 566.7f, 396.5f, 571.2f, 448.3f, 571.2f, 491, float.NaN, float.NaN, float.NaN, float.NaN,
+ 571.2f, 532.2f, 566.7f, 584.1f, 526.5f, 654.2f, 644.2f, 770.1f, 542.9f, 872.3f,
+ 431.1f, 750.3f, float.NaN, float.NaN, float.NaN, float.NaN, 402.8f, 780.8f, 317.9f, 800.6f,
+ 271.7f, 800.6f, float.NaN, float.NaN, float.NaN, float.NaN, 224.1f, 800.6f, 139.1f, 780.8f,
+ 110.8f, 750.3f, -0.9f, 872.3f, -102.2f, 770.1f,
+ ],
+ [
+ 271.7f, 304.9f, float.NaN, float.NaN, float.NaN, float.NaN, 174.9f, 304.9f, 98.9f, 389.7f,
+ 98.9f, 491.8f, float.NaN, float.NaN, float.NaN, float.NaN, 98.9f, 590.7f, 174.9f, 675.5f,
+ 271.7f, 675.5f, float.NaN, float.NaN, float.NaN, float.NaN, 365.6f, 675.5f, 443, 590.7f,
+ 443, 491.8f, float.NaN, float.NaN, float.NaN, float.NaN, 443, 389.7f, 365.6f, 304.9f,
+ 271.7f, 304.9f,
+ ],
+ ],
+
+ ['\u00a5'] =
+ [
+ [
+ 692, 1153.8f, 493.2f, 1153.8f, 271.8f, 843.2f, 48.8f, 1153.8f, -150, 1153.8f,
+ 187.8f, 686.3f, 187.8f, 497.7f, -98.3f, 497.7f, -98.3f, 348.7f, 187.8f, 348.7f,
+ 187.8f, 0, 354.2f, 0, 354.2f, 348.7f, 641.9f, 348.7f, 641.9f, 497.7f,
+ 354.2f, 497.7f, 354.2f, 686.3f, 692, 1153.8f,
+ ],
+ ],
+
+ ['\u00a7'] =
+ [
+ [
+ 252.5f, 637.5f, float.NaN, float.NaN, float.NaN, float.NaN, 296.3f, 632.9f, 341.7f, 628.3f,
+ 384.1f, 622.2f, float.NaN, float.NaN, float.NaN, float.NaN, 456.7f, 610, 489.9f, 573.3f,
+ 479.4f, 533.5f, float.NaN, float.NaN, float.NaN, float.NaN, 473.3f, 515.2f, 467.3f, 490.7f,
+ 453.6f, 470.8f, float.NaN, float.NaN, float.NaN, float.NaN, 411.3f, 477, 361.4f, 489.2f,
+ 306.9f, 490.7f, float.NaN, float.NaN, float.NaN, float.NaN, 264.6f, 492.2f, 204.1f, 498.4f,
+ 143.6f, 506, float.NaN, float.NaN, float.NaN, float.NaN, 111.8f, 510.6f, 90.6f, 538.1f,
+ 90.6f, 570.2f, float.NaN, float.NaN, float.NaN, float.NaN, 90.6f, 611.5f, 110.3f, 635.9f,
+ 134.5f, 663.5f, float.NaN, float.NaN, float.NaN, float.NaN, 167.8f, 649.7f, 208.6f, 643.6f,
+ 252.5f, 637.5f,
+ ],
+ [
+ -54.9f, 250.7f, float.NaN, float.NaN, float.NaN, float.NaN, -25.5f, 44.3f, 114, -13.8f,
+ 307.8f, -13.8f, float.NaN, float.NaN, float.NaN, float.NaN, 576.5f, -13.8f, 625, 172.7f,
+ 625, 230.8f, float.NaN, float.NaN, float.NaN, float.NaN, 625, 275.2f, 629.4f, 333.3f,
+ 558.9f, 405.1f, float.NaN, float.NaN, float.NaN, float.NaN, 592.7f, 440.3f, 625, 477,
+ 625, 561, float.NaN, float.NaN, float.NaN, float.NaN, 625, 704.7f, 475.2f, 770.5f,
+ 310.7f, 776.6f, float.NaN, float.NaN, float.NaN, float.NaN, 159.5f, 782.7f, 131.6f, 836.2f,
+ 131.6f, 889.7f, float.NaN, float.NaN, float.NaN, float.NaN, 131.6f, 943.2f, 197.7f, 989.1f,
+ 293.1f, 989.1f, float.NaN, float.NaN, float.NaN, float.NaN, 391.5f, 989.1f, 447.3f, 952.4f,
+ 447.3f, 880.5f, 598.5f, 880.5f, float.NaN, float.NaN, float.NaN, float.NaN, 598.5f, 1019.6f,
+ 486.9f, 1132.8f, 299, 1132.8f, float.NaN, float.NaN, float.NaN, float.NaN, 59.6f, 1132.8f,
+ -19.7f, 963.1f, -19.7f, 895.8f, float.NaN, float.NaN, float.NaN, float.NaN, -19.7f, 807.2f,
+ 3.8f, 761.3f, 34.7f, 730.7f, float.NaN, float.NaN, float.NaN, float.NaN, 9.7f, 704.7f,
+ -54.9f, 668, -54.9f, 574.8f, float.NaN, float.NaN, float.NaN, float.NaN, -54.9f, 388.3f,
+ 109.5f, 353.1f, 272.5f, 351.6f, float.NaN, float.NaN, float.NaN, float.NaN, 406.2f, 350.1f,
+ 473.7f, 322.6f, 473.7f, 240, float.NaN, float.NaN, float.NaN, float.NaN, 473.7f, 157.5f,
+ 368, 129.9f, 281.3f, 129.9f, float.NaN, float.NaN, float.NaN, float.NaN, 197.7f, 129.9f,
+ 131.6f, 157.5f, 96.3f, 250.7f, -54.9f, 250.7f,
+ ],
+ ],
+
+ ['\u00a8'] =
+ [
+ [
+ 491, 974.2f, 491, 1227, 351.6f, 1227, 351.6f, 974.2f, 491, 974.2f,
+ ],
+ [
+ 207.9f, 974.2f, 207.9f, 1227, 67, 1227, 67, 974.2f, 207.9f, 974.2f,
+ ],
+ ],
+
+ ['\u00b4'] =
+ [
+ [
+ 253.9f, 1195.7f, 125, 931.2f, 262.4f, 931.2f, 435.1f, 1195.7f, 253.9f, 1195.7f,
+ ],
+ ],
+
+ ['\u00b8'] =
+ [
+ [
+ 381.2f, -13.5f, 259.4f, -13.5f, 168.8f, -293.8f, 317.6f, -293.8f, 381.2f, -13.5f,
+ ],
+ ],
+
+ ['\u00c4'] =
+ [
+ [
+ 417, 309.1f, 490.3f, 0, 646.1f, 0, 369.1f, 1074.9f, 172.9f, 1074.9f,
+ -104.1f, 0, 51.6f, 0, 123.5f, 309.1f, 417, 309.1f,
+ ],
+ [
+ 374, 417.6f, 168.1f, 417.6f, 270.4f, 883.7f, 273.1f, 883.7f, 374, 417.6f,
+ ],
+ [
+ 633.3f, 951.3f, 633.3f, 1162.2f, 500.6f, 1162.2f, 500.6f, 951.3f, 633.3f, 951.3f,
+ ],
+ [
+ 48.6f, 951.3f, 48.6f, 1162.2f, -89.6f, 1162.2f, -89.6f, 951.3f, 48.6f, 951.3f,
+ ],
+ ],
+
+ ['\u00c5'] =
+ [
+ [
+ 634.9f, 0, 448.9f, 696.2f, float.NaN, float.NaN, float.NaN, float.NaN, 523.9f, 741.5f,
+ 564.4f, 803.4f, 568.9f, 894, float.NaN, float.NaN, float.NaN, float.NaN, 564.4f, 1069.2f,
+ 415.9f, 1119.1f, 269, 1119.1f, float.NaN, float.NaN, float.NaN, float.NaN, 117.6f, 1119.1f,
+ -30.9f, 1069.2f, -30.9f, 894, float.NaN, float.NaN, float.NaN, float.NaN, -30.9f, 803.4f,
+ 9.6f, 741.5f, 83.1f, 696.2f, -96.9f, 0, 51.6f, 0, 98.1f, 197.8f,
+ 439.9f, 197.8f, 486.4f, 0, 634.9f, 0,
+ ],
+ [
+ 406.9f, 333.8f, 131.1f, 333.8f, 206, 653.9f, 269, 649.4f, 332, 653.9f,
+ 406.9f, 333.8f,
+ ],
+ [
+ 262, 1002.8f, float.NaN, float.NaN, float.NaN, float.NaN, 340.8f, 1002.8f, 431.9f, 980.1f,
+ 431.9f, 898.6f, float.NaN, float.NaN, float.NaN, float.NaN, 431.9f, 798.9f, 348.5f, 770.2f,
+ 262, 770.2f, float.NaN, float.NaN, float.NaN, float.NaN, 175.5f, 770.2f, 92.1f, 798.9f,
+ 92.1f, 898.6f, float.NaN, float.NaN, float.NaN, float.NaN, 92.1f, 980.1f, 183.2f, 1002.8f,
+ 262, 1002.8f,
+ ],
+ ],
+
+ ['\u00c6'] =
+ [
+ [
+ 289.7f, 394.4f, 130, 394.4f, 224, 896.1f, 259.8f, 896.1f, 289.7f, 394.4f,
+ ],
+ [
+ 298.6f, 268.7f, 315, 0, 622.4f, 0, 622.4f, 146, 444.8f, 146,
+ 421, 479.9f, 611.9f, 479.9f, 611.9f, 625.9f, 410.5f, 625.9f, 389.6f, 907,
+ 611.9f, 907, 611.9f, 1052.9f, 118.1f, 1052.9f, -83.3f, 0, 52.5f, 0,
+ 104.7f, 268.7f, 298.6f, 268.7f,
+ ],
+ ],
+
+ ['\u00d1'] =
+ [
+ [
+ 469.5f, 218.6f, 466.5f, 218.6f, 119.1f, 793.6f, -59.1f, 793.6f, -59.1f, 0,
+ 80.5f, 0, 80.5f, 600.4f, 83.5f, 600.4f, 442.8f, 0, 609.1f, 0,
+ 609.1f, 793.6f, 469.5f, 793.6f, 469.5f, 218.6f,
+ ],
+ [
+ 110.6f, 949.6f, float.NaN, float.NaN, float.NaN, float.NaN, 110.6f, 984.4f, 121, 1036.7f,
+ 161.1f, 1036.7f, float.NaN, float.NaN, float.NaN, float.NaN, 189.3f, 1036.7f, 219, 1011.3f,
+ 239.8f, 995.5f, float.NaN, float.NaN, float.NaN, float.NaN, 284.3f, 959.1f, 333.3f, 935.3f,
+ 391.2f, 935.3f, float.NaN, float.NaN, float.NaN, float.NaN, 507.1f, 935.3f, 565, 1050.9f,
+ 565, 1160.2f, 425.4f, 1160.2f, float.NaN, float.NaN, float.NaN, float.NaN, 425.4f, 1128.6f,
+ 416.5f, 1076.3f, 373.4f, 1076.3f, float.NaN, float.NaN, float.NaN, float.NaN, 342.2f, 1076.3f,
+ 317, 1103.2f, 282.8f, 1128.6f, float.NaN, float.NaN, float.NaN, float.NaN, 251.7f, 1153.9f,
+ 211.6f, 1180.8f, 155.2f, 1180.8f, float.NaN, float.NaN, float.NaN, float.NaN, 36.4f, 1180.8f,
+ -29, 1085.8f, -29, 965.4f, -29, 949.6f, 110.6f, 949.6f,
+ ],
+ ],
+
+ ['\u00d6'] =
+ [
+ [
+ 281.7f, 913.8f, float.NaN, float.NaN, float.NaN, float.NaN, 48.2f, 913.8f, -74.4f, 678.8f,
+ -74.4f, 456.1f, float.NaN, float.NaN, float.NaN, float.NaN, -74.4f, 235, 48.2f, -13.9f,
+ 281.7f, -13.9f, float.NaN, float.NaN, float.NaN, float.NaN, 515.3f, -13.9f, 636.4f, 235,
+ 636.4f, 456.1f, float.NaN, float.NaN, float.NaN, float.NaN, 636.4f, 678.8f, 515.3f, 913.8f,
+ 281.7f, 913.8f,
+ ],
+ [
+ 281.7f, 784.5f, float.NaN, float.NaN, float.NaN, float.NaN, 435.4f, 784.5f, 484.2f, 612.5f,
+ 484.2f, 462.8f, float.NaN, float.NaN, float.NaN, float.NaN, 484.2f, 324.3f, 435.4f, 128.4f,
+ 281.7f, 128.4f, float.NaN, float.NaN, float.NaN, float.NaN, 128, 128.4f, 77.8f, 324.3f,
+ 77.8f, 462.8f, float.NaN, float.NaN, float.NaN, float.NaN, 77.8f, 612.5f, 128, 784.5f,
+ 281.7f, 784.5f,
+ ],
+ [
+ 633.1f, 943.6f, 633.1f, 1149.3f, 495.5f, 1149.3f, 495.5f, 943.6f, 633.1f, 943.6f,
+ ],
+ [
+ 45.5f, 943.6f, 45.5f, 1149.3f, -92.1f, 1149.3f, -92.1f, 943.6f, 45.5f, 943.6f,
+ ],
+ ],
+
+ ['\u00d8'] =
+ [
+ [
+ 275, 1026.9f, float.NaN, float.NaN, float.NaN, float.NaN, 18.1f, 1026.9f, -83.5f, 746.5f,
+ -83.5f, 563.5f, float.NaN, float.NaN, float.NaN, float.NaN, -83.5f, 364.5f, 16.6f, 97.4f,
+ 275, 97.4f, float.NaN, float.NaN, float.NaN, float.NaN, 533.4f, 97.4f, 633.5f, 364.5f,
+ 633.5f, 563.5f, float.NaN, float.NaN, float.NaN, float.NaN, 633.5f, 746.5f, 531.9f, 1026.9f,
+ 275, 1026.9f,
+ ],
+ [
+ 275, 913.5f, float.NaN, float.NaN, float.NaN, float.NaN, 452.8f, 913.5f, 479.7f, 678.3f,
+ 479.7f, 558.6f, float.NaN, float.NaN, float.NaN, float.NaN, 479.7f, 441.7f, 449.8f, 214.7f,
+ 275, 214.7f, float.NaN, float.NaN, float.NaN, float.NaN, 98.7f, 214.7f, 70.3f, 441.7f,
+ 70.3f, 558.6f, float.NaN, float.NaN, float.NaN, float.NaN, 70.3f, 678.3f, 95.7f, 913.5f,
+ 275, 913.5f,
+ ],
+ [
+ 373.5f, 1138.8f, 69.4f, 14.5f, 176.5f, -14.4f, 480.6f, 1109.8f, 373.5f, 1138.8f,
+ ],
+ ],
+
+ ['\u00dc'] =
+ [
+ [
+ -88.4f, 868.4f, -88.4f, 349.5f, float.NaN, float.NaN, float.NaN, float.NaN, -88.4f, 123.2f,
+ 29.1f, -13.9f, 296, -13.9f, float.NaN, float.NaN, float.NaN, float.NaN, 561.2f, -13.9f,
+ 680.4f, 123.2f, 680.4f, 349.5f, 680.4f, 868.4f, 507.5f, 868.4f, 507.5f, 360.3f,
+ float.NaN, float.NaN, float.NaN, float.NaN, 507.5f, 214, 468.9f, 130.9f, 296, 130.9f,
+ float.NaN, float.NaN, float.NaN, float.NaN, 123.1f, 130.9f, 84.5f, 214, 84.5f, 360.3f,
+ 84.5f, 868.4f, -88.4f, 868.4f,
+ ],
+ [
+ 122.8f, 1028.1f, 122.8f, 1137.4f, -84.2f, 1137.4f, -84.2f, 1028.1f, 122.8f, 1028.1f,
+ ],
+ [
+ 637, 1028.1f, 637, 1137.4f, 436.3f, 1137.4f, 436.3f, 1028.1f, 637, 1028.1f,
+ ],
+ ],
+
+ ['\u00df'] =
+ [
+ [
+ 274.4f, 529.7f, float.NaN, float.NaN, float.NaN, float.NaN, 423.1f, 529.7f, 465.6f, 450.5f,
+ 465.6f, 304.5f, float.NaN, float.NaN, float.NaN, float.NaN, 465.6f, 175.5f, 417.1f, 133.6f,
+ 297.2f, 133.6f, 280.5f, 133.6f, 280.5f, 0, 313.9f, 0, float.NaN, float.NaN,
+ float.NaN, float.NaN, 491.4f, 0, 622, 122.7f, 622, 313.8f, float.NaN, float.NaN,
+ float.NaN, float.NaN, 622, 448.9f, 567.3f, 562.3f, 441.4f, 607.4f, float.NaN, float.NaN,
+ float.NaN, float.NaN, 530.9f, 658.7f, 587.1f, 741, 587.1f, 851.3f, float.NaN, float.NaN,
+ float.NaN, float.NaN, 587.1f, 1045.5f, 417.1f, 1151.1f, 247.1f, 1151.1f, float.NaN, float.NaN,
+ float.NaN, float.NaN, 51.3f, 1151.1f, -91.4f, 1014.4f, -91.4f, 806.2f, -91.4f, 0,
+ 65, 0, 65, 753.4f, float.NaN, float.NaN, float.NaN, float.NaN, 65, 901,
+ 89.2f, 1017.5f, 256.2f, 1017.5f, float.NaN, float.NaN, float.NaN, float.NaN, 368.5f, 1017.5f,
+ 430.7f, 956.9f, 430.7f, 837.3f, float.NaN, float.NaN, float.NaN, float.NaN, 430.7f, 727,
+ 376.1f, 675.7f, 274.4f, 675.7f, 250.1f, 675.7f, 250.1f, 529.7f, 274.4f, 529.7f,
+ ],
+ ],
+
+ ['\u00e5'] =
+ [
+ [
+ 595.9f, 471.3f, float.NaN, float.NaN, float.NaN, float.NaN, 595.9f, 542.2f, 604.9f, 675.2f,
+ 474.3f, 712.9f, float.NaN, float.NaN, float.NaN, float.NaN, 538.9f, 758.2f, 565.9f, 806.6f,
+ 565.9f, 886.6f, float.NaN, float.NaN, float.NaN, float.NaN, 565.9f, 1054.3f, 427.8f, 1119.2f,
+ 283.7f, 1119.2f, float.NaN, float.NaN, float.NaN, float.NaN, 139.6f, 1119.2f, 1.5f, 1054.3f,
+ 1.5f, 886.6f, float.NaN, float.NaN, float.NaN, float.NaN, 1.5f, 806.6f, 28.6f, 758.2f,
+ 93.1f, 712.9f, float.NaN, float.NaN, float.NaN, float.NaN, 31.6f, 684.2f, -3, 635.9f,
+ -13.5f, 566.4f, 141.1f, 566.4f, float.NaN, float.NaN, float.NaN, float.NaN, 141.1f, 640.4f,
+ 222.2f, 654, 301.7f, 654, float.NaN, float.NaN, float.NaN, float.NaN, 429.3f, 654,
+ 441.3f, 589.1f, 441.3f, 503, 441.3f, 475.8f, 301.7f, 475.8f, float.NaN, float.NaN,
+ float.NaN, float.NaN, 136.6f, 475.8f, -46.5f, 416.9f, -46.5f, 220.5f, float.NaN, float.NaN,
+ float.NaN, float.NaN, -46.5f, 66.5f, 78.1f, -13.6f, 223.7f, -13.6f, float.NaN, float.NaN,
+ float.NaN, float.NaN, 303.2f, -13.6f, 382.8f, 16.6f, 441.3f, 72.5f, 441.3f, 0,
+ 595.9f, 0, 595.9f, 471.3f,
+ ],
+ [
+ 438.4f, 359.5f, 438.4f, 332.3f, float.NaN, float.NaN, float.NaN, float.NaN, 438.4f, 208.4f,
+ 370.4f, 102.7f, 229.7f, 102.7f, float.NaN, float.NaN, float.NaN, float.NaN, 152.4f, 102.7f,
+ 95.2f, 148, 95.2f, 226.6f, float.NaN, float.NaN, float.NaN, float.NaN, 95.2f, 326.3f,
+ 186.4f, 359.5f, 273, 359.5f, 438.4f, 359.5f,
+ ],
+ [
+ 290, 1007.5f, float.NaN, float.NaN, float.NaN, float.NaN, 375.1f, 1007.5f, 453.9f, 974.2f,
+ 453.9f, 886.6f, float.NaN, float.NaN, float.NaN, float.NaN, 453.9f, 786.9f, 373.5f, 764.3f,
+ 290, 764.3f, float.NaN, float.NaN, float.NaN, float.NaN, 206.6f, 764.3f, 126.2f, 786.9f,
+ 126.2f, 886.6f, float.NaN, float.NaN, float.NaN, float.NaN, 126.2f, 974.2f, 205, 1007.5f,
+ 290, 1007.5f,
+ ],
+ ],
+
+ ['\u00e6'] =
+ [
+ [
+ 338.2f, 477.9f, 338.2f, 523.1f, float.NaN, float.NaN, float.NaN, float.NaN, 338.2f, 597.2f,
+ 338.2f, 682.7f, 417.7f, 682.7f, float.NaN, float.NaN, float.NaN, float.NaN, 500.3f, 682.7f,
+ 500.3f, 597.2f, 500.3f, 523.1f, 500.3f, 477.9f, 338.2f, 477.9f,
+ ],
+ [
+ 209.9f, 359.1f, 209.9f, 260.7f, float.NaN, float.NaN, float.NaN, float.NaN, 209.9f, 202.7f,
+ 203.9f, 110.7f, 129.7f, 110.7f, float.NaN, float.NaN, float.NaN, float.NaN, 55.5f, 110.7f,
+ 49.5f, 186.5f, 49.5f, 236.5f, float.NaN, float.NaN, float.NaN, float.NaN, 49.5f, 343,
+ 108.5f, 359.1f, 199.3f, 359.1f, 209.9f, 359.1f,
+ ],
+ [
+ 630.7f, 355.4f, 630.7f, 504.2f, float.NaN, float.NaN, float.NaN, float.NaN, 630.7f, 660.8f,
+ 612.5f, 798.6f, 427.9f, 798.6f, float.NaN, float.NaN, float.NaN, float.NaN, 371.9f, 798.6f,
+ 305.3f, 775.1f, 276.5f, 718.7f, float.NaN, float.NaN, float.NaN, float.NaN, 250.8f, 782.9f,
+ 181.2f, 798.6f, 120.6f, 798.6f, float.NaN, float.NaN, float.NaN, float.NaN, -5, 798.6f,
+ -80.7f, 709.3f, -80.7f, 580.9f, 49.5f, 580.9f, float.NaN, float.NaN, float.NaN, float.NaN,
+ 49.5f, 629.5f, 76.7f, 676.4f, 132.7f, 676.4f, float.NaN, float.NaN, float.NaN, float.NaN,
+ 209.9f, 676.4f, 209.9f, 593.4f, 209.9f, 512, 209.9f, 477.6f, 197.8f, 477.6f,
+ float.NaN, float.NaN, float.NaN, float.NaN, 5.6f, 477.6f, -80.7f, 440, -80.7f, 222.3f,
+ float.NaN, float.NaN, float.NaN, float.NaN, -80.7f, 93.9f, -32.3f, -14.1f, 107, -14.1f,
+ float.NaN, float.NaN, float.NaN, float.NaN, 176.6f, -14.1f, 246.2f, 11, 276.5f, 79.9f,
+ float.NaN, float.NaN, float.NaN, float.NaN, 302.2f, 9.4f, 364.3f, -14.1f, 433.9f, -14.1f,
+ float.NaN, float.NaN, float.NaN, float.NaN, 564.1f, -14.1f, 630.7f, 68.9f, 630.7f, 198.9f,
+ 630.7f, 234.9f, 497.5f, 234.9f, float.NaN, float.NaN, float.NaN, float.NaN, 497.5f, 178.5f,
+ 489.9f, 114.3f, 423.3f, 114.3f, float.NaN, float.NaN, float.NaN, float.NaN, 343.1f, 114.3f,
+ 343.1f, 205.1f, 343.1f, 267.8f, 343.1f, 355.4f, 630.7f, 355.4f,
+ ],
+ ],
+
+ ['\u00f8'] =
+ [
+ [
+ 599.3f, 905.5f, 479.5f, 905.5f, 409.4f, 769, float.NaN, float.NaN, float.NaN, float.NaN,
+ 368.5f, 783, 321.8f, 789.2f, 278, 790.7f, float.NaN, float.NaN, float.NaN, float.NaN,
+ 56, 790.7f, -76.9f, 627.9f, -76.9f, 392.3f, float.NaN, float.NaN, float.NaN, float.NaN,
+ -76.9f, 274.4f, -38.9f, 150.4f, 50.2f, 66.7f, -55, -139.5f, 63.3f, -139.5f,
+ 139.3f, 9.3f, float.NaN, float.NaN, float.NaN, float.NaN, 181.6f, -4.7f, 226.9f, -14,
+ 278, -14, float.NaN, float.NaN, float.NaN, float.NaN, 502.9f, -14, 632.9f, 162.8f,
+ 632.9f, 392.3f, float.NaN, float.NaN, float.NaN, float.NaN, 632.9f, 502.4f, 584.7f, 637.2f,
+ 498.5f, 707, 599.3f, 905.5f,
+ ],
+ [
+ 449.4f, 578.3f, float.NaN, float.NaN, float.NaN, float.NaN, 489.7f, 525.6f, 502.1f, 448.1f,
+ 502.1f, 390.7f, float.NaN, float.NaN, float.NaN, float.NaN, 502.1f, 246.5f, 452.5f, 105.4f,
+ 285, 105.4f, float.NaN, float.NaN, float.NaN, float.NaN, 250.9f, 105.4f, 224.5f, 116.3f,
+ 202.8f, 127.1f, 449.4f, 578.3f,
+ ],
+ [
+ 111.3f, 184, float.NaN, float.NaN, float.NaN, float.NaN, 67.9f, 239.9f, 67.9f, 319.8f,
+ 67.9f, 388.4f, float.NaN, float.NaN, float.NaN, float.NaN, 67.9f, 537, 125.3f, 677.5f,
+ 285, 677.5f, float.NaN, float.NaN, float.NaN, float.NaN, 309.8f, 677.5f, 336.2f, 672.7f,
+ 361, 658.3f, 111.3f, 184,
+ ],
+ ],
+
+ ['\u0132'] =
+ [
+ [
+ 54.4f, 1065.6f, -106.2f, 1065.6f, -106.2f, 0, 54.4f, 0, 54.4f, 1065.6f,
+ ],
+ [
+ 503.5f, 303.3f, float.NaN, float.NaN, float.NaN, float.NaN, 503.5f, 238.9f, 497.3f, 136.7f,
+ 395.9f, 136.7f, float.NaN, float.NaN, float.NaN, float.NaN, 347.6f, 136.7f, 299.2f, 146.2f,
+ 254, 161.9f, 254, 0, 410, -12.6f, float.NaN, float.NaN, float.NaN, float.NaN,
+ 503.5f, -18.9f, 590.9f, 11, 634.6f, 100.6f, float.NaN, float.NaN, float.NaN, float.NaN,
+ 664.2f, 160.3f, 664.2f, 240.5f, 664.2f, 303.3f, 664.2f, 1065.6f, 503.5f, 1065.6f,
+ 503.5f, 303.3f,
+ ],
+ ],
+
+ ['\u0133'] =
+ [
+ [
+ 93, 771.6f, -56.4f, 771.6f, -56.4f, 7, 93, 7, 93, 771.6f,
+ ],
+ [
+ 447.2f, 764.6f, 447.2f, 80.9f, float.NaN, float.NaN, float.NaN, float.NaN, 447.2f, -67.2f,
+ 434.2f, -132.8f, 281.9f, -132.8f, 194.9f, -132.8f, 194.9f, -264, 303.7f, -264,
+ float.NaN, float.NaN, float.NaN, float.NaN, 548.7f, -264, 596.6f, -148, 596.6f, 90,
+ 596.6f, 764.6f, 447.2f, 764.6f,
+ ],
+ [
+ 428.3f, 1152.4f, 428.3f, 952.6f, 646.9f, 952.6f, 646.9f, 1152.4f, 428.3f, 1152.4f,
+ ],
+ [
+ -104.7f, 1152.4f, -104.7f, 952.6f, 113.9f, 952.6f, 113.9f, 1152.4f, -104.7f, 1152.4f,
+ ],
+ ],
+
+ ['|'] =
+ [
+ [275, -90, 275, 1170],
+ ],
+
+ ['\u2223'] =
+ [
+ [275, -190, 275, 514.75f],
+ [275, 565.25f, 275, 1270],
+ ],
+
+ ['\u2015'] =
+ [
+ [-309, -454, 859, -454],
+ ],
+
+ ['\u2588'] =
+ [
+ [-99, -51, 649, -51, 649, 1090, -99, 1090, -99, -51],
+ ],
+ };
+
+ ///
+ /// Gets, per character, the indices of strokes drawn with projecting square caps.
+ ///
+ public static IReadOnlyDictionary SquareStrokes { get; } = new Dictionary
+ {
+ };
+
+ ///
+ /// Gets, per character, the indices of strokes cut off exactly at their endpoints.
+ ///
+ public static IReadOnlyDictionary ButtStrokes { get; } = new Dictionary
+ {
+ ['|'] = [0],
+ ['\u2223'] = [0, 1],
+ ['\u2015'] = [0],
+ };
+
+ ///
+ /// Gets, per character, the indices of strokes drawn with butt ends and sharp miter joins.
+ ///
+ public static IReadOnlyDictionary MiterStrokes { get; } = new Dictionary
+ {
+ };
+
+ ///
+ /// Gets, per character, the indices of strokes drawn with round caps and sharp miter joins.
+ ///
+ public static IReadOnlyDictionary MiterRoundStrokes { get; } = new Dictionary
+ {
+ };
+
+ ///
+ /// Gets the stroke width per character. Zero marks a character whose entry is filled ink
+ /// outlines fitted to the standard.
+ ///
+ public static IReadOnlyDictionary StrokeWidths { get; } = new Dictionary
+ {
+ ['!'] = 0,
+ ['"'] = 0,
+ ['#'] = 0,
+ ['$'] = 0,
+ ['%'] = 0,
+ ['&'] = 0,
+ ['\''] = 0,
+ ['('] = 0,
+ [')'] = 0,
+ ['*'] = 0,
+ ['+'] = 0,
+ [','] = 0,
+ ['-'] = 0,
+ ['.'] = 0,
+ ['/'] = 0,
+ ['0'] = 0,
+ ['1'] = 0,
+ ['2'] = 0,
+ ['3'] = 0,
+ ['4'] = 0,
+ ['5'] = 0,
+ ['6'] = 0,
+ ['7'] = 0,
+ ['8'] = 0,
+ ['9'] = 0,
+ [':'] = 0,
+ [';'] = 0,
+ ['<'] = 0,
+ ['='] = 0,
+ ['>'] = 0,
+ ['?'] = 0,
+ ['@'] = 0,
+ ['A'] = 0,
+ ['B'] = 0,
+ ['C'] = 0,
+ ['D'] = 0,
+ ['E'] = 0,
+ ['F'] = 0,
+ ['G'] = 0,
+ ['H'] = 0,
+ ['I'] = 0,
+ ['J'] = 0,
+ ['K'] = 0,
+ ['L'] = 0,
+ ['M'] = 0,
+ ['N'] = 0,
+ ['O'] = 0,
+ ['P'] = 0,
+ ['Q'] = 0,
+ ['R'] = 0,
+ ['S'] = 0,
+ ['T'] = 0,
+ ['U'] = 0,
+ ['V'] = 0,
+ ['W'] = 0,
+ ['X'] = 0,
+ ['Y'] = 0,
+ ['Z'] = 0,
+ ['['] = 0,
+ ['\\'] = 0,
+ [']'] = 0,
+ ['^'] = 0,
+ ['_'] = 0,
+ ['`'] = 0,
+ ['a'] = 0,
+ ['b'] = 0,
+ ['c'] = 0,
+ ['d'] = 0,
+ ['e'] = 0,
+ ['f'] = 0,
+ ['g'] = 0,
+ ['h'] = 0,
+ ['i'] = 0,
+ ['j'] = 0,
+ ['k'] = 0,
+ ['l'] = 0,
+ ['m'] = 0,
+ ['n'] = 0,
+ ['o'] = 0,
+ ['p'] = 0,
+ ['q'] = 0,
+ ['r'] = 0,
+ ['s'] = 0,
+ ['t'] = 0,
+ ['u'] = 0,
+ ['v'] = 0,
+ ['w'] = 0,
+ ['x'] = 0,
+ ['y'] = 0,
+ ['z'] = 0,
+ ['{'] = 0,
+ ['}'] = 0,
+ ['~'] = 0,
+ ['\u00a3'] = 0,
+ ['\u00a4'] = 0,
+ ['\u00a5'] = 0,
+ ['\u00a7'] = 0,
+ ['\u00a8'] = 0,
+ ['\u00b4'] = 0,
+ ['\u00b8'] = 0,
+ ['\u00c4'] = 0,
+ ['\u00c5'] = 0,
+ ['\u00c6'] = 0,
+ ['\u00d1'] = 0,
+ ['\u00d6'] = 0,
+ ['\u00d8'] = 0,
+ ['\u00dc'] = 0,
+ ['\u00df'] = 0,
+ ['\u00e5'] = 0,
+ ['\u00e6'] = 0,
+ ['\u00f8'] = 0,
+ ['\u0132'] = 0,
+ ['\u0133'] = 0,
+ };
+
+ ///
+ /// Gets per-stroke width overrides keyed by character and stroke index. The value zero
+ /// marks a stroke that is already a filled ink polygon rather than a centerline to stroke.
+ ///
+ public static IReadOnlyDictionary<(char Character, int Stroke), float> StrokeWidthOverrides { get; } =
+ new Dictionary<(char Character, int Stroke), float>
+ {
+ [('\u2588', 0)] = 0,
+ };
+
+ ///
+ /// Gets the design definition the generator consumes.
+ ///
+ public static FontDesign Design { get; } = new()
+ {
+ Name = "OcrB",
+ FamilyName = "SixLabors OCRB",
+ DataSummary = "Built clean-room from the character sheet and dimension tables of ECMA-11.",
+ UnitsPerEm = UnitsPerEm,
+ H = H,
+ W = W,
+ Descender = D,
+ DefaultStrokeWidth = T,
+ SmallLetterHeight = SmallLetterHeight,
+ CapitalHeight = CapitalHeight,
+ AscenderHeight = AscenderHeight,
+ DescenderDepth = DescenderDepth,
+ NormalizationExceptions = string.Empty,
+ DrawnCapitalHeight = 1050,
+
+ // The reference OCR-B digitization prints its capital ink 678 units above the baseline per em, so
+ // the scale lands ours there and a point size means the same glyph size in both.
+ EmScale = 678F / CapitalHeight,
+ DrawnSmallLetterHeight = 780.6F,
+ DrawnAscenderHeight = 1154.2F,
+ DrawnDescenderDepth = 266.7F,
+ CutTerminals = false,
+ Skeletons = Skeletons,
+ SquareStrokes = SquareStrokes,
+ ButtStrokes = ButtStrokes,
+ MiterStrokes = MiterStrokes,
+ MiterRoundStrokes = MiterRoundStrokes,
+ StrokeWidths = StrokeWidths,
+ StrokeWidthOverrides = StrokeWidthOverrides,
+ Expectations = SpecChecks.OcrB,
+ };
+}
diff --git a/src/ImageSharp.Drawing.FontGenerator/Program.cs b/src/ImageSharp.Drawing.FontGenerator/Program.cs
new file mode 100644
index 00000000..6ccf7dfc
--- /dev/null
+++ b/src/ImageSharp.Drawing.FontGenerator/Program.cs
@@ -0,0 +1,1255 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+// Generates the SixLabors OCR fonts from the OCR standards themselves: OCR-A from the dimensioned
+// character drawings of FIPS PUB 32, OCR-B from the character sheet and dimension tables of ECMA-11.
+// Each design's glyph skeletons are stroked with the library's own path stroker, the outlines are
+// verified against the independently transcribed SpecChecks expectations, and FontData.cs is
+// written straight into the library. The .ttf and the proof, grid and comparison sheets are written
+// into artifacts/fonts. See specs/README.md for the source documents.
+using System.Globalization;
+using System.Numerics;
+using System.Text;
+using SixLabors.Fonts;
+using SixLabors.ImageSharp;
+using SixLabors.ImageSharp.Drawing;
+using SixLabors.ImageSharp.Drawing.FontGenerator;
+using SixLabors.ImageSharp.Drawing.Processing;
+using SixLabors.ImageSharp.PixelFormats;
+using SixLabors.ImageSharp.Processing;
+
+string? referencePath = null;
+string? referenceBPath = null;
+List inspectTexts = [];
+for (int argIndex = 0; argIndex < args.Length; argIndex++)
+{
+ if (args[argIndex] == "--ref" && argIndex + 1 < args.Length)
+ {
+ referencePath = args[++argIndex];
+ }
+ else if (args[argIndex] == "--refb" && argIndex + 1 < args.Length)
+ {
+ referenceBPath = args[++argIndex];
+ }
+ else if (args[argIndex] is "-h" or "--help")
+ {
+ Console.WriteLine("Usage: ImageSharp.Drawing.FontGenerator [--ref ] [--refb ] [...]");
+ Console.WriteLine(" --ref renders a side-by-side comparison sheet of the OCR-A design against the given font");
+ Console.WriteLine(" --refb renders the same sheet for the OCR-B design");
+ Console.WriteLine(" Remaining arguments render full-size single glyph inspection images per design.");
+ return 0;
+ }
+ else
+ {
+ inspectTexts.Add(args[argIndex]);
+ }
+}
+
+string libraryFontDirectory = RepositoryPaths.LibraryFonts;
+string outputDirectory = RepositoryPaths.Artifacts;
+Directory.CreateDirectory(outputDirectory);
+
+StrokeOptions roundStroke = new()
+{
+ LineJoin = LineJoin.Round,
+ LineCap = LineCap.Round,
+};
+
+StrokeOptions squareStroke = new()
+{
+ LineJoin = LineJoin.Round,
+ LineCap = LineCap.Square,
+};
+
+StrokeOptions buttStroke = new()
+{
+ LineJoin = LineJoin.Miter,
+ LineCap = LineCap.Butt,
+ MiterLimit = 1.5D,
+};
+
+StrokeOptions miterSquareStroke = new()
+{
+ LineJoin = LineJoin.Miter,
+ LineCap = LineCap.Square,
+ MiterLimit = 1.5D,
+};
+
+StrokeOptions miterStroke = new()
+{
+ LineJoin = LineJoin.Miter,
+ LineCap = LineCap.Butt,
+};
+
+StrokeOptions miterRoundStroke = new()
+{
+ LineJoin = LineJoin.Miter,
+ LineCap = LineCap.Round,
+};
+
+FontDesign[] designs = [OcrAGlyphs.Design, OcrBGlyphs.Design];
+FontCollection collection = new();
+Dictionary families = [];
+foreach (FontDesign design in designs)
+{
+ if (design.Skeletons.Count == 0)
+ {
+ Console.WriteLine($"{design.Name}: no glyphs yet, skipped");
+ continue;
+ }
+
+ float emScale = design.EmScale;
+ ushort advanceWidth = (ushort)MathF.Round(design.UnitsPerEm * emScale);
+
+ // The sheet the skeletons are traced from fixes shape but not size, so every glyph is normalized
+ // onto the height the standard gives its class. The scale is the glyph's own drawn bounds against
+ // the bounds the standard calls for, and it applies to both axes, so the glyph lands on the line
+ // and keeps every ratio it was drawn with. Each glyph carries its own scale, so a line of capitals,
+ // digits and full height marks is one height throughout.
+ float NormalizedScale(char character, float top, float bottom)
+ {
+ if (design.NormalizationExceptions.Contains(character, StringComparison.Ordinal))
+ {
+ return 1F;
+ }
+
+ // A letter carries its category's scale, so a letter that reaches past its line, an accented one
+ // for example, keeps the mark in proportion with the body. A digit and a mark carry their own,
+ // so a line of digits and full height marks is one height throughout. A mark that never reaches
+ // the capital line, a hyphen or a comma, is not a full height glyph: normalizing it onto that
+ // line would enlarge it out of all proportion, so it carries the scale of the capitals.
+ switch (Classify(character))
+ {
+ case GlyphClass.Absolute:
+ return 1F;
+ case GlyphClass.Digit:
+ return design.CapitalHeight / top;
+ case GlyphClass.Ascender:
+ return design.AscenderHeight / design.DrawnAscenderHeight;
+ case GlyphClass.SmallLetter:
+ return design.SmallLetterHeight / design.DrawnSmallLetterHeight;
+ case GlyphClass.Descender:
+ return design.DescenderDepth / design.DrawnDescenderDepth;
+ default:
+ float capitalScale = design.CapitalHeight / design.DrawnCapitalHeight;
+ return char.IsLetter(character) || top < design.DrawnCapitalHeight
+ ? capitalScale
+ : design.CapitalHeight / top;
+ }
+ }
+
+ // Ink projects a half stroke beyond the centerline box at every terminal, dots rise above it and
+ // descenders drop below it, so the vertical metrics cover those extremes. Text layout grows the
+ // line box for any glyph whose ink passes the ascender, which shifts that glyph off the shared
+ // baseline, so the metrics are measured from the geometry rather than assumed from the box.
+ float inkTop = 0;
+ float inkBottom = 0;
+
+ // The cap height is the ink height of a capital, which text layout uses to set a line of capitals
+ // or digits against a reference line.
+ float capHeight = design.CapitalHeight;
+
+ float sideBearing = (design.UnitsPerEm - design.W) / 2;
+ Dictionary>> auditOutlines = [];
+ Dictionary>> quantizedGlyphs = [];
+ Dictionary glyphScales = [];
+ List specFailures = [];
+ foreach ((char character, float[][] strokes) in design.Skeletons.OrderBy(entry => entry.Key))
+ {
+ List> designContours = [];
+ design.SquareStrokes.TryGetValue(character, out int[]? squareIndices);
+ design.ButtStrokes.TryGetValue(character, out int[]? buttIndices);
+ design.MiterStrokes.TryGetValue(character, out int[]? miterIndices);
+ design.MiterRoundStrokes.TryGetValue(character, out int[]? miterRoundIndices);
+ float characterWidth = design.StrokeWidths.TryGetValue(character, out float perCharacter)
+ ? perCharacter
+ : design.DefaultStrokeWidth;
+
+ // A skeleton may record one outline as a run of single-segment strokes that share endpoints.
+ // Under cut terminals every seam would project two square caps, so such runs merge into one
+ // figure. Glyphs with per-stroke styling or width overrides keep their stroke list untouched
+ // so the recorded stroke indices stay valid.
+ float[][] strokeSet = strokes;
+ if (design.CutTerminals
+ && squareIndices is null
+ && buttIndices is null
+ && miterIndices is null
+ && miterRoundIndices is null
+ && !design.StrokeWidthOverrides.Keys.Any(key => key.Character == character))
+ {
+ List chained = [];
+ foreach (float[] stroke in strokes)
+ {
+ float[]? previous = chained.Count > 0 ? chained[^1] : null;
+ if (previous is not null && previous[^2] == stroke[0] && previous[^1] == stroke[1])
+ {
+ float[] run = new float[previous.Length + stroke.Length - 2];
+
+ previous.CopyTo(run, 0);
+ stroke.AsSpan(2).CopyTo(run.AsSpan(previous.Length));
+ chained[^1] = run;
+ }
+ else
+ {
+ chained.Add(stroke);
+ }
+ }
+
+ strokeSet = [.. chained];
+ }
+
+ // Union all strokes into one outline so the glyph carries no overlapping contours: coincident
+ // edges from overlaid strokes otherwise double-cover antialiased pixels at render time.
+ List centerlines = [];
+ List strokeWidths = [];
+ List strokeStyles = [];
+ List cutFlags = [];
+ for (int strokeIndex = 0; strokeIndex < strokeSet.Length; strokeIndex++)
+ {
+ float[] tokens = strokeSet[strokeIndex];
+ PathBuilder builder = new();
+ builder.StartFigure();
+ BuildStroke(builder, tokens);
+
+ // A stroke that returns to its start point is a loop: close it so the seam takes a join
+ // rather than two caps.
+ if (tokens[0] == tokens[^2] && tokens[1] == tokens[^1])
+ {
+ builder.CloseFigure();
+ }
+
+ StrokeOptions options = design.CutTerminals ? buttStroke : roundStroke;
+ bool cutEnds = design.CutTerminals;
+ if (squareIndices is not null && Array.IndexOf(squareIndices, strokeIndex) >= 0)
+ {
+ options = squareStroke;
+ cutEnds = false;
+ }
+ else if (buttIndices is not null && Array.IndexOf(buttIndices, strokeIndex) >= 0)
+ {
+ options = buttStroke;
+ cutEnds = false;
+ }
+ else if (miterIndices is not null && Array.IndexOf(miterIndices, strokeIndex) >= 0)
+ {
+ options = miterStroke;
+ cutEnds = false;
+ }
+ else if (miterRoundIndices is not null && Array.IndexOf(miterRoundIndices, strokeIndex) >= 0)
+ {
+ options = miterRoundStroke;
+ cutEnds = false;
+ }
+
+ float effectiveWidth = design.StrokeWidthOverrides.TryGetValue((character, strokeIndex), out float overrideWidth)
+ ? overrideWidth
+ : characterWidth;
+
+ // A sub-quantum width offset keeps stroke outlines from sharing exactly coincident
+ // collinear edges, which the polygon clipper cannot union reliably; the deviation is far
+ // below the design grid and vanishes in quantization. Width zero marks a pre-computed
+ // filled ink polygon, carried through as drawn.
+ if (effectiveWidth != 0)
+ {
+ effectiveWidth += strokeIndex * 0.02f;
+ }
+
+ centerlines.Add(builder.Build());
+ strokeWidths.Add(effectiveWidth);
+ strokeStyles.Add(options);
+ cutFlags.Add(cutEnds);
+ }
+
+ // Butt-capped ink for every stroke, the probe geometry that tells a buried junction end from
+ // a true terminal.
+ IPath[] probeInk = new IPath[centerlines.Count];
+ if (design.CutTerminals)
+ {
+ for (int strokeIndex = 0; strokeIndex < centerlines.Count; strokeIndex++)
+ {
+ probeInk[strokeIndex] = strokeWidths[strokeIndex] == 0
+ ? centerlines[strokeIndex]
+ : centerlines[strokeIndex].GenerateOutline(strokeWidths[strokeIndex], buttStroke);
+ }
+ }
+
+ // Open-stroke endpoints, indexed by stroke, for junction detection at shared points.
+ List<(int Stroke, Vector2 Point)> strokeEndpoints = [];
+ for (int strokeIndex = 0; strokeIndex < centerlines.Count; strokeIndex++)
+ {
+ ISimplePath[] flat = [.. centerlines[strokeIndex].Flatten()];
+ if (flat.Length == 1 && !flat[0].IsClosed && strokeWidths[strokeIndex] != 0)
+ {
+ ReadOnlySpan pp = flat[0].Points.Span;
+
+ strokeEndpoints.Add((strokeIndex, pp[0]));
+ strokeEndpoints.Add((strokeIndex, pp[^1]));
+ }
+ }
+
+ List outlines = [];
+ for (int strokeIndex = 0; strokeIndex < centerlines.Count; strokeIndex++)
+ {
+ float effectiveWidth = strokeWidths[strokeIndex];
+ if (effectiveWidth == 0)
+ {
+ outlines.Add(centerlines[strokeIndex]);
+ continue;
+ }
+
+ ISimplePath[] flattened = [.. centerlines[strokeIndex].Flatten()];
+ if (!cutFlags[strokeIndex] || flattened.Length != 1 || flattened[0].IsClosed)
+ {
+ StrokeOptions closedOptions = cutFlags[strokeIndex] ? miterSquareStroke : strokeStyles[strokeIndex];
+
+ outlines.Add(centerlines[strokeIndex].GenerateOutline(effectiveWidth, closedOptions));
+ continue;
+ }
+
+ CutTerminalStroke(
+ flattened[0].Points.ToArray(),
+ centerlines[strokeIndex],
+ effectiveWidth,
+ strokeIndex,
+ probeInk,
+ strokeEndpoints,
+ buttStroke,
+ outlines);
+ }
+
+ // TrueType fills with the nonzero winding rule, so overlapping same-winding contours render
+ // identically to their union; the polygon clipper drops pieces from many-input unions, so the
+ // cut-terminal design writes its overlapping contours directly.
+ bool allInk = strokeWidths.TrueForAll(width => width == 0);
+ IPath merged = outlines.Count == 1
+ ? outlines[0]
+ : design.CutTerminals || allInk
+ ? new ComplexPolygon([.. outlines])
+ : outlines[0].Clip(BooleanOperation.Union, outlines.Skip(1));
+
+ List> contours = [];
+ foreach (ISimplePath simple in merged.Flatten())
+ {
+ ReadOnlySpan points = simple.Points.Span;
+ Vector2[] dense = new Vector2[points.Length];
+ for (int i = 0; i < points.Length; i++)
+ {
+ dense[i] = new Vector2(points[i].X, points[i].Y);
+ }
+
+ // The stroker flattens curves densely; most of those points are collinear within a
+ // fraction of a design unit and would bloat the font. 0.6 units is 1.5 microns of print.
+ designContours.Add(SimplifyContour(dense, 0.6f));
+ }
+
+ // Both axes take one scale, about the baseline and the centre of the character cell, so the
+ // glyph lands on the height its category stands at and keeps the shape it was drawn with.
+ float drawnTop = 0;
+ float drawnBottom = 0;
+ foreach (IReadOnlyList drawnContour in designContours)
+ {
+ foreach (Vector2 point in drawnContour)
+ {
+ drawnTop = MathF.Max(drawnTop, point.Y);
+ drawnBottom = MathF.Min(drawnBottom, point.Y);
+ }
+ }
+
+ float glyphScale = NormalizedScale(character, drawnTop, drawnBottom);
+ glyphScales[character] = glyphScale;
+ float cellCentre = design.W / 2;
+ foreach (Vector2[] designPoints in designContours.Cast())
+ {
+ for (int i = 0; i < designPoints.Length; i++)
+ {
+ Vector2 point = designPoints[i];
+ designPoints[i] = new Vector2(cellCentre + ((point.X - cellCentre) * glyphScale), point.Y * glyphScale);
+ inkTop = MathF.Max(inkTop, designPoints[i].Y);
+ inkBottom = MathF.Min(inkBottom, designPoints[i].Y);
+ }
+
+ List<(short X, short Y)> contour = new(designPoints.Length);
+ foreach (Vector2 point in designPoints)
+ {
+ (short X, short Y) quantized = ((short)MathF.Round((point.X + sideBearing) * emScale), (short)MathF.Round(point.Y * emScale));
+ if (contour.Count == 0 || contour[^1] != quantized)
+ {
+ contour.Add(quantized);
+ }
+ }
+
+ if (contour.Count > 2 && contour[0] == contour[^1])
+ {
+ contour.RemoveAt(contour.Count - 1);
+ }
+
+ if (contour.Count > 2)
+ {
+ contours.Add(contour);
+ }
+ }
+
+ quantizedGlyphs[character] = contours;
+ auditOutlines[character] = designContours;
+ SpecChecks.Verify(character, designContours, design.Expectations, specFailures);
+ }
+
+ // Text layout centers the em box inside the declared line height, so a glyph cell reaches
+ // ascender - (lineHeight - unitsPerEm) / 2 above the baseline. Ink above that line makes the
+ // layout lower that one glyph to keep it inside the cell, which breaks the shared baseline.
+ // Solving for a zero shift with a zero line gap gives the ascender below. This design needs it
+ // because the em carries the 0.1 inch pitch while the ink stands taller than the pitch.
+ float inkTopUnits = inkTop * emScale;
+ short descender = (short)MathF.Round(inkBottom * emScale);
+ short ascender = (short)MathF.Round(MathF.Max(inkTopUnits, (2 * inkTopUnits) - descender - design.UnitsPerEm));
+ TrueTypeWriter writer = new(
+ design.UnitsPerEm,
+ ascender,
+ descender,
+ (short)MathF.Round(capHeight * emScale),
+ design.FamilyName,
+ "Copyright (c) Six Labors. Licensed under the Six Labors Split License.");
+
+ writer.AddGlyph(' ', [], advanceWidth);
+ foreach ((char glyphCharacter, List> glyphContours) in quantizedGlyphs.OrderBy(entry => entry.Key))
+ {
+ writer.AddGlyph(glyphCharacter, glyphContours, advanceWidth);
+ }
+
+ if (specFailures.Count > 0)
+ {
+ Console.WriteLine($"{design.Name} SPEC CHECK FAILED: {specFailures.Count} deviations");
+ foreach (string failure in specFailures)
+ {
+ Console.WriteLine($" {failure}");
+ }
+ }
+ else
+ {
+ Console.WriteLine($"{design.Name} SPEC CHECK PASSED");
+ }
+
+ // Normalizing the glyphs moves every extreme, so the expectations that record accepted ink are
+ // rewritten from the normalized outlines. The probe points are not rewritten: each was chosen by
+ // eye to sit inside or outside a particular sweep, so it takes the same scale as the outline it
+ // probes and keeps testing the same feature.
+ StringBuilder expectationRows = new();
+ foreach (char expectedCharacter in auditOutlines.Keys.OrderBy(character => character))
+ {
+ float expectedMinX = float.MaxValue;
+ float expectedMaxX = float.MinValue;
+ float expectedMinY = float.MaxValue;
+ float expectedMaxY = float.MinValue;
+ foreach (IReadOnlyList contour in auditOutlines[expectedCharacter])
+ {
+ foreach (Vector2 point in contour)
+ {
+ expectedMinX = MathF.Min(expectedMinX, point.X);
+ expectedMaxX = MathF.Max(expectedMaxX, point.X);
+ expectedMinY = MathF.Min(expectedMinY, point.Y);
+ expectedMaxY = MathF.Max(expectedMaxY, point.Y);
+ }
+ }
+
+ float expectedScale = glyphScales[expectedCharacter];
+ float expectedCentre = design.W / 2;
+ (float[] Bounds, float[][] Inked, float[][] Blank) old = design.Expectations[expectedCharacter];
+ string Probes(float[][] points) => $"[{string.Join(", ", points.Select(Probe))}]";
+ string Probe(float[] point) => $"[{Number(expectedCentre + ((point[0] - expectedCentre) * expectedScale))}, {Number(point[1] * expectedScale)}]";
+ string bounds = $"[{Number(expectedMinX)}, {Number(expectedMaxX)}, {Number(expectedMinY)}, {Number(expectedMaxY)}]";
+ string key = $" [{Literal(expectedCharacter)}] = (";
+ string row = $"{key}{bounds}, {Probes(old.Inked)}, {Probes(old.Blank)}),";
+
+ // The .editorconfig limits lines to 160 characters. A longer row puts each tuple element on its own line.
+ if (row.Length <= 160)
+ {
+ expectationRows.AppendLine(row);
+ }
+ else
+ {
+ expectationRows.AppendLine(key);
+ expectationRows.AppendLine(CultureInfo.InvariantCulture, $" {bounds},");
+ expectationRows.AppendLine(CultureInfo.InvariantCulture, $" {Probes(old.Inked)},");
+ expectationRows.AppendLine(CultureInfo.InvariantCulture, $" {Probes(old.Blank)}),");
+ }
+ }
+
+ string expectationPath = System.IO.Path.Combine(outputDirectory, $"{design.Name}-expectations.txt");
+ File.WriteAllText(expectationPath, expectationRows.ToString());
+ Console.WriteLine(expectationPath);
+
+ static string Number(float value)
+ {
+ float rounded = MathF.Round(value, 1);
+ return rounded == MathF.Round(rounded)
+ ? ((int)rounded).ToString(CultureInfo.InvariantCulture)
+ : rounded.ToString("0.0#", CultureInfo.InvariantCulture) + "f";
+ }
+
+ static string Literal(char value) => value switch
+ {
+ '\'' => @"'\''",
+ '\\' => @"'\\'",
+ _ => $"'{value}'",
+ };
+
+ static GlyphClass Classify(char value) => value switch
+ {
+ // ECMA-11 section 10 and FIPS PUB 32 section 2.6 dimension these in millimetres rather than
+ // drawing them, so they carry their own absolute size.
+ '―' or '∣' or '█' or '|' => GlyphClass.Absolute,
+ >= '0' and <= '9' => GlyphClass.Digit,
+ 'b' or 'd' or 'f' or 'h' or 'i' or 'k' or 'l' or 't' => GlyphClass.Ascender,
+ 'g' or 'j' or 'p' or 'q' or 'y' => GlyphClass.Descender,
+ _ when char.IsLower(value) => GlyphClass.SmallLetter,
+ _ => GlyphClass.Capital,
+ };
+
+ byte[] font = writer.Write();
+ string fontPath = System.IO.Path.Combine(outputDirectory, $"{design.Name}.ttf");
+ File.WriteAllBytes(fontPath, font);
+ Console.WriteLine($"{fontPath}: {font.Length} bytes");
+
+ // Emit the font bytes as C# data for embedding in the library.
+ StringBuilder data = new();
+ data.AppendLine("// Copyright (c) Six Labors.");
+ data.AppendLine("// Licensed under the Six Labors Split License.");
+ data.AppendLine();
+ data.AppendLine("// ");
+ data.AppendLine("// Generated by ImageSharp.Drawing.FontGenerator. Do not edit.");
+ data.AppendLine("// ");
+ data.AppendLine();
+ data.AppendLine("namespace SixLabors.ImageSharp.Drawing.Barcodes;");
+ data.AppendLine();
+ data.AppendLine("/// ");
+ data.AppendLine(CultureInfo.InvariantCulture, $"/// The {design.FamilyName} TrueType font file.");
+ data.AppendLine(CultureInfo.InvariantCulture, $"/// {design.DataSummary}");
+ data.AppendLine("/// ");
+ data.AppendLine(CultureInfo.InvariantCulture, $"internal static class {design.Name}FontData");
+ data.AppendLine("{");
+ data.AppendLine(" /// ");
+ data.AppendLine(" /// Gets the font file bytes.");
+ data.AppendLine(" /// ");
+ data.AppendLine(" public static ReadOnlySpan Bytes =>");
+ data.AppendLine(" [");
+ for (int byteIndex = 0; byteIndex < font.Length; byteIndex += 16)
+ {
+ int lineEnd = Math.Min(byteIndex + 16, font.Length);
+ data.Append(" ");
+ for (int lineByte = byteIndex; lineByte < lineEnd; lineByte++)
+ {
+ data.Append("0x");
+ data.Append(font[lineByte].ToString("X2", CultureInfo.InvariantCulture));
+ data.Append(',');
+ if (lineByte + 1 < lineEnd)
+ {
+ data.Append(' ');
+ }
+ }
+
+ data.AppendLine();
+ }
+
+ data.AppendLine(" ];");
+ data.AppendLine("}");
+ string dataPath = System.IO.Path.Combine(libraryFontDirectory, $"{design.Name}FontData.cs");
+ File.WriteAllText(dataPath, data.ToString());
+ Console.WriteLine(dataPath);
+
+ // Proof sheet: load the generated font back and render the full set with the library itself.
+ using MemoryStream stream = new(font);
+ FontFamily family = collection.Add(stream);
+ families[design.Name] = family;
+ Font proofFont = family.CreateFont(100);
+
+ string[] lines =
+ [
+ .. design.Skeletons.Keys
+ .OrderBy(character => character)
+ .Select((character, index) => (character, index))
+ .GroupBy(entry => entry.index / 17)
+ .Select(group => new string([.. group.Select(entry => entry.character)])),
+ ];
+
+ using Image proof = new(1800, 20 + (lines.Length * 160), Color.White.ToPixel());
+ proof.Mutate(context => context.Paint(canvas =>
+ {
+ for (int i = 0; i < lines.Length; i++)
+ {
+ canvas.DrawText(
+ new RichTextOptions(proofFont)
+ {
+ Origin = new PointF(20, 20 + (i * 160)),
+ },
+ lines[i],
+ Brushes.Solid(Color.Black),
+ null);
+ }
+ }));
+
+ string proofPath = System.IO.Path.Combine(outputDirectory, $"{design.Name}-proof.png");
+ proof.SaveAsPng(proofPath);
+ Console.WriteLine(proofPath);
+
+ // Large per-glyph grid for auditing each outline against its source drawing, every cell labelled
+ // with the character and code point it renders.
+ Font gridFont = family.CreateFont(200);
+ Font labelFont = SystemFonts.CreateFont("Arial", 18);
+ char[] auditCharacters = [.. design.Skeletons.Keys.OrderBy(character => character)];
+ int columns = 10;
+ int rows = (auditCharacters.Length + columns - 1) / columns;
+
+ using Image grid = new(columns * 240, rows * 360, Color.White.ToPixel());
+ grid.Mutate(context => context.Paint(canvas =>
+ {
+ for (int index = 0; index < auditCharacters.Length; index++)
+ {
+ int column = index % columns;
+ int row = index / columns;
+ canvas.DrawText(
+ new RichTextOptions(gridFont)
+ {
+ Origin = new PointF((column * 240) + 20, (row * 360) + 56),
+ },
+ auditCharacters[index].ToString(),
+ Brushes.Solid(Color.Black),
+ null);
+ canvas.DrawText(
+ new RichTextOptions(labelFont)
+ {
+ Origin = new PointF((column * 240) + 20, (row * 360) + 8),
+ },
+ $"U+{(int)auditCharacters[index]:X4} {auditCharacters[index]}",
+ Brushes.Solid(Color.Gray),
+ null);
+ }
+ }));
+
+ string gridPath = System.IO.Path.Combine(outputDirectory, $"{design.Name}-grid.png");
+ grid.SaveAsPng(gridPath);
+ Console.WriteLine(gridPath);
+}
+
+// Reference comparison sheets: each glyph from a reference font on the left, ours on the right,
+// baseline aligned so metric differences between the faces do not read as glyph offsets.
+(string Name, string? Path)[] referenceJobs = [("OcrA", referencePath), ("OcrB", referenceBPath)];
+foreach ((string designName, string? jobPath) in referenceJobs)
+{
+ if (jobPath is null || !families.TryGetValue(designName, out FontFamily compareFamily))
+ {
+ continue;
+ }
+
+ FontDesign design = designs.First(entry => entry.Name == designName);
+ FontCollection referenceCollection = new();
+ Font referenceFont = referenceCollection.Add(jobPath).CreateFont(200);
+ Font compareFont = compareFamily.CreateFont(200);
+ char[] compareCharacters = [.. design.Skeletons.Keys.OrderBy(c => c)];
+ int pairColumns = 5;
+ int pairRows = (compareCharacters.Length + pairColumns - 1) / pairColumns;
+ using Image pairs = new(pairColumns * 520, pairRows * 320, Color.White.ToPixel());
+ pairs.Mutate(context => context.Paint(canvas =>
+ {
+ float referenceAscent = canvas.MeasureText(new RichTextOptions(referenceFont), "0").LineMetrics[0].Ascender;
+ float compareAscent = canvas.MeasureText(new RichTextOptions(compareFont), "0").LineMetrics[0].Ascender;
+ float baselineY = MathF.Max(referenceAscent, compareAscent) + 30;
+ for (int index = 0; index < compareCharacters.Length; index++)
+ {
+ int column = index % pairColumns;
+ int row = index / pairColumns;
+ string text = compareCharacters[index].ToString();
+ canvas.DrawText(
+ new RichTextOptions(referenceFont)
+ {
+ Origin = new PointF((column * 520) + 20, (row * 320) + baselineY - referenceAscent),
+ },
+ text,
+ Brushes.Solid(Color.ParseHex("007700")),
+ null);
+ canvas.DrawText(
+ new RichTextOptions(compareFont)
+ {
+ Origin = new PointF((column * 520) + 250, (row * 320) + baselineY - compareAscent),
+ },
+ text,
+ Brushes.Solid(Color.Black),
+ null);
+ }
+ }));
+
+ string referenceStem = System.IO.Path.GetFileNameWithoutExtension(jobPath).ToLowerInvariant();
+ string pairsPath = System.IO.Path.Combine(outputDirectory, $"{designName}-vs-{referenceStem}.png");
+ pairs.SaveAsPng(pairsPath);
+ Console.WriteLine(pairsPath);
+}
+
+// Full-size single glyph renders for inspection, requested as extra arguments.
+foreach ((string designName, FontFamily inspectFamily) in families)
+{
+ Font inspectFont = inspectFamily.CreateFont(700);
+ foreach (string text in inspectTexts)
+ {
+ using Image single = new(900, 1100, Color.White.ToPixel());
+ single.Mutate(context => context.Paint(canvas => canvas.DrawText(
+ new RichTextOptions(inspectFont)
+ {
+ Origin = new PointF(100, 100),
+ },
+ text,
+ Brushes.Solid(Color.Black),
+ null)));
+
+ string singlePath = System.IO.Path.Combine(outputDirectory, $"inspect-{designName}-{(int)text[0]}.png");
+ single.SaveAsPng(singlePath);
+ Console.WriteLine(singlePath);
+ }
+}
+
+return 0;
+
+// Adds one stroke's ink to the piece list with its terminals cut on axis-aligned planes, the way the
+// drawings print them: feet sit exactly on the baseline and diagonal arms end on vertical cuts. The
+// stroke takes butt caps, then every open end whose projection is not buried inside another stroke
+// gains a filled patch running from its butt face out to the cut plane, half a stroke width from the
+// endpoint. A buried end stays butt so nothing pokes through the partner stroke. probeInk carries
+// butt-capped ink for every stroke of the glyph, the geometry a terminal is tested against, and
+// strokeEndpoints every open endpoint paired with its stroke index.
+static void CutTerminalStroke(
+ PointF[] points,
+ IPath centerline,
+ float width,
+ int strokeIndex,
+ IPath[] probeInk,
+ List<(int Stroke, Vector2 Point)> strokeEndpoints,
+ StrokeOptions buttStroke,
+ List pieces)
+{
+ float half = width / 2;
+ IPath body = centerline.GenerateOutline(width, buttStroke);
+
+ pieces.Add(body);
+
+ // Nonzero fill cancels where windings oppose, so every patch must turn the same way as the
+ // stroke outline it overlaps.
+ float bodyWinding = 0;
+ foreach (ISimplePath simple in body.Flatten())
+ {
+ float area = SignedArea(simple.Points.Span);
+ if (MathF.Abs(area) > MathF.Abs(bodyWinding))
+ {
+ bodyWinding = area;
+ }
+ }
+
+ Vector2[] pts = [.. points.Select(p => (Vector2)p)];
+ List<(bool Horizontal, float Plane, Vector2 P, Vector2 Tangent)> cutsWanted = [];
+ for (int end = 0; end < 2; end++)
+ {
+ Vector2 p = end == 0 ? pts[0] : pts[^1];
+ Vector2 inner = p;
+ for (int i = 1; i < pts.Length; i++)
+ {
+ inner = end == 0 ? pts[i] : pts[^(i + 1)];
+ if ((inner - p).LengthSquared() > 0.01F)
+ {
+ break;
+ }
+ }
+
+ Vector2 tangent = Vector2.Normalize(p - inner);
+ Vector2 probe = p + (tangent * (half / 2));
+ bool buried = false;
+ for (int other = 0; other < probeInk.Length; other++)
+ {
+ if (other != strokeIndex && probeInk[other].Contains(probe, IntersectionRule.NonZero, Vector2.One))
+ {
+ buried = true;
+ break;
+ }
+ }
+
+ if (buried)
+ {
+ // Two butt faces meeting at a shared junction point leave an unfilled wedge, so such
+ // ends run a short way past the point. An end buried in a partner's body stays exactly
+ // butt: any overrun there pokes into the partner's counter.
+ bool meetsAnotherEnd = false;
+ foreach ((int otherStroke, Vector2 otherPoint) in strokeEndpoints)
+ {
+ if (otherStroke != strokeIndex && (otherPoint - p).Length() < half)
+ {
+ meetsAnotherEnd = true;
+ break;
+ }
+ }
+
+ if (meetsAnotherEnd)
+ {
+ Vector2 tucked = p + (tangent * (half * 0.6F));
+ if (end == 0)
+ {
+ pts[0] = tucked;
+ }
+ else
+ {
+ pts[^1] = tucked;
+ }
+ }
+
+ continue;
+ }
+
+ // Two open ends meeting tip-to-tip form a point, as in the curly bracket middles: both run
+ // out along their tangents and the union forms the point, with no cut plane.
+ bool meetsEnd = false;
+ foreach ((int otherStroke, Vector2 otherPoint) in strokeEndpoints)
+ {
+ if (otherStroke != strokeIndex && (otherPoint - p).Length() < width)
+ {
+ meetsEnd = true;
+ break;
+ }
+ }
+
+ if (meetsEnd)
+ {
+ Vector2 pointward = p + (tangent * (half * 0.9F));
+ if (end == 0)
+ {
+ pts[0] = pointward;
+ }
+ else
+ {
+ pts[^1] = pointward;
+ }
+
+ continue;
+ }
+
+ // A curved terminal ends on a face perpendicular to the stroke, extended half a stroke
+ // width; only straight-segment terminals take the axis-aligned cuts of the drawings.
+ Vector2 back = p;
+ float walked = 0;
+ int step = 1;
+ while (step < pts.Length && walked < width * 1.2F)
+ {
+ Vector2 next = end == 0 ? pts[step] : pts[^(step + 1)];
+ walked += (next - back).Length();
+ back = next;
+ step++;
+ }
+
+ Vector2 chord = Vector2.Normalize(p - back);
+ bool curvedEnd = Vector2.Dot(chord, tangent) < 0.985F;
+ if (curvedEnd)
+ {
+ Vector2 extended = p + (tangent * (half * 0.9F));
+ if (end == 0)
+ {
+ pts[0] = extended;
+ }
+ else
+ {
+ pts[^1] = extended;
+ }
+
+ continue;
+ }
+
+ // The drawings cut a straight terminal on the reference line its ink abuts: the baseline,
+ // the cap line or the x-height line for steep strokes, a vertical side plane otherwise.
+ bool meetsLine = MathF.Abs(tangent.Y) >= 0.55F
+ && (p.Y - half <= 35 || p.Y + half >= 905 || MathF.Abs(p.Y + half - 741) <= 40);
+ bool horizontalCut = meetsLine || MathF.Abs(tangent.X) < 0.5F * MathF.Abs(tangent.Y);
+ float plane = horizontalCut
+ ? p.Y + (half * MathF.Sign(tangent.Y))
+ : p.X + (half * MathF.Sign(tangent.X));
+ cutsWanted.Add((horizontalCut, plane, p, tangent));
+ }
+
+ // Both arms of a chevron cut on the same axis toward the same side end flush on a single line
+ // in the drawings, so their planes unify to the outermost one.
+ if (cutsWanted.Count == 2 && cutsWanted[0].Horizontal == cutsWanted[1].Horizontal
+ && MathF.Abs(cutsWanted[0].Plane - cutsWanted[1].Plane) <= half)
+ {
+ float d0 = cutsWanted[0].Horizontal ? cutsWanted[0].Tangent.Y : cutsWanted[0].Tangent.X;
+ float d1 = cutsWanted[1].Horizontal ? cutsWanted[1].Tangent.Y : cutsWanted[1].Tangent.X;
+ if (MathF.Sign(d0) == MathF.Sign(d1))
+ {
+ float unified = d0 > 0
+ ? MathF.Max(cutsWanted[0].Plane, cutsWanted[1].Plane)
+ : MathF.Min(cutsWanted[0].Plane, cutsWanted[1].Plane);
+
+ cutsWanted[0] = (cutsWanted[0].Horizontal, unified, cutsWanted[0].P, cutsWanted[0].Tangent);
+ cutsWanted[1] = (cutsWanted[1].Horizontal, unified, cutsWanted[1].P, cutsWanted[1].Tangent);
+ }
+ }
+
+ foreach ((bool horizontalCut, float plane, Vector2 p, Vector2 tangent) in cutsWanted)
+ {
+ // The patch roots half a stroke inside the body and runs a hair narrower than the stroke:
+ // exactly coincident edges are the one geometry the polygon clipper cannot union reliably.
+ Vector2 normal = new(-tangent.Y, tangent.X);
+ Vector2 inside = p - (tangent * (half * 0.5F));
+ Vector2 side = normal * (half - 0.02F);
+ Vector2 c1 = inside + side;
+ Vector2 c2 = inside - side;
+ float t1 = horizontalCut ? (plane - c1.Y) / tangent.Y : (plane - c1.X) / tangent.X;
+ float t2 = horizontalCut ? (plane - c2.Y) / tangent.Y : (plane - c2.X) / tangent.X;
+ Vector2 c1Out = c1 + (tangent * Math.Clamp(t1, 0, half * 2.5F));
+ Vector2 c2Out = c2 + (tangent * Math.Clamp(t2, 0, half * 2.5F));
+
+ PointF[] quad = [(PointF)c1, (PointF)c1Out, (PointF)c2Out, (PointF)c2];
+ if (SignedArea(quad) * bodyWinding < 0)
+ {
+ Array.Reverse(quad);
+ }
+
+ pieces.Add(new Polygon(new LinearLineSegment(quad)));
+ }
+}
+
+// Returns the signed area of a closed point loop. The sign carries the winding direction, which
+// nonzero fill depends on: a patch must turn the same way as the outline it overlaps.
+static float SignedArea(ReadOnlySpan points)
+{
+ float area = 0;
+ for (int i = 0; i < points.Length; i++)
+ {
+ PointF a = points[i];
+ PointF b = points[(i + 1) % points.Length];
+ area += (a.X * b.Y) - (b.X * a.Y);
+ }
+
+ return area / 2;
+}
+
+// Converts one skeleton stroke into path segments. The token stream is a leading point, then line
+// points, arcs (NaN, radius, end), corners (NaN, NaN, radius, vertex), splines
+// (NaN, NaN, NaN, count, then count points) and cubic Bezier segments
+// (NaN, NaN, NaN, NaN, control1, control2, end). Corners resolve against the neighbouring anchor
+// points: a positive radius rounds the corner tangentially, a negative radius produces an arc through
+// the vertex itself so a dimensioned pointed extreme keeps its position. A spline runs a
+// tangent-continuous curve from the current point through every listed anchor, and cubic segments
+// carry exact control points, the form the OCR-B outlines use.
+static void BuildStroke(PathBuilder builder, float[] stroke)
+{
+ List<(int Kind, float Radius, Vector2 Point, Vector2[]? Anchors)> elements = [];
+ int i = 0;
+ while (i < stroke.Length)
+ {
+ if (float.IsNaN(stroke[i]))
+ {
+ if (float.IsNaN(stroke[i + 1]) && float.IsNaN(stroke[i + 2]) && float.IsNaN(stroke[i + 3]))
+ {
+ Vector2[] controls =
+ [
+ new Vector2(stroke[i + 4], stroke[i + 5]),
+ new Vector2(stroke[i + 6], stroke[i + 7]),
+ ];
+ elements.Add((4, 0, new Vector2(stroke[i + 8], stroke[i + 9]), controls));
+ i += 10;
+ }
+ else if (float.IsNaN(stroke[i + 1]) && float.IsNaN(stroke[i + 2]))
+ {
+ int count = (int)stroke[i + 3];
+ Vector2[] anchors = new Vector2[count];
+ for (int a = 0; a < count; a++)
+ {
+ anchors[a] = new Vector2(stroke[i + 4 + (a * 2)], stroke[i + 5 + (a * 2)]);
+ }
+
+ elements.Add((3, 0, anchors[^1], anchors));
+ i += 4 + (count * 2);
+ }
+ else if (float.IsNaN(stroke[i + 1]))
+ {
+ elements.Add((2, stroke[i + 2], new Vector2(stroke[i + 3], stroke[i + 4]), null));
+ i += 5;
+ }
+ else
+ {
+ elements.Add((1, stroke[i + 1], new Vector2(stroke[i + 2], stroke[i + 3]), null));
+ i += 4;
+ }
+ }
+ else
+ {
+ elements.Add((0, 0, new Vector2(stroke[i], stroke[i + 1]), null));
+ i += 2;
+ }
+ }
+
+ bool closed = elements.Count > 2 && elements[0].Kind == 0 && elements[^1].Kind != 2 &&
+ elements[0].Point == elements[^1].Point;
+
+ Vector2 current = elements[0].Point;
+ builder.MoveTo(current);
+ for (int k = 1; k < elements.Count; k++)
+ {
+ (int kind, float radius, Vector2 point, Vector2[]? anchors) = elements[k];
+ if (kind == 0)
+ {
+ builder.LineTo(point);
+ current = point;
+ }
+ else if (kind == 1)
+ {
+ AddArc(builder, current, point, MathF.Abs(radius), radius > 0);
+ current = point;
+ }
+ else if (kind == 3)
+ {
+ AddSpline(builder, current, anchors!);
+ current = point;
+ }
+ else if (kind == 4)
+ {
+ AddCubic(builder, current, anchors![0], anchors[1], point);
+ current = point;
+ }
+ else
+ {
+ Vector2 next = elements[k + 1].Point;
+ Vector2 u1 = Vector2.Normalize(point - current);
+ Vector2 u2 = Vector2.Normalize(next - point);
+ float cross = (u1.X * u2.Y) - (u1.Y * u2.X);
+ float dot = Vector2.Dot(u1, u2);
+ float r = MathF.Abs(radius);
+
+ float tangent = r * MathF.Abs(cross) / (1 + dot);
+ Vector2 start = point - (u1 * tangent);
+ Vector2 end = point + (u2 * tangent);
+
+ builder.LineTo(start);
+ AddArc(builder, start, end, r, cross > 0);
+ current = end;
+ }
+ }
+
+ if (closed)
+ {
+ builder.CloseFigure();
+ }
+}
+
+// Samples a C2 cubic spline from the current point through the anchors, parameterized by chord
+// length: a natural spline for open runs, which gives zero end curvature, and a periodic spline when
+// the curve closes on its start. The anchor sets were authored for smooth interpolation, so a merely
+// tangent-continuous curve through them reads as lumpy, and C2 continuity restores the drawn shape.
+static void AddSpline(PathBuilder builder, Vector2 current, Vector2[] anchors)
+{
+ Vector2[] points = new Vector2[anchors.Length + 1];
+ points[0] = current;
+ anchors.CopyTo(points, 1);
+ bool cyclic = points[0] == points[^1] && points.Length > 3;
+ int knotCount = cyclic ? points.Length - 1 : points.Length;
+
+ float[] chords = new float[points.Length - 1];
+ for (int i = 0; i < chords.Length; i++)
+ {
+ chords[i] = MathF.Max(Vector2.Distance(points[i], points[i + 1]), 1e-3f);
+ }
+
+ // Solve for the second derivative M at every knot, per coordinate, with a dense Gaussian solve:
+ // the systems are tiny. Row i couples M[i-1], M[i], M[i+1] with the standard cubic spline
+ // continuity equation; open ends pin M to zero, cyclic rows wrap.
+ Vector2[] m = new Vector2[knotCount];
+ int unknowns = cyclic ? knotCount : knotCount - 2;
+ if (unknowns > 0)
+ {
+ float[,] matrix = new float[unknowns, unknowns];
+ Vector2[] rhs = new Vector2[unknowns];
+ for (int row = 0; row < unknowns; row++)
+ {
+ int i = cyclic ? row : row + 1;
+ float hPrev = chords[cyclic ? ((i - 1) + chords.Length) % chords.Length : i - 1];
+ float hNext = chords[i % chords.Length];
+ Vector2 prev = points[cyclic ? ((i - 1) + knotCount) % knotCount : i - 1];
+ Vector2 here = points[i];
+ Vector2 next = points[cyclic ? (i + 1) % knotCount : i + 1];
+
+ void Add(int knot, float value)
+ {
+ int column = cyclic ? ((knot % knotCount) + knotCount) % knotCount : knot - 1;
+ if (column >= 0 && column < unknowns)
+ {
+ matrix[row, column] += value;
+ }
+ }
+
+ Add(i - 1, hPrev);
+ Add(i, 2 * (hPrev + hNext));
+ Add(i + 1, hNext);
+ rhs[row] = 6 * (((next - here) / hNext) - ((here - prev) / hPrev));
+ }
+
+ for (int pivot = 0; pivot < unknowns; pivot++)
+ {
+ int best = pivot;
+ for (int row = pivot + 1; row < unknowns; row++)
+ {
+ if (MathF.Abs(matrix[row, pivot]) > MathF.Abs(matrix[best, pivot]))
+ {
+ best = row;
+ }
+ }
+
+ if (best != pivot)
+ {
+ for (int column = 0; column < unknowns; column++)
+ {
+ (matrix[pivot, column], matrix[best, column]) = (matrix[best, column], matrix[pivot, column]);
+ }
+
+ (rhs[pivot], rhs[best]) = (rhs[best], rhs[pivot]);
+ }
+
+ float diagonal = matrix[pivot, pivot];
+ for (int row = pivot + 1; row < unknowns; row++)
+ {
+ float factor = matrix[row, pivot] / diagonal;
+ if (factor == 0)
+ {
+ continue;
+ }
+
+ for (int column = pivot; column < unknowns; column++)
+ {
+ matrix[row, column] -= factor * matrix[pivot, column];
+ }
+
+ rhs[row] -= factor * rhs[pivot];
+ }
+ }
+
+ Vector2[] solution = new Vector2[unknowns];
+ for (int row = unknowns - 1; row >= 0; row--)
+ {
+ Vector2 sum = rhs[row];
+ for (int column = row + 1; column < unknowns; column++)
+ {
+ sum -= matrix[row, column] * solution[column];
+ }
+
+ solution[row] = sum / matrix[row, row];
+ }
+
+ for (int row = 0; row < unknowns; row++)
+ {
+ m[cyclic ? row : row + 1] = solution[row];
+ }
+ }
+
+ for (int segment = 0; segment < points.Length - 1; segment++)
+ {
+ float h = chords[segment];
+ Vector2 p1 = points[segment];
+ Vector2 p2 = points[segment + 1];
+ Vector2 m1 = m[segment % knotCount];
+ Vector2 m2 = m[(segment + 1) % knotCount];
+ int steps = Math.Max(4, (int)MathF.Ceiling(h / 8));
+ for (int step = 1; step <= steps; step++)
+ {
+ float t = h * step / steps;
+ float a = (h - t) / h;
+ float b = t / h;
+ Vector2 value = (a * p1) + (b * p2) +
+ ((h * h / 6) * ((((a * a * a) - a) * m1) + (((b * b * b) - b) * m2)));
+ builder.LineTo(value);
+ }
+ }
+}
+
+// Removes flattening points that deviate from a straight run by less than the tolerance, keeping the
+// outline within a fraction of a design unit of the dense curve.
+static Vector2[] SimplifyContour(Vector2[] points, float tolerance)
+{
+ if (points.Length < 4)
+ {
+ return points;
+ }
+
+ bool[] keep = new bool[points.Length];
+ keep[0] = keep[^1] = true;
+ Simplify(0, points.Length - 1);
+ return [.. points.Where((_, index) => keep[index])];
+
+ void Simplify(int first, int last)
+ {
+ if (last <= first + 1)
+ {
+ return;
+ }
+
+ Vector2 span = points[last] - points[first];
+ float norm = span.Length();
+ float worst = -1;
+ int peak = first;
+ for (int i = first + 1; i < last; i++)
+ {
+ Vector2 offset = points[i] - points[first];
+ float distance = norm < 1e-6f
+ ? offset.Length()
+ : MathF.Abs((span.X * offset.Y) - (span.Y * offset.X)) / norm;
+ if (distance > worst)
+ {
+ worst = distance;
+ peak = i;
+ }
+ }
+
+ if (worst > tolerance)
+ {
+ keep[peak] = true;
+ Simplify(first, peak);
+ Simplify(peak, last);
+ }
+ }
+}
+
+// Samples one cubic Bezier segment with exact control points.
+static void AddCubic(PathBuilder builder, Vector2 from, Vector2 control1, Vector2 control2, Vector2 to)
+{
+ float chord = Vector2.Distance(from, to) +
+ Vector2.Distance(from, control1) + Vector2.Distance(control2, to);
+ int steps = Math.Max(4, (int)MathF.Ceiling(chord / 8));
+ for (int step = 1; step <= steps; step++)
+ {
+ float t = (float)step / steps;
+ float u = 1 - t;
+ builder.LineTo(
+ (u * u * u * from) + (3 * u * u * t * control1) + (3 * u * t * t * control2) + (t * t * t * to));
+ }
+}
+
+// Samples a circular arc of the given radius between two points. The centre sits on whichever side of
+// the chord the requested direction demands, so the arc keeps both endpoints exactly.
+static void AddArc(PathBuilder builder, Vector2 from, Vector2 to, float radius, bool counterClockwise)
+{
+ Vector2 chord = to - from;
+ float chordLength = chord.Length();
+ Vector2 mid = (from + to) / 2;
+ float height = MathF.Sqrt(MathF.Max(0, (radius * radius) - (chordLength * chordLength / 4)));
+ Vector2 normal = Vector2.Normalize(new Vector2(-chord.Y, chord.X));
+ Vector2 center = counterClockwise ? mid + (normal * height) : mid - (normal * height);
+
+ float startAngle = MathF.Atan2(from.Y - center.Y, from.X - center.X);
+ float endAngle = MathF.Atan2(to.Y - center.Y, to.X - center.X);
+ float sweep = endAngle - startAngle;
+ if (counterClockwise && sweep < 0)
+ {
+ sweep += 2 * MathF.PI;
+ }
+ else if (!counterClockwise && sweep > 0)
+ {
+ sweep -= 2 * MathF.PI;
+ }
+
+ int steps = Math.Max(4, (int)MathF.Ceiling(MathF.Abs(sweep) / (2 * MathF.Sqrt(0.2f / radius))));
+ for (int step = 1; step <= steps; step++)
+ {
+ float angle = startAngle + (sweep * step / steps);
+ builder.LineTo(center + (radius * new Vector2(MathF.Cos(angle), MathF.Sin(angle))));
+ }
+}
diff --git a/src/ImageSharp.Drawing.FontGenerator/RepositoryPaths.cs b/src/ImageSharp.Drawing.FontGenerator/RepositoryPaths.cs
new file mode 100644
index 00000000..5fd8f57d
--- /dev/null
+++ b/src/ImageSharp.Drawing.FontGenerator/RepositoryPaths.cs
@@ -0,0 +1,53 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Drawing.FontGenerator;
+
+///
+/// Resolves the directories the generator writes to. The generator takes no output path: the font data
+/// is source, so it is written where the library compiles it from, and the sheets are written to the
+/// repository artifacts directory. Both are found by walking up from the running assembly to the
+/// solution file.
+///
+internal static class RepositoryPaths
+{
+ private const string SolutionFileName = "ImageSharp.Drawing.slnx";
+ private const string LibraryFontsRelativePath = @"src\ImageSharp.Drawing\Barcodes\Fonts";
+ private const string ArtifactsRelativePath = @"artifacts\fonts";
+
+ private static readonly Lazy SolutionDirectoryFullPathLazy = new(GetSolutionDirectoryFullPathImpl);
+
+ ///
+ /// Gets the directory the library compiles the generated font data from.
+ ///
+ public static string LibraryFonts => GetFullPath(LibraryFontsRelativePath);
+
+ ///
+ /// Gets the directory the fonts and the proof, grid and comparison sheets are written to.
+ ///
+ public static string Artifacts => GetFullPath(ArtifactsRelativePath);
+
+ private static string SolutionDirectoryFullPath => SolutionDirectoryFullPathLazy.Value;
+
+ private static string GetSolutionDirectoryFullPathImpl()
+ {
+ string assemblyLocation = AppContext.BaseDirectory;
+ DirectoryInfo? directory = new FileInfo(assemblyLocation).Directory
+ ?? throw new IOException($"Unable to find the SixLabors solution directory from {assemblyLocation}!");
+
+ while (!directory.EnumerateFiles(SolutionFileName).Any())
+ {
+ directory = directory.Parent;
+ if (directory is null)
+ {
+ throw new IOException($"Unable to find the SixLabors solution directory from {assemblyLocation}!");
+ }
+ }
+
+ return directory.FullName;
+ }
+
+ private static string GetFullPath(string relativePath)
+ => System.IO.Path.Combine(SolutionDirectoryFullPath, relativePath)
+ .Replace('\\', System.IO.Path.DirectorySeparatorChar);
+}
diff --git a/src/ImageSharp.Drawing.FontGenerator/SpecChecks.cs b/src/ImageSharp.Drawing.FontGenerator/SpecChecks.cs
new file mode 100644
index 00000000..6510de03
--- /dev/null
+++ b/src/ImageSharp.Drawing.FontGenerator/SpecChecks.cs
@@ -0,0 +1,482 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Numerics;
+
+namespace SixLabors.ImageSharp.Drawing.FontGenerator;
+
+///
+/// The regression gate for the generated outlines, run on every build. Each glyph asserts its exact
+/// ink bounding box (the drawings dimension every extreme: the reference lines plus the half-stroke
+/// cap projection, and per-glyph values such as the -13.8 tail of the OCR-A t) plus probe points that
+/// must be inside or outside the ink, catching arc sweep and construction errors the bounding box
+/// cannot see. A failure means an edit moved accepted ink: an extreme drifted, a counter filled in, or
+/// a stroke vanished. The expectations were transcribed from the same drawings as the skeletons, so
+/// they cannot catch a misreading of the drawings themselves; full-size visual review against the
+/// figures remains the acceptance gate for shape.
+///
+internal static class SpecChecks
+{
+ ///
+ /// The permitted difference in design units between an expected and a measured ink extreme. Arc
+ /// flattening and the sub-quantum stroke width perturbation that keeps union edges apart move the
+ /// measured extremes by fractions of a unit; the drawings dimension nothing finer than whole units.
+ ///
+ private const float BoundsTolerance = 4;
+
+ ///
+ /// Gets the OCR-B per-glyph expectations, in the format of . ECMA-11 prints
+ /// OCR-B as a character sheet rather than as dimensioned drawings, so unlike the OCR-A rows these
+ /// values are not transcribed numbers: they record the ink of the outlines that passed review
+ /// against that sheet. They therefore catch an edit that moves accepted ink, which is what a
+ /// regression gate is for, but they cannot judge whether a shape matches the standard. That
+ /// judgement stays with the printed sheet and the generated proof and grid images.
+ ///
+ public static IReadOnlyDictionary OcrB { get; } =
+ new Dictionary
+ {
+ ['!'] = ([196.6f, 364.4f, 5.5f, 968.5f], [[286, 891.8f], [223.3f, 30.9f], [320.4f, 456.7f], [242.1f, 669.5f]], []),
+ ['"'] = ([24.5f, 525.5f, 595.7f, 968.5f], [[96.1f, 896.8f], [460.4f, 621.9f], [415.7f, 941.5f], [51.4f, 621.9f]], []),
+ ['#'] = (
+ [-11.1f, 561.1f, 0, 968.5f],
+ [[96.1f, 306.8f], [527.1f, 940], [165.8f, 721.2f], [503.9f, 336.6f]],
+ [[278.5f, 499], [195.6f, 389.6f], [311.6f, 389.6f]]),
+ ['$'] = (
+ [-34.2f, 584.2f, 0, 968.5f],
+ [[269.9f, 129.1f], [300.2f, 940.4f], [81.3f, 543.2f], [468.5f, 455.6f]],
+ [[158.8f, 708.1f], [377.6f, 223.4f], [354, 371.5f]]),
+ ['%'] = (
+ [-17.2f, 573.9f, -12, 968.5f],
+ [[332.7f, 568.1f], [18.1f, 26.9f], [535.6f, 87.8f], [55.3f, 899.6f]],
+ [[123, 815], [440.9f, 104.7f], [427.4f, 202.8f]]),
+ ['&'] = (
+ [-34.4f, 574.4f, -11.8f, 968.5f],
+ [[174.3f, 489.1f], [523.2f, 25], [296.1f, 936.7f], [85.4f, 57.9f]],
+ [[220.4f, 745.8f], [276.3f, 130.3f], [161.1f, 357.4f]]),
+ ['\''] = ([185.8f, 364.2f, 517.1f, 968.5f], [[276.4f, 884.8f], [236.9f, 542.6f], [322.5f, 707.1f], [223.8f, 776.2f]], []),
+ ['('] = ([91.2f, 442.1f, 0, 968.5f], [[159.3f, 542.8f], [416, 25.9f], [416, 939.6f], [186, 232.7f]], []),
+ [')'] = ([104.5f, 452.1f, 0, 968.5f], [[386.5f, 536.1f], [133.1f, 25.9f], [133.1f, 939.6f], [359.9f, 232.7f]], []),
+ ['*'] = ([-69.2f, 619.2f, 129, 817.5f], [[271.7f, 457.6f], [312.3f, 786], [493.1f, 217.8f], [120.4f, 173.5f]], []),
+ ['+'] = ([-37, 587, 113.8f, 863.9f], [[274.2f, 507.4f], [241, 142.1f], [241, 832.1f], [558.3f, 555.4f]], []),
+ [','] = ([24.2f, 538.7f, -249.2f, 254.9f], [[365.2f, 135], [77.4f, -219.2f], [280.3f, -90], [509.1f, 223.6f]], []),
+ ['-'] = ([-41.1f, 591.1f, 502.3f, 681.9f], [[48.9f, 591.2f], [561.8f, 650.2f], [314.6f, 532.1f], [200.2f, 650.2f]], []),
+ ['.'] = ([131.5f, 418.6f, 0, 248.4f], [[255.1f, 124.9f], [387.9f, 29], [387.9f, 217.1f], [162.8f, 29]], []),
+ ['/'] = ([32.4f, 522.5f, 0, 968.5f], [[435.4f, 902.2f], [72.5f, 27.4f], [292.8f, 448.6f], [322, 688.3f]], []),
+ ['0'] = (
+ [-34.5f, 591.2f, -11.8f, 968.5f],
+ [[31.5f, 568.6f], [467.6f, 57.5f], [515.3f, 841.2f], [34.9f, 132.5f]],
+ [[276.8f, 623.1f], [300.6f, 129], [126.9f, 367.6f]]),
+ ['1'] = ([92.7f, 441, 0, 968.5f], [[356.8f, 881.6f], [414.7f, 26.8f], [343.2f, 452.5f], [121.8f, 673.8f]], []),
+ ['2'] = ([-2.2f, 542, 0, 968.5f], [[70, 78.4f], [378.4f, 919.7f], [348.2f, 453.8f], [505.8f, 28.2f]], []),
+ ['3'] = ([-13.6f, 570.4f, -11.7f, 968.5f], [[446.1f, 878.6f], [15.2f, 64.2f], [544.5f, 305.1f], [164.5f, 535.9f]], []),
+ ['4'] = ([-37.6f, 570.2f, 0, 968.5f], [[386.4f, 276.5f], [257.3f, 939.3f], [86.4f, 559.1f], [-7.8f, 241.6f]], []),
+ ['5'] = ([13.7f, 536.2f, -12, 968.5f], [[93.2f, 600.3f], [42.3f, 16.7f], [503.8f, 274.6f], [483.4f, 939.7f]], []),
+ ['6'] = (
+ [-35.4f, 585.3f, -12, 968.5f],
+ [[110.5f, 542.1f], [450.6f, 70.7f], [420.3f, 939.4f], [39.8f, 131.3f]],
+ [[275.5f, 309.8f], [289, 131.3f], [282.2f, 474.7f]]),
+ ['7'] = ([-28.8f, 581.9f, 0, 968.5f], [[496.6f, 885.1f], [131.6f, 26.9f], [-0.1f, 939.2f], [226.3f, 493.2f]], []),
+ ['8'] = (
+ [-35.1f, 578.4f, -11.8f, 968.5f],
+ [[310.3f, 532], [135.2f, 37], [539.2f, 171.7f], [152, 922.6f]],
+ [[269.9f, 272.7f], [290.1f, 818.2f], [259.8f, 629.7f]]),
+ ['9'] = (
+ [-34, 583.8f, 0, 968.5f],
+ [[434.2f, 423.8f], [122.4f, 903.2f], [119, 28.2f], [31.8f, 500.9f]],
+ [[276.6f, 655.1f], [206.2f, 806], [370.5f, 789.2f]]),
+ [':'] = ([135.3f, 409.8f, 0, 754.5f], [[259, 638.4f], [377, 29.6f], [166.7f, 199.3f], [340.1f, 199.3f]], []),
+ [';'] = ([16.5f, 525.2f, -251.5f, 728.3f], [[357.5f, 616.4f], [66, -221.1f], [420.2f, 125.7f], [176.7f, 22.4f]], []),
+ ['<'] = ([-38.3f, 565.2f, 92.7f, 968.5f], [[71.6f, 530.8f], [536.8f, 144.8f], [536.8f, 913.5f], [332.2f, 372.5f]], []),
+ ['='] = ([-33.1f, 583.1f, 274, 717.2f], [[32.1f, 652.9f], [552.4f, 302.4f], [434.3f, 686.1f], [172.3f, 302.4f]], []),
+ ['>'] = ([-8.2f, 588, 92.7f, 968.5f], [[477.3f, 529.4f], [20.2f, 913.6f], [20.2f, 145.2f], [218.9f, 373.8f]], []),
+ ['?'] = ([19.1f, 537.6f, 0, 968.5f], [[289.7f, 91.6f], [265.3f, 939.2f], [415.3f, 520.7f], [49.1f, 688.1f]], []),
+ ['@'] = (
+ [-36.6f, 586.7f, -12.9f, 968.5f],
+ [[289.6f, 462.5f], [127.3f, 916.8f], [73.2f, 33.4f], [524, 76.6f]],
+ [[163.4f, 350.7f], [145.4f, 116.3f], [221.1f, 228.1f]]),
+ ['A'] = (
+ [-70.6f, 620.6f, 0, 991.5f],
+ [[440.1f, 358.8f], [207.7f, 960.2f], [-28.5f, 30.4f], [108, 572.8f]],
+ [[274.1f, 499], [274.1f, 713], [248.3f, 606]]),
+ ['B'] = (
+ [-56.4f, 613, 0, 975],
+ [[26.2f, 508.1f], [542.8f, 142.9f], [528, 829.1f], [-25.4f, 28.5f]],
+ [[225.5f, 286.8f], [103.7f, 810.7f], [413.6f, 663.1f]]),
+ ['C'] = ([-6.7f, 548.9f, -13.3f, 1011], [[304.9f, 57], [286.4f, 979.4f], [24.5f, 514.5f], [65.1f, 223]], []),
+ ['D'] = (
+ [-34, 562.2f, 0, 979.5f],
+ [[45.2f, 85.3f], [-10.1f, 948.7f], [524.9f, 550.2f], [85.8f, 520.7f]],
+ [[266.6f, 491.2f], [148.5f, 170.2f], [148.5f, 808.5f]]),
+ ['E'] = ([-33.2f, 558.2f, 0, 991.1f], [[56.7f, 517.5f], [528.9f, 30.4f], [510.5f, 960.2f], [-2.4f, 30.4f]], []),
+ ['F'] = ([16.8f, 516.6f, 0, 999.4f], [[106.6f, 521.4f], [486.6f, 967.9f], [47.5f, 30.7f], [47.5f, 956.8f]], []),
+ ['G'] = ([-41.1f, 598.7f, -13.3f, 1011.2f], [[513.8f, 451.8f], [144.9f, 927.7f], [96.9f, 108.7f], [-10.1f, 525.6f]], []),
+ ['H'] = ([-39, 589, 0, 968.4f], [[497.9f, 505.5f], [-7.5f, 29.5f], [-7.5f, 937.1f], [560.7f, 29.5f]], []),
+ ['I'] = ([36.8f, 494.8f, 0, 991.1f], [[263.7f, 904.9f], [466.6f, 30.4f], [226.8f, 436.3f], [68.1f, 67.3f]], []),
+ ['J'] = ([44.8f, 530.4f, -13, 983.2f], [[459.5f, 910.7f], [238.1f, 21.5f], [500.1f, 427.3f], [75.8f, 290.8f]], []),
+ ['K'] = ([-86.7f, 648.5f, 0, 984.3f], [[4.1f, 513.5f], [579.7f, 30.2f], [550.1f, 952.5f], [-54.9f, 30.2f]], []),
+ ['L'] = ([-39.2f, 569.1f, 0, 979.8f], [[44.1f, 81.6f], [-7.5f, 948.7f], [538.5f, 30], [70, 517]], []),
+ ['M'] = ([-60.6f, 623.5f, 0, 972], [[280.6f, 498.3f], [594.2f, 29.7f], [-29.3f, 29.7f], [594.2f, 941]], []),
+ ['N'] = ([-43.1f, 593.1f, 0, 976.3f], [[483.1f, 162.6f], [-11.3f, 944.8f], [564.3f, 764], [-11.3f, 395.1f]], []),
+ ['O'] = (
+ [-60.2f, 617.5f, -13.2f, 1015.5f],
+ [[542.6f, 582.4f], [125.7f, 73.2f], [-18.2f, 637.7f], [299.1f, 984.6f]],
+ [[280.6f, 519.7f], [265.9f, 147], [262.2f, 859.1f]]),
+ ['P'] = (
+ [-63, 589.8f, 0, 980.1f],
+ [[27.2f, 469], [514.2f, 852.7f], [-31.9f, 948.7f], [-31.9f, 30]],
+ [[241.1f, 686.7f], [410.9f, 749.4f], [119.4f, 808.5f]]),
+ ['Q'] = (
+ [-88.8f, 658.3f, 0, 999.7f],
+ [[407.5f, 248.4f], [127, 949.4f], [-57.4f, 469.8f], [507.1f, 694.8f]],
+ [[230.4f, 639.5f], [237.7f, 204.1f], [381.6f, 425.5f]]),
+ ['R'] = (
+ [-62.2f, 603.6f, 0, 991.5f],
+ [[32.1f, 480.6f], [552.4f, 30.4f], [449.1f, 882.7f], [-30.6f, 960.2f]],
+ [[246.1f, 701.9f], [128.1f, 576.5f], [128.1f, 823.7f]]),
+ ['S'] = ([-26.7f, 569.3f, -13.4f, 1019.2f], [[59.3f, 184.1f], [384, 970], [516.8f, 383.3f], [44.6f, 649]], []),
+ ['T'] = ([-45.1f, 595.1f, 0, 1007.8f], [[273.9f, 917.9f], [233.3f, 28.7f], [314.5f, 471.4f], [565.4f, 976.9f]], []),
+ ['U'] = ([-63.7f, 600.8f, -13.3f, 1002.9f], [[12.4f, 927.5f], [407.2f, 38.3f], [569.5f, 643.4f], [-31.9f, 374]], []),
+ ['V'] = ([-69, 619.1f, 0, 976.3f], [[242.2f, 77.8f], [577.9f, 944.8f], [42.9f, 653.3f], [463.5f, 483.6f]], []),
+ ['W'] = ([-70.3f, 627.7f, 0, 988.2f], [[440.1f, 152], [-39.5f, 956.3f], [595.1f, 790.3f], [-6.3f, 428.7f]], []),
+ ['X'] = ([-77.2f, 608.7f, 0, 976.3f], [[271.4f, 546.3f], [-27.4f, 29.8f], [559.2f, 29.8f], [-23.8f, 944.8f]], []),
+ ['Y'] = ([-49.1f, 599.1f, 0, 988.2f], [[277.4f, 532], [0.7f, 956.3f], [550.4f, 956.3f], [236.8f, 30.3f]], []),
+ ['Z'] = ([-9, 558.9f, 0, 979.8f], [[84.7f, 89], [483.2f, 948.7f], [239.7f, 539.1f], [531.1f, 30]], []),
+ ['['] = ([50.6f, 488.2f, 0, 968.5f], [[122.5f, 897.3f], [462.6f, 26.5f], [78, 379.3f], [106.6f, 26.5f]], []),
+ ['\\'] = ([12.5f, 527.8f, 0, 968.5f], [[104.3f, 902], [489.3f, 25.2f], [249.9f, 442.6f], [224, 688.5f]], []),
+ [']'] = ([71, 494.9f, 0, 968.5f], [[425.2f, 897.3f], [97.9f, 26.5f], [469.7f, 382.5f], [457, 26.5f]], []),
+ ['^'] = ([-21.4f, 571.4f, 470.5f, 968.5f], [[275.7f, 819.5f], [26.2f, 530.7f], [531.8f, 537.2f], [118.2f, 704.6f]], []),
+ ['_'] = ([-50.3f, 581.9f, -286.9f, -116], [[36.3f, -203.3f], [552.8f, -148], [301.9f, -258.6f], [187.6f, -148]], []),
+ ['`'] = ([140.2f, 400.9f, 754.3f, 968.5f], [[255.7f, 907], [359.2f, 780.2f], [275.8f, 816.9f], [192.3f, 940.4f]], []),
+ ['a'] = (
+ [-17.8f, 562, -13.1f, 744.5f],
+ [[471.9f, 137.4f], [110.1f, 672.8f], [14.1f, 215], [523.6f, 562]],
+ [[246.7f, 215], [398, 307.3f], [361.1f, 189.1f]]),
+ ['b'] = (
+ [-72.6f, 591.8f, -12.6f, 1024.1f],
+ [[25.2f, 149.2f], [-42.2f, 993.5f], [493.4f, 589.1f], [493.4f, 120.8f]],
+ [[273.5f, 344.3f], [294.8f, 571.3f], [287.7f, 127.9f]]),
+ ['c'] = ([1.3f, 548.8f, -13.4f, 759.4f], [[74, 439.5f], [480.1f, 107.2f], [513.3f, 576.1f], [125.7f, 85.1f]], []),
+ ['d'] = (
+ [-38.6f, 617.5f, -12.5f, 1015.4f],
+ [[519.6f, 154.7f], [590.5f, 984.8f], [58.4f, 587.5f], [58.4f, 119.2f]],
+ [[271.3f, 371.1f], [257.1f, 126.3f], [211, 559.1f]]),
+ ['e'] = (
+ [-34.5f, 577.2f, -13.2f, 753.4f],
+ [[52.8f, 401.2f], [503.3f, 98.5f], [503.3f, 608], [145.1f, 54.2f]],
+ [[281.7f, 548.9f], [422, 497.2f], [148.8f, 497.2f]]),
+ ['f'] = ([28.2f, 517.1f, 0, 1017.6f], [[271.6f, 639.6f], [310.7f, 29.4f], [488, 987.3f], [236.2f, 331]], []),
+ ['g'] = (
+ [-26.1f, 594.7f, -240.2f, 708.4f],
+ [[503.2f, 554.5f], [85.2f, -178.9f], [3.7f, 352.5f], [567, 30.1f]],
+ [[269.4f, 387.9f], [290.7f, 171.8f], [276.5f, 568.6f]]),
+ ['h'] = ([-13.5f, 551.1f, 0, 1023.7f], [[80.8f, 574.9f], [524.2f, 28.6f], [16.9f, 67.6f], [517.1f, 475.5f]], []),
+ ['i'] = ([110.1f, 464.8f, 0, 1042.9f], [[353, 952.9f], [438.1f, 27], [438.1f, 495.2f], [140.1f, 672.6f]], []),
+ ['j'] = ([99.3f, 468.4f, -239, 1018], [[373.7f, 930.5f], [129.2f, -210.3f], [441, 321.2f], [210.7f, 604.6f]], []),
+ ['k'] = ([-46.4f, 572.2f, 0, 1007.9f], [[40.4f, 385], [-16.3f, 977.4f], [505.1f, 26.7f], [466.1f, 665.2f]], []),
+ ['l'] = ([77.9f, 442.5f, 0, 1049.1f], [[186.1f, 118.2f], [108.1f, 1019.3f], [108.1f, 565.2f], [413.2f, 29.5f]], []),
+ ['m'] = ([-62.5f, 612.5f, 0, 734.5f], [[279.3f, 607.1f], [-30.8f, 31.2f], [582.1f, 71.8f], [-30.8f, 404.1f]], []),
+ ['n'] = ([-55.1f, 592.1f, 0, 731.6f], [[43.4f, 564], [564, 28.7f], [-23, 28.7f], [549.2f, 538.2f]], []),
+ ['o'] = (
+ [-64.1f, 614.1f, -13, 738.7f],
+ [[542.6f, 396.9f], [-14.9f, 227.1f], [151.2f, 684.8f], [350.6f, 27.7f]],
+ [[269.4f, 371], [258.3f, 127.4f], [250.9f, 596.2f]]),
+ ['p'] = (
+ [-71.4f, 592.8f, -235.7f, 694.7f],
+ [[25.5f, 533.8f], [-41.8f, -206.6f], [500.2f, 126.4f], [479, 579.9f]],
+ [[273.5f, 335.4f], [255.8f, 558.6f], [287.7f, 115.8f]]),
+ ['q'] = (
+ [-52, 623, -239.1f, 704.8f],
+ [[523.5f, 543.7f], [594.4f, -211], [45.2f, 118.5f], [59.4f, 586.2f]],
+ [[272, 352.3f], [257.8f, 125.6f], [307.4f, 561.4f]]),
+ ['r'] = ([-15.6f, 538.2f, 0, 743.4f], [[78.1f, 575.6f], [15.4f, 29.2f], [510.1f, 520.2f], [100.3f, 295]], []),
+ ['s'] = ([-18.3f, 553.5f, -13.2f, 747.3f], [[75.5f, 542.4f], [463.2f, 73.6f], [31.2f, 110.5f], [492.7f, 597.8f]], []),
+ ['t'] = ([32.9f, 528.6f, -12.5f, 904.9f], [[232.8f, 648.1f], [484.7f, 38], [193.8f, 275.6f], [498.8f, 680]], []),
+ ['u'] = ([-49.1f, 612, -12.9f, 721.5f], [[513.7f, 155], [-18, 690.3f], [583.8f, 690.3f], [-6.9f, 169.7f]], []),
+ ['v'] = ([-72.2f, 622.2f, 0, 712.6f], [[520.1f, 637], [198.9f, 27.9f], [-30, 681.3f], [442.6f, 290]], []),
+ ['w'] = ([-60, 610, 0, 720.7f], [[119.5f, 116.8f], [577.3f, 689.1f], [-24.5f, 689.1f], [525.6f, 260.8f]], []),
+ ['x'] = ([-50.1f, 600, 0, 720.7f], [[268.7f, 397.4f], [541.9f, 28.2f], [6.6f, 28.2f], [6.6f, 689.1f]], []),
+ ['y'] = ([-64.6f, 619.8f, -241.7f, 700.1f], [[288, 227], [-16.7f, 669.9f], [575, 669.9f], [11.7f, -212.3f]], []),
+ ['z'] = ([-1.7f, 562.8f, 0, 752.8f], [[443, 662], [29.5f, 30.7f], [531.6f, 100.9f], [51.6f, 721.1f]], []),
+ ['{'] = ([-3.9f, 553.9f, 0, 968.5f], [[245.5f, 766.7f], [526.2f, 28.2f], [198.7f, 325.6f], [526.2f, 940.5f]], []),
+ ['|'] = ([205, 345, -90, 1170], [[275.6f, 1099.4f], [239.6f, -56.6f], [311.6f, 519.4f], [239.6f, 807.4f]], []),
+ ['}'] = ([0.3f, 556.3f, 0, 968.5f], [[306.6f, 767.9f], [28, 25], [353, 326.8f], [28, 940.4f]], []),
+ ['~'] = ([12.1f, 531.1f, 766, 968.5f], [[105.6f, 881.2f], [502, 925.6f], [314, 809.4f], [235.4f, 925.6f]], []),
+ ['£'] = ([5.3f, 554.6f, 0, 968.5f], [[251.6f, 511.3f], [528.8f, 26.1f], [33.7f, 62.4f], [509, 920.5f]], []),
+ ['¤'] = (
+ [-72.9f, 615.5f, 99.9f, 804.6f],
+ [[101.8f, 625.6f], [522.4f, 145.9f], [20.6f, 145.9f], [570.3f, 710.4f]],
+ [[271.5f, 455.8f], [282.6f, 312], [260.4f, 592.4f]]),
+ ['¥'] = ([-81.7f, 625, 0, 968.5f], [[272.5f, 356.1f], [-26.3f, 940.3f], [571.3f, 940.3f], [238.9f, 708.6f]], []),
+ ['§'] = (
+ [-7.1f, 574.2f, -11.7f, 968.5f],
+ [[137.7f, 638.9f], [459.2f, 54.1f], [24.8f, 187.5f], [544.7f, 481.6f]],
+ [[178.7f, 495.3f], [425, 467.9f], [305.3f, 512.4f]]),
+ ['¨'] = ([110.8f, 445.5f, 769, 968.5f], [[165.8f, 912.8f], [421.6f, 792.8f], [361.6f, 941.2f], [197.4f, 792.8f]], []),
+ ['´'] = ([153.5f, 404.7f, 754.3f, 968.5f], [[294.4f, 905.4f], [197.2f, 779], [275, 817.9f], [356, 941]], []),
+ ['¸'] = ([177, 373, -271, -12.5f], [[268, -199.1f], [334.4f, -44.2f], [264.3f, -106.9f], [312.3f, -151.2f]], []),
+ ['Ä'] = (
+ [-74.7f, 617.3f, 0, 1072],
+ [[251.8f, 904.4f], [576.4f, 30], [41.5f, 328.8f], [458.4f, 502.2f]],
+ [[270.2f, 458], [273.9f, 675.6f], [248.1f, 568.7f]]),
+ ['Å'] = (
+ [-68, 607, 0, 1032.2f],
+ [[81.1f, 252.2f], [420.6f, 975.3f], [564.4f, 30.8f], [435.3f, 525.2f]],
+ [[273, 816.7f], [177, 337], [306.2f, 562.1f]]),
+ ['Æ'] = (
+ [-55.5f, 595.4f, 0, 971.2f],
+ [[319, 888.1f], [-16.8f, 28.4f], [566.2f, 105.9f], [71.8f, 489.6f]],
+ [[219.4f, 430.6f], [237.8f, 725.7f], [212, 578.2f]]),
+ ['Ñ'] = ([-33.2f, 583.2f, 0, 1089.2f], [[484.7f, 105.7f], [100.9f, 1046.5f], [-2.4f, 456.2f], [554.8f, 685]], []),
+ ['Ö'] = (
+ [-63.6f, 608.3f, -12.8f, 1060.1f],
+ [[23.5f, 367.7f], [576.9f, 1028.1f], [-31.9f, 976.4f], [576.9f, 463.6f]],
+ [[281.7f, 426.7f], [270.7f, 150], [263.3f, 692.3f]]),
+ ['Ø'] = (
+ [-55.7f, 605.6f, -13.3f, 1050.4f],
+ [[536.7f, 510], [123.5f, 34], [-9.4f, 650.2f], [389.1f, 1008.1f]],
+ [[186.2f, 683.4f], [322.7f, 237], [433.4f, 513.7f]]),
+ ['Ü'] = ([-60.2f, 648.9f, -12.8f, 1049.1f], [[568.4f, 719], [40.8f, 102.8f], [-25.6f, 1017.9f], [590.5f, 165.6f]], []),
+ ['ß'] = ([-63.2f, 595, 0, 1062.4f], [[355.6f, 558.8f], [-32.1f, 30.8f], [104.5f, 998.1f], [462.6f, 75.1f]], []),
+ ['å'] = (
+ [-21.7f, 571.2f, -12.6f, 1033],
+ [[479.2f, 137.9f], [161.7f, 979.6f], [17.7f, 274.5f], [479.2f, 617.8f]],
+ [[265, 215.4f], [316.7f, 898.4f], [176.4f, 776.6f]]),
+ ['æ'] = (
+ [-53.3f, 603.3f, -12.8f, 737.1f],
+ [[277.2f, 380.8f], [37.2f, 41.2f], [513.5f, 41.2f], [528.3f, 668.8f]],
+ [[140.6f, 222.1f], [432.3f, 591.3f], [362.1f, 469.4f]]),
+ ['ø'] = (
+ [-49.8f, 605.3f, -128.8f, 835.8f],
+ [[137.1f, 84.5f], [524.8f, 804.5f], [52.2f, 594], [572.8f, 313.4f]],
+ [[369.7f, 232.2f], [229.4f, 586.6f], [115, 335.6f]]),
+ ['IJ'] = ([-76.6f, 634, -12.3f, 982.9f], [[516.1f, 102.7f], [-48.4f, 951.3f], [604.7f, 774.2f], [-48.4f, 338.8f]], []),
+ ['ij'] = ([-75.5f, 618.3f, -243.7f, 1063.6f], [[19.2f, 969.9f], [388.3f, -207.9f], [543.4f, 486.2f], [-3, 246.2f]], []),
+ ['―'] = ([-309, 859, -524, -384], [[-238.3f, -453.3f], [825.7f, -493.3f], [297.7f, -417.3f], [33.7f, -493.3f]], []),
+ ['∣'] = ([205, 345, -190, 1270], [[275.6f, 1199.4f], [239.6f, -156.6f], [311.6f, 483.4f], [239.6f, 839.4f]], []),
+ ['█'] = ([-99, 649, -51, 1090], [[271.4f, 716.3f], [615.4f, -19.7f], [-68.6f, 108.3f], [615.4f, 1056.3f]], []),
+ };
+
+ ///
+ /// Gets the OCR-A per-glyph expectations. Bounds are [minX, maxX, minY, maxY] of the ink in design
+ /// units, or an empty array when the figure leaves an extreme underdetermined; probes are [x, y]
+ /// points. Inked probes assert ink where a stroke must pass and blank probes assert holes in
+ /// counters and gaps, so errors that preserve the bounding box still trip a check.
+ ///
+ public static IReadOnlyDictionary OcrA { get; } =
+ new Dictionary
+ {
+ ['0'] = ([-70, 620, -70, 1010], [[275, 940], [275, 0], [0, 470], [550, 470]], [[275, 470]]),
+ ['1'] = ([-70, 620, -70, 1010], [[137, 940], [275, 470], [275, 0], [550, 300]], [[450, 600]]),
+ ['2'] = ([-70, 620, -70, 1010], [[275, 940], [275, 470], [275, 0], [0, 235], [550, 705]], [[100, 700], [450, 235]]),
+ ['3'] = ([-70, 620, -70, 1010], [[275, 940], [275, 470], [275, 0], [550, 700]], [[100, 700], [100, 235]]),
+ ['4'] = ([-1.25f, 620, -70, 1010], [[68.75f, 600], [550, 600], [275, 352.5f]], [[275, 600]]),
+ ['5'] = ([-70, 620, -70, 1010], [[300, 975], [137.5f, 700], [300, 500], [550, 235], [300, -30], [0, 59]], [[300, 700], [300, 180], [-40, 940]]),
+ ['6'] = ([-70, 620, -70, 1010], [[0, 500], [68.75f, 940], [275, 0], [550, 176]], [[275, 176], [275, 600]]),
+ ['7'] = ([-70, 620, -70, 1010], [[0, 900], [275, 940], [550, 750], [275, 100]], [[100, 300], [450, 200]]),
+ ['8'] = ([-70, 620, -70, 1010], [[275, 470], [275, 940], [275, 0], [0, 235], [550, 235]], [[275, 705], [275, 235]]),
+ ['9'] = ([-70, 620, -70, 1010], [[550, 500], [481.25f, 0], [275, 940], [0, 750]], [[275, 764], [275, 300]]),
+ ['A'] = ([-70, 620, -70, 1010], [[275, 940], [275, 235], [30, 100], [520, 100]], [[275, 100], [275, 500]]),
+ ['B'] = ([-70, 620, -70, 1010], [[0, 470], [275, 940], [275, 470], [275, 0], [550, 700], [550, 235]], [[275, 705], [275, 235]]),
+ ['C'] = ([-70, 620, -70, 1010], [[0, 470], [275, 940], [275, 0], [550, 940], [550, 0]], [[275, 470], [550, 470]]),
+ ['D'] = ([-70, 620, -70, 1010], [[137.5f, 470], [275, 940], [275, 0], [550, 470]], [[340, 470], [30, 470]]),
+ ['E'] = ([-70, 620, -70, 1010], [[0, 470], [275, 940], [275, 470], [275, 0], [550, 940]], [[450, 470], [275, 705], [275, 235]]),
+ ['F'] = ([-70, 620, -70, 1010], [[0, 470], [275, 940], [275, 587.5f], [412, 587.5f]], [[450, 300], [275, 300], [275, 760]]),
+ ['G'] = ([-70, 620, -70, 1010], [[275, 940], [0, 470], [275, 0], [550, 200], [400, 352.5f]], [[275, 600], [450, 500]]),
+ ['H'] = ([-70, 620, -70, 1010], [[0, 470], [550, 470], [275, 470]], [[275, 700], [275, 235]]),
+ ['I'] = ([-70, 620, -70, 1010], [[275, 470], [275, 940], [275, 0], [0, 940], [550, 0]], [[100, 470], [450, 470]]),
+ ['J'] = ([-1.25f, 551.25f, -70, 1010], [[481.25f, 700], [275, 0], [68.75f, 300]], [[275, 400], [275, 700]]),
+ ['K'] = ([-70, 620, -70, 1010], [[0, 470], [275, 705], [275, 235]], [[450, 470], [300, 900]]),
+ ['L'] = ([-70, 620, -70, 1010], [[0, 470], [275, 0], [550, 0]], [[275, 470]]),
+ ['M'] = ([-70, 620, -70, 1010], [[0, 470], [550, 470], [275, 660], [275, 530], [137, 800], [0, 990]], [[275, 300], [275, 880]]),
+ ['N'] = ([-70, 620, -70, 1010], [[0, 470], [550, 470], [275, 470]], [[200, 235], [350, 705]]),
+ ['O'] = ([-70, 620, -70, 1010], [[0, 470], [550, 470], [275, 940], [275, 0]], [[275, 470]]),
+ ['P'] = ([-70, 620, -70, 1010], [[0, 470], [275, 940], [275, 411.25f], [550, 700]], [[275, 700], [350, 150]]),
+ ['Q'] = ([-70, 620, -70, 1010], [[400, 940], [550, 470], [0, 300], [430, 25]], [[250, 550]]),
+ ['R'] = ([-70, 620, -70, 1010], [[0, 470], [275, 940], [275, 587.5f], [378, 294]], [[275, 760], [150, 235]]),
+ ['S'] = (
+ [-70, 620, -70, 1010],
+ [[275, 470], [300, 1000], [550, 822.5f], [0, 117.5f], [550, 765], [300, -60], [550, 87]],
+ [[350, 700], [200, 200], [275, 700], [275, 200], [432, 822], [117, 117]]),
+ ['T'] = ([-70, 620, -70, 1010], [[275, 470], [0, 870], [550, 870], [275, 940]], [[100, 400], [450, 400]]),
+ ['U'] = ([-70, 620, -70, 1010], [[0, 470], [550, 470], [275, 0]], [[275, 470]]),
+ ['V'] = ([-70, 620, -70, 1010], [[0, 800], [137.5f, 352.5f], [412.5f, 352.5f]], [[275, 600], [60, 100]]),
+ ['W'] = (
+ [-70, 620, -70, 1010],
+ [[0, 470], [550, 470], [275, 400], [137.5f, -30], [412.5f, -30]],
+ [[137.5f, 500], [412.5f, 500], [275, -60], [275, 800]]),
+ ['X'] = ([-70, 620, -70, 1010], [[275, 470], [50, 85], [500, 855]], [[275, 100], [275, 840]]),
+ ['Y'] = ([-70, 620, -70, 1010], [[275, 470], [275, 100], [0, 900], [550, 900]], [[100, 300], [450, 300]]),
+ ['Z'] = ([-70, 620, -70, 1010], [[275, 940], [275, 470], [275, 0], [590, 940], [-40, 0], [590, 0]], [[60, 500], [500, 200]]),
+ ['.'] = ([136.25f, 413.75f, 106.25f, 363.75f], [[275, 235]], [[275, 600]]),
+ [','] = ([67.5f, 482.5f, -70, 422.5f], [[275, 352.5f], [412.5f, 100]], [[200, 100]]),
+ [':'] = ([136.25f, 413.75f, 106.25f, 833.75f], [[275, 235], [275, 705]], [[275, 470]]),
+ [';'] = ([67.5f, 482.5f, -70, 892.5f], [[275, 352.5f], [206, 764]], [[275, 550]]),
+ ['='] = ([-70, 620, 282.5f, 716.25f], [[275, 352.5f], [275, 646.25f]], [[275, 500]]),
+ ['+'] = ([-70, 620, 125, 815], [[275, 470], [275, 200], [275, 740], [0, 470], [550, 470]], [[100, 200]]),
+ ['-'] = ([-70, 620, 341.25f, 540], [[275, 440], [0, 440], [550, 440]], [[275, 600]]),
+ ['/'] = ([-70, 620, -70, 1010], [[275, 470]], [[100, 700], [450, 235]]),
+ ['*'] = ([-70, 620, 47.5f, 892.5f], [[275, 470], [275, 150], [275, 790]], [[100, 470]]),
+ ['"'] = ([-70, 620, 517.5f, 1010], [[275, 940], [37, 750], [513, 750], [68.75f, 620], [481.25f, 620]], [[275, 700]]),
+ ['\''] = ([136, 414, 400, 1010], [[206.25f, 700], [275, 940], [280, 600]], [[450, 800]]),
+ ['%'] = ([-70, 620, -70, 1010], [[68.75f, 880], [481.25f, 60], [275, 470]], [[450, 880], [100, 60]]),
+ ['$'] = ([-70, 620, -70, 1010], [[275, 470], [275, 970], [0, 600]], [[100, 100], [450, 850]]),
+ ['('] = ([-70, 620, -70, 1010], [[275, 470], [550, 940], [550, 0], [0, 470]], [[450, 470]]),
+ [')'] = ([-70, 620, -70, 1010], [[275, 470], [0, 940], [0, 0], [550, 470]], [[100, 470]]),
+ ['?'] = ([-70, 620, -70, 1010], [[137.5f, 0], [0, 587.5f], [275, 822.5f], [412, 900], [550, 705], [137.5f, 300]], [[275, 300], [400, 150]]),
+ ['&'] = ([-70, 620, -70, 892.5f], [[275, 0], [0, 300], [206, 822.5f], [412.5f, 650], [183, 392], [550, 30]], [[450, 400], [100, 700]]),
+ ['|'] = ([205, 345, -294, 1166], [[275, 0], [275, 1000], [275, -250]], [[100, 470], [275, 436]]),
+ ['⑀'] = ([-70, 620, -70, 1010], [[275, 470], [550, 700], [0, 176], [275, 940], [275, 0]], [[450, 176]]),
+ ['⑁'] = ([-70, 620, -70, 1010], [[550, 470], [0, 235], [-30, 470]], [[300, 150]]),
+ ['⑂'] = ([-70, 620, -70, 1010], [[0, 700], [550, 700], [275, 470], [275, 100]], [[275, 700]]),
+ ['█'] = ([-70, 620, -70, 1010], [[275, 470], [0, 470], [550, 470], [275, 940], [275, 0]], []),
+ ['―'] = ([-225, 775, 400, 540], [[275, 470], [-200, 470], [750, 470]], [[275, 600]]),
+ ['a'] = ([-70, 620, -70, 716], [[138, 646], [275, 353], [275, 0], [0, 176], [470, 400]], [[200, 176], [250, 500]]),
+ ['b'] = ([-70, 620, -70, 1010], [[0, 800], [275, 646], [275, 0], [550, 300]], [[275, 300]]),
+ ['c'] = ([-70, 620, -70, 716], [[275, 646], [275, 0], [0, 300]], [[275, 300], [480, 300]]),
+ ['d'] = ([-70, 620, -70, 1010], [[550, 800], [275, 646], [275, 0], [0, 300]], [[275, 300]]),
+ ['e'] = ([-70, 620, -70, 716], [[275, 294], [275, 646], [275, 0], [0, 470]], [[275, 450], [480, 100]]),
+ ['f'] = ([-1, 620, -70, 1010], [[206, 300], [275, 646], [450, 940]], [[60, 300], [520, 646]]),
+ ['g'] = ([-70, 620, -364, 716], [[275, 646], [275, 0], [550, 300], [550, -100], [300, -294]], [[275, 300], [100, -150]]),
+ ['h'] = ([-70, 620, -70, 1010], [[0, 800], [0, 300], [275, 646], [545, 300]], [[275, 300]]),
+ ['i'] = ([-1, 551, -70, 1069], [[275, 300], [240, 970], [275, 0], [100, 646]], [[275, 800], [100, 300]]),
+ ['j'] = ([-70, 483, -364, 1069], [[413, 300], [378, 970], [200, -294], [200, 646]], [[200, 300], [200, -50]]),
+ ['k'] = ([-70, 620, -70, 1010], [[0, 800], [0, 300], [300, 536], [300, 161]], [[450, 353]]),
+ ['l'] = ([-1, 551, -70, 1010], [[275, 600], [275, 0], [100, 0]], [[100, 400]]),
+ ['m'] = ([-85, 635, -70, 725], [[-15, 50], [-15, 600], [275, 300], [565, 50], [178, 700], [454, 700]], [[140, 300], [420, 300], [314, 690]]),
+ ['n'] = ([-70, 620, -70, 716], [[0, 300], [550, 300], [300, 646]], [[275, 300]]),
+ ['o'] = ([-70, 620, -70, 716], [[0, 300], [550, 300], [275, 646], [275, 0]], [[275, 300]]),
+ ['p'] = ([-70, 620, -364, 745.5f], [[0, 300], [0, -200], [550, 300], [275, 675.5f], [275, -29.5f]], [[275, 300]]),
+ ['q'] = ([-70, 620, -364, 716], [[550, -200], [0, 300], [275, 646], [275, 0], [550, 300]], [[275, 300]]),
+ ['r'] = ([-70, 620, -70, 716], [[0, 300], [300, 646], [550, 500]], [[300, 300]]),
+ ['s'] = ([-61, 616, -70, 716], [[275, 323], [300, 700], [250, -55], [535, 588], [15, 59], [560, 138]], [[275, 500], [275, 150], [422, 508]]),
+ ['t'] = ([-70, 620, -70, 951], [[275, 646], [138, 300], [345, -30], [550, 118], [138, 880]], [[300, 300], [0, 300]]),
+ ['u'] = ([-70, 620, -70, 716], [[0, 300], [550, 300], [275, 0]], [[275, 300]]),
+ ['v'] = ([-70, 620, -70, 716], [[0, 500], [550, 500], [275, 0]], [[275, 500]]),
+ ['w'] = ([-84, 634, -70, 716], [[-10, 600], [560, 600], [275, 350], [275, 411], [103, 0], [447, 0]], [[150, 500], [275, 550]]),
+ ['x'] = ([-55, 605, -70, 716], [[275, 323], [480, 578]], [[275, 100], [275, 550]]),
+ ['y'] = ([-70, 620, -364, 716], [[0, 600], [550, 600], [100, -294], [350, 100]], [[100, 100]]),
+ ['z'] = ([-70, 620, -70, 716], [[275, 646], [275, 323], [275, 0]], [[60, 300], [490, 350]]),
+ };
+
+ ///
+ /// Verifies one glyph's flattened contours against its expectations, appending any deviation to
+ /// as a line of glyph, feature, expected and actual values. A character
+ /// with no recorded expectation is itself a failure, so a new glyph cannot land unchecked.
+ ///
+ /// The character being verified.
+ /// The flattened outline contours in design units.
+ /// The expectation table of the design being built.
+ /// The failure sink.
+ public static void Verify(
+ char character,
+ IReadOnlyList> contours,
+ IReadOnlyDictionary expectations,
+ List failures)
+ {
+ if (!expectations.TryGetValue(character, out (float[] Bounds, float[][] Inked, float[][] Blank) spec))
+ {
+ failures.Add($"U+{(int)character:X4} '{character}': no spec expectation recorded");
+ return;
+ }
+
+ float minX = float.MaxValue;
+ float maxX = float.MinValue;
+ float minY = float.MaxValue;
+ float maxY = float.MinValue;
+ foreach (IReadOnlyList contour in contours)
+ {
+ foreach (Vector2 point in contour)
+ {
+ minX = MathF.Min(minX, point.X);
+ maxX = MathF.Max(maxX, point.X);
+ minY = MathF.Min(minY, point.Y);
+ maxY = MathF.Max(maxY, point.Y);
+ }
+ }
+
+ if (spec.Bounds.Length == 4)
+ {
+ Check(failures, character, "minX", spec.Bounds[0], minX);
+ Check(failures, character, "maxX", spec.Bounds[1], maxX);
+ Check(failures, character, "minY", spec.Bounds[2], minY);
+ Check(failures, character, "maxY", spec.Bounds[3], maxY);
+ }
+
+ foreach (float[] point in spec.Inked)
+ {
+ if (!Contains(contours, new Vector2(point[0], point[1])))
+ {
+ failures.Add($"'{character}': expected ink at ({point[0]}, {point[1]}) but found none");
+ }
+ }
+
+ foreach (float[] point in spec.Blank)
+ {
+ if (Contains(contours, new Vector2(point[0], point[1])))
+ {
+ failures.Add($"'{character}': expected no ink at ({point[0]}, {point[1]}) but found ink");
+ }
+ }
+ }
+
+ ///
+ /// Compares one measured ink extreme against its expected value within .
+ ///
+ /// The failure sink.
+ /// The character being verified.
+ /// The name of the extreme being compared.
+ /// The expected value in design units.
+ /// The measured value in design units.
+ private static void Check(List failures, char character, string feature, float expected, float actual)
+ {
+ if (MathF.Abs(expected - actual) > BoundsTolerance)
+ {
+ failures.Add($"'{character}': {feature} expected {expected} but measured {actual:F1}");
+ }
+ }
+
+ ///
+ /// Determines whether a point lies inside the outline ink by the non-zero winding rule, the fill
+ /// rule the TrueType rasteriser applies to the glyph.
+ ///
+ /// The flattened outline contours in design units.
+ /// The point to test.
+ /// when the point is inside the ink; otherwise .
+ private static bool Contains(IReadOnlyList> contours, Vector2 point)
+ {
+ int winding = 0;
+ foreach (IReadOnlyList contour in contours)
+ {
+ for (int i = 0; i < contour.Count; i++)
+ {
+ Vector2 a = contour[i];
+ Vector2 b = contour[(i + 1) % contour.Count];
+ if (a.Y <= point.Y)
+ {
+ if (b.Y > point.Y && Cross(a, b, point) > 0)
+ {
+ winding++;
+ }
+ }
+ else if (b.Y <= point.Y && Cross(a, b, point) < 0)
+ {
+ winding--;
+ }
+ }
+ }
+
+ return winding != 0;
+ }
+
+ ///
+ /// Computes the z component of the cross product of the edge from to
+ /// with the vector from to ,
+ /// positive when the point lies left of the edge.
+ ///
+ /// The edge start.
+ /// The edge end.
+ /// The point to classify.
+ /// The signed area term.
+ private static float Cross(Vector2 a, Vector2 b, Vector2 point) =>
+ ((b.X - a.X) * (point.Y - a.Y)) - ((point.X - a.X) * (b.Y - a.Y));
+}
diff --git a/src/ImageSharp.Drawing.FontGenerator/TrueTypeWriter.cs b/src/ImageSharp.Drawing.FontGenerator/TrueTypeWriter.cs
new file mode 100644
index 00000000..4c3d913a
--- /dev/null
+++ b/src/ImageSharp.Drawing.FontGenerator/TrueTypeWriter.cs
@@ -0,0 +1,616 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Buffers.Binary;
+
+namespace SixLabors.ImageSharp.Drawing.FontGenerator;
+
+///
+/// Writes a minimal TrueType font from flattened glyph contours. Outlines contain on-curve points only,
+/// which the TrueType glyf format permits, so no curve conversion is required. The emitted tables are
+/// head, hhea, maxp, OS/2, hmtx, cmap, loca, glyf, name and post.
+///
+internal sealed class TrueTypeWriter
+{
+ private readonly List glyphs = [];
+ private readonly SortedDictionary characterMap = [];
+
+ ///
+ /// Initializes a new instance of the class. Glyph index 0 is the
+ /// required empty .notdef glyph.
+ ///
+ /// The design units per em.
+ /// The typographic ascender in design units.
+ /// The typographic descender in design units, negative below the baseline.
+ /// The ink height of a flat topped capital in design units.
+ /// The font family name.
+ /// The copyright notice written to name ID 0.
+ public TrueTypeWriter(ushort unitsPerEm, short ascender, short descender, short capHeight, string familyName, string copyright)
+ {
+ this.UnitsPerEm = unitsPerEm;
+ this.Ascender = ascender;
+ this.Descender = descender;
+ this.CapHeight = capHeight;
+ this.FamilyName = familyName;
+ this.Copyright = copyright;
+ this.glyphs.Add(new GlyphRecord([], unitsPerEm));
+ }
+
+ ///
+ /// Gets the design units per em.
+ ///
+ public ushort UnitsPerEm { get; }
+
+ ///
+ /// Gets the typographic ascender in design units.
+ ///
+ public short Ascender { get; }
+
+ ///
+ /// Gets the typographic descender in design units.
+ ///
+ public short Descender { get; }
+
+ ///
+ /// Gets the ink height of a flat topped capital in design units. Text layout uses this value to
+ /// place a line of capitals or digits against a reference line.
+ ///
+ public short CapHeight { get; }
+
+ ///
+ /// Gets the font family name.
+ ///
+ public string FamilyName { get; }
+
+ ///
+ /// Gets the copyright notice.
+ ///
+ public string Copyright { get; }
+
+ ///
+ /// Adds a glyph mapped to a character. Contours are closed point lists in font units with the y axis
+ /// pointing up; fill follows the non-zero winding rule.
+ ///
+ /// The character the glyph represents.
+ /// The closed contours, each a list of on-curve points.
+ /// The advance width in font units.
+ public void AddGlyph(char character, IReadOnlyList> contours, ushort advanceWidth)
+ {
+ this.characterMap.Add(character, (ushort)this.glyphs.Count);
+ this.glyphs.Add(new GlyphRecord(contours, advanceWidth));
+ }
+
+ ///
+ /// Serializes the font.
+ ///
+ /// The font file bytes.
+ public byte[] Write()
+ {
+ byte[] glyf = this.BuildGlyf(out uint[] locaOffsets, out short xMin, out short yMin, out short xMax, out short yMax);
+ byte[] loca = BuildLoca(locaOffsets);
+ byte[] head = this.BuildHead(xMin, yMin, xMax, yMax);
+ byte[] hhea = this.BuildHhea(xMin, xMax);
+ byte[] maxp = this.BuildMaxp();
+ byte[] hmtx = this.BuildHmtx();
+ byte[] cmap = this.BuildCmap();
+ byte[] os2 = this.BuildOs2(xMin, yMin, xMax, yMax);
+ byte[] name = this.BuildName();
+ byte[] post = BuildPost();
+
+ (string Tag, byte[] Data)[] tables =
+ [
+ ("OS/2", os2),
+ ("cmap", cmap),
+ ("glyf", glyf),
+ ("head", head),
+ ("hhea", hhea),
+ ("hmtx", hmtx),
+ ("loca", loca),
+ ("maxp", maxp),
+ ("name", name),
+ ("post", post),
+ ];
+
+ ushort numTables = (ushort)tables.Length;
+ ushort searchRange = 16;
+ ushort entrySelector = 0;
+ while (searchRange * 2 <= numTables * 16)
+ {
+ searchRange *= 2;
+ entrySelector++;
+ }
+
+ int headerLength = 12 + (numTables * 16);
+ int totalLength = headerLength;
+ foreach ((_, byte[] data) in tables)
+ {
+ totalLength += (data.Length + 3) & ~3;
+ }
+
+ byte[] font = new byte[totalLength];
+ BinaryPrimitives.WriteUInt32BigEndian(font, 0x00010000);
+ BinaryPrimitives.WriteUInt16BigEndian(font.AsSpan(4), numTables);
+ BinaryPrimitives.WriteUInt16BigEndian(font.AsSpan(6), searchRange);
+ BinaryPrimitives.WriteUInt16BigEndian(font.AsSpan(8), entrySelector);
+ BinaryPrimitives.WriteUInt16BigEndian(font.AsSpan(10), (ushort)((numTables * 16) - searchRange));
+
+ int record = 12;
+ int offset = headerLength;
+ int headOffset = 0;
+ foreach ((string tag, byte[] data) in tables)
+ {
+ if (tag == "head")
+ {
+ headOffset = offset;
+ }
+
+ for (int i = 0; i < 4; i++)
+ {
+ font[record + i] = (byte)tag[i];
+ }
+
+ data.CopyTo(font, offset);
+ BinaryPrimitives.WriteUInt32BigEndian(font.AsSpan(record + 4), TableChecksum(font.AsSpan(offset, (data.Length + 3) & ~3)));
+ BinaryPrimitives.WriteUInt32BigEndian(font.AsSpan(record + 8), (uint)offset);
+ BinaryPrimitives.WriteUInt32BigEndian(font.AsSpan(record + 12), (uint)data.Length);
+ record += 16;
+ offset += (data.Length + 3) & ~3;
+ }
+
+ uint total = TableChecksum(font);
+ BinaryPrimitives.WriteUInt32BigEndian(font.AsSpan(headOffset + 8), 0xB1B0AFBA - total);
+ return font;
+ }
+
+ ///
+ /// Sums the table as big-endian 32 bit words with zero padding, the per-table checksum the sfnt
+ /// directory records.
+ ///
+ /// The table bytes.
+ /// The checksum.
+ private static uint TableChecksum(ReadOnlySpan data)
+ {
+ uint sum = 0;
+ for (int i = 0; i < data.Length; i += 4)
+ {
+ uint value = 0;
+ for (int b = 0; b < 4; b++)
+ {
+ value <<= 8;
+ if (i + b < data.Length)
+ {
+ value |= data[i + b];
+ }
+ }
+
+ sum += value;
+ }
+
+ return sum;
+ }
+
+ ///
+ /// Builds the long-format loca table: one 32 bit glyf offset per glyph plus the trailing end offset.
+ ///
+ /// The glyf byte offsets.
+ /// The table bytes.
+ private static byte[] BuildLoca(uint[] offsets)
+ {
+ byte[] loca = new byte[offsets.Length * 4];
+ for (int i = 0; i < offsets.Length; i++)
+ {
+ BinaryPrimitives.WriteUInt32BigEndian(loca.AsSpan(i * 4), offsets[i]);
+ }
+
+ return loca;
+ }
+
+ ///
+ /// Builds a version 3.0 post table, the variant that stores no glyph names.
+ ///
+ /// The table bytes.
+ private static byte[] BuildPost()
+ {
+ byte[] post = new byte[32];
+ BinaryPrimitives.WriteUInt32BigEndian(post, 0x00030000);
+ return post;
+ }
+
+ ///
+ /// Builds the glyf table from the glyph contours: per glyph a simple-glyph header, contour end
+ /// indices, one on-curve flag per point and relative 16 bit coordinates, padded to 4 bytes.
+ ///
+ /// The glyf byte offset of every glyph plus the trailing end offset.
+ /// The font-wide minimum x of the ink.
+ /// The font-wide minimum y of the ink.
+ /// The font-wide maximum x of the ink.
+ /// The font-wide maximum y of the ink.
+ /// The table bytes.
+ private byte[] BuildGlyf(out uint[] locaOffsets, out short xMin, out short yMin, out short xMax, out short yMax)
+ {
+ using MemoryStream stream = new();
+ Span coordinate = stackalloc byte[2];
+ locaOffsets = new uint[this.glyphs.Count + 1];
+ xMin = short.MaxValue;
+ yMin = short.MaxValue;
+ xMax = short.MinValue;
+ yMax = short.MinValue;
+
+ for (int g = 0; g < this.glyphs.Count; g++)
+ {
+ locaOffsets[g] = (uint)stream.Length;
+ GlyphRecord glyph = this.glyphs[g];
+ if (glyph.Contours.Count == 0)
+ {
+ continue;
+ }
+
+ glyph.GetBounds(out short gxMin, out short gyMin, out short gxMax, out short gyMax);
+ xMin = Math.Min(xMin, gxMin);
+ yMin = Math.Min(yMin, gyMin);
+ xMax = Math.Max(xMax, gxMax);
+ yMax = Math.Max(yMax, gyMax);
+
+ int pointCount = glyph.Contours.Sum(contour => contour.Count);
+ byte[] header = new byte[10 + (glyph.Contours.Count * 2) + 2];
+ BinaryPrimitives.WriteInt16BigEndian(header, (short)glyph.Contours.Count);
+ BinaryPrimitives.WriteInt16BigEndian(header.AsSpan(2), gxMin);
+ BinaryPrimitives.WriteInt16BigEndian(header.AsSpan(4), gyMin);
+ BinaryPrimitives.WriteInt16BigEndian(header.AsSpan(6), gxMax);
+ BinaryPrimitives.WriteInt16BigEndian(header.AsSpan(8), gyMax);
+
+ int end = -1;
+ for (int c = 0; c < glyph.Contours.Count; c++)
+ {
+ end += glyph.Contours[c].Count;
+ BinaryPrimitives.WriteUInt16BigEndian(header.AsSpan(10 + (c * 2)), (ushort)end);
+ }
+
+ BinaryPrimitives.WriteUInt16BigEndian(header.AsSpan(10 + (glyph.Contours.Count * 2)), 0);
+ stream.Write(header);
+
+ // All points are on-curve; write one flag byte per point followed by absolute-to-relative
+ // deltas as int16 coordinates.
+ for (int i = 0; i < pointCount; i++)
+ {
+ stream.WriteByte(0x01);
+ }
+
+ short previous = 0;
+ foreach (IReadOnlyList<(short X, short Y)> contour in glyph.Contours)
+ {
+ foreach ((short x, _) in contour)
+ {
+ BinaryPrimitives.WriteInt16BigEndian(coordinate, (short)(x - previous));
+ stream.Write(coordinate);
+ previous = x;
+ }
+ }
+
+ previous = 0;
+ foreach (IReadOnlyList<(short X, short Y)> contour in glyph.Contours)
+ {
+ foreach ((_, short y) in contour)
+ {
+ BinaryPrimitives.WriteInt16BigEndian(coordinate, (short)(y - previous));
+ stream.Write(coordinate);
+ previous = y;
+ }
+ }
+
+ while ((stream.Length & 3) != 0)
+ {
+ stream.WriteByte(0);
+ }
+ }
+
+ locaOffsets[this.glyphs.Count] = (uint)stream.Length;
+ if (xMin == short.MaxValue)
+ {
+ xMin = 0;
+ yMin = 0;
+ xMax = 0;
+ yMax = 0;
+ }
+
+ return stream.ToArray();
+ }
+
+ ///
+ /// Builds the head table: units per em, the font-wide ink bounds, a lowest recommended size of 8
+ /// pixels per em and the long loca format flag.
+ ///
+ /// The font-wide minimum x of the ink.
+ /// The font-wide minimum y of the ink.
+ /// The font-wide maximum x of the ink.
+ /// The font-wide maximum y of the ink.
+ /// The table bytes.
+ private byte[] BuildHead(short xMin, short yMin, short xMax, short yMax)
+ {
+ byte[] head = new byte[54];
+ BinaryPrimitives.WriteUInt32BigEndian(head, 0x00010000);
+ BinaryPrimitives.WriteUInt32BigEndian(head.AsSpan(4), 0x00010000);
+ BinaryPrimitives.WriteUInt32BigEndian(head.AsSpan(12), 0x5F0F3CF5);
+ BinaryPrimitives.WriteUInt16BigEndian(head.AsSpan(16), 0x0003);
+ BinaryPrimitives.WriteUInt16BigEndian(head.AsSpan(18), this.UnitsPerEm);
+ BinaryPrimitives.WriteInt16BigEndian(head.AsSpan(36), xMin);
+ BinaryPrimitives.WriteInt16BigEndian(head.AsSpan(38), yMin);
+ BinaryPrimitives.WriteInt16BigEndian(head.AsSpan(40), xMax);
+ BinaryPrimitives.WriteInt16BigEndian(head.AsSpan(42), yMax);
+ BinaryPrimitives.WriteUInt16BigEndian(head.AsSpan(46), 8);
+ BinaryPrimitives.WriteInt16BigEndian(head.AsSpan(48), 2);
+ BinaryPrimitives.WriteInt16BigEndian(head.AsSpan(50), 1);
+ return head;
+ }
+
+ ///
+ /// Builds the hhea table: the vertical metrics, a zero line gap, the maximum advance width and the
+ /// horizontal metric count.
+ ///
+ /// The font-wide minimum x of the ink.
+ /// The font-wide maximum x of the ink.
+ /// The table bytes.
+ private byte[] BuildHhea(short xMin, short xMax)
+ {
+ byte[] hhea = new byte[36];
+ BinaryPrimitives.WriteUInt32BigEndian(hhea, 0x00010000);
+ BinaryPrimitives.WriteInt16BigEndian(hhea.AsSpan(4), this.Ascender);
+ BinaryPrimitives.WriteInt16BigEndian(hhea.AsSpan(6), this.Descender);
+ BinaryPrimitives.WriteInt16BigEndian(hhea.AsSpan(8), 0);
+ BinaryPrimitives.WriteUInt16BigEndian(hhea.AsSpan(10), this.glyphs.Max(glyph => glyph.AdvanceWidth));
+ BinaryPrimitives.WriteInt16BigEndian(hhea.AsSpan(12), xMin);
+ BinaryPrimitives.WriteInt16BigEndian(hhea.AsSpan(14), 0);
+ BinaryPrimitives.WriteInt16BigEndian(hhea.AsSpan(16), xMax);
+ BinaryPrimitives.WriteInt16BigEndian(hhea.AsSpan(18), 1);
+ BinaryPrimitives.WriteUInt16BigEndian(hhea.AsSpan(34), (ushort)this.glyphs.Count);
+ return hhea;
+ }
+
+ ///
+ /// Builds the version 1.0 maxp table with the glyph count and the point and contour maxima.
+ ///
+ /// The table bytes.
+ private byte[] BuildMaxp()
+ {
+ int maxPoints = 0;
+ int maxContours = 0;
+ foreach (GlyphRecord glyph in this.glyphs)
+ {
+ maxPoints = Math.Max(maxPoints, glyph.Contours.Sum(contour => contour.Count));
+ maxContours = Math.Max(maxContours, glyph.Contours.Count);
+ }
+
+ byte[] maxp = new byte[32];
+ BinaryPrimitives.WriteUInt32BigEndian(maxp, 0x00010000);
+ BinaryPrimitives.WriteUInt16BigEndian(maxp.AsSpan(4), (ushort)this.glyphs.Count);
+ BinaryPrimitives.WriteUInt16BigEndian(maxp.AsSpan(6), (ushort)maxPoints);
+ BinaryPrimitives.WriteUInt16BigEndian(maxp.AsSpan(8), (ushort)maxContours);
+ BinaryPrimitives.WriteUInt16BigEndian(maxp.AsSpan(14), 2);
+ return maxp;
+ }
+
+ ///
+ /// Builds the hmtx table: the advance width and left side bearing of every glyph.
+ ///
+ /// The table bytes.
+ private byte[] BuildHmtx()
+ {
+ byte[] hmtx = new byte[this.glyphs.Count * 4];
+ for (int i = 0; i < this.glyphs.Count; i++)
+ {
+ GlyphRecord glyph = this.glyphs[i];
+ glyph.GetBounds(out short gxMin, out _, out _, out _);
+ BinaryPrimitives.WriteUInt16BigEndian(hmtx.AsSpan(i * 4), glyph.AdvanceWidth);
+ BinaryPrimitives.WriteInt16BigEndian(hmtx.AsSpan((i * 4) + 2), glyph.Contours.Count == 0 ? (short)0 : gxMin);
+ }
+
+ return hmtx;
+ }
+
+ ///
+ /// Builds the cmap table with a single format 4 subtable for the Windows Unicode BMP platform.
+ ///
+ /// The table bytes.
+ private byte[] BuildCmap()
+ {
+ // Build format 4 segments from runs of consecutive characters mapping to consecutive glyph ids.
+ List<(ushort Start, ushort End, ushort GlyphStart)> segments = [];
+ foreach (KeyValuePair entry in this.characterMap)
+ {
+ if (segments.Count > 0)
+ {
+ (ushort start, ushort end, ushort glyphStart) = segments[^1];
+ if (entry.Key == end + 1 && entry.Value == glyphStart + (end - start) + 1)
+ {
+ segments[^1] = (start, entry.Key, glyphStart);
+ continue;
+ }
+ }
+
+ segments.Add((entry.Key, entry.Key, entry.Value));
+ }
+
+ segments.Add((0xFFFF, 0xFFFF, 0));
+ ushort segCount = (ushort)segments.Count;
+ ushort segCountX2 = (ushort)(segCount * 2);
+ ushort searchRange = 2;
+ ushort entrySelector = 0;
+ while (searchRange * 2 <= segCountX2)
+ {
+ searchRange *= 2;
+ entrySelector++;
+ }
+
+ int subtableLength = 16 + (segCount * 8);
+ byte[] cmap = new byte[12 + subtableLength];
+ BinaryPrimitives.WriteUInt16BigEndian(cmap.AsSpan(2), 1);
+ BinaryPrimitives.WriteUInt16BigEndian(cmap.AsSpan(4), 3);
+ BinaryPrimitives.WriteUInt16BigEndian(cmap.AsSpan(6), 1);
+ BinaryPrimitives.WriteUInt32BigEndian(cmap.AsSpan(8), 12);
+
+ Span subtable = cmap.AsSpan(12);
+ BinaryPrimitives.WriteUInt16BigEndian(subtable, 4);
+ BinaryPrimitives.WriteUInt16BigEndian(subtable[2..], (ushort)subtableLength);
+ BinaryPrimitives.WriteUInt16BigEndian(subtable[6..], segCountX2);
+ BinaryPrimitives.WriteUInt16BigEndian(subtable[8..], searchRange);
+ BinaryPrimitives.WriteUInt16BigEndian(subtable[10..], entrySelector);
+ BinaryPrimitives.WriteUInt16BigEndian(subtable[12..], (ushort)(segCountX2 - searchRange));
+
+ int endCodes = 14;
+ int startCodes = endCodes + segCountX2 + 2;
+ int idDeltas = startCodes + segCountX2;
+ int idRanges = idDeltas + segCountX2;
+ for (int i = 0; i < segCount; i++)
+ {
+ (ushort start, ushort end, ushort glyphStart) = segments[i];
+ BinaryPrimitives.WriteUInt16BigEndian(subtable[(endCodes + (i * 2))..], end);
+ BinaryPrimitives.WriteUInt16BigEndian(subtable[(startCodes + (i * 2))..], start);
+ ushort delta = i == segCount - 1 ? (ushort)1 : (ushort)(glyphStart - start);
+ BinaryPrimitives.WriteUInt16BigEndian(subtable[(idDeltas + (i * 2))..], delta);
+ BinaryPrimitives.WriteUInt16BigEndian(subtable[(idRanges + (i * 2))..], 0);
+ }
+
+ return cmap;
+ }
+
+ ///
+ /// Builds the version 4 OS/2 table: regular weight and width, the SixL vendor tag, the character
+ /// range, the typographic and Windows vertical metrics and the space break character.
+ ///
+ /// The font-wide minimum x of the ink.
+ /// The font-wide minimum y of the ink.
+ /// The font-wide maximum x of the ink.
+ /// The font-wide maximum y of the ink.
+ /// The table bytes.
+ private byte[] BuildOs2(short xMin, short yMin, short xMax, short yMax)
+ {
+ byte[] os2 = new byte[96];
+ BinaryPrimitives.WriteUInt16BigEndian(os2, 4);
+ BinaryPrimitives.WriteInt16BigEndian(os2.AsSpan(2), (short)this.UnitsPerEm);
+ BinaryPrimitives.WriteUInt16BigEndian(os2.AsSpan(4), 400);
+ BinaryPrimitives.WriteUInt16BigEndian(os2.AsSpan(6), 5);
+ BinaryPrimitives.WriteInt16BigEndian(os2.AsSpan(10), (short)(this.UnitsPerEm / 5));
+ BinaryPrimitives.WriteInt16BigEndian(os2.AsSpan(12), (short)(this.UnitsPerEm / 10));
+ os2[58] = (byte)'S';
+ os2[59] = (byte)'i';
+ os2[60] = (byte)'x';
+ os2[61] = (byte)'L';
+ BinaryPrimitives.WriteUInt16BigEndian(os2.AsSpan(62), 0x0040);
+ BinaryPrimitives.WriteUInt16BigEndian(os2.AsSpan(64), this.characterMap.Keys.Min());
+ BinaryPrimitives.WriteUInt16BigEndian(os2.AsSpan(66), this.characterMap.Keys.Max());
+ BinaryPrimitives.WriteInt16BigEndian(os2.AsSpan(68), this.Ascender);
+ BinaryPrimitives.WriteInt16BigEndian(os2.AsSpan(70), this.Descender);
+ BinaryPrimitives.WriteInt16BigEndian(os2.AsSpan(72), 0);
+ BinaryPrimitives.WriteUInt16BigEndian(os2.AsSpan(74), (ushort)yMax);
+ BinaryPrimitives.WriteUInt16BigEndian(os2.AsSpan(76), (ushort)Math.Max(0, -yMin));
+ BinaryPrimitives.WriteInt16BigEndian(os2.AsSpan(88), this.CapHeight);
+ BinaryPrimitives.WriteUInt16BigEndian(os2.AsSpan(92), ' ');
+ return os2;
+ }
+
+ ///
+ /// Builds the name table for the Windows Unicode platform: copyright, family, subfamily, unique and
+ /// full names, the version string and the PostScript name.
+ ///
+ /// The table bytes.
+ private byte[] BuildName()
+ {
+ (ushort Id, string Value)[] entries =
+ [
+ (0, this.Copyright),
+ (1, this.FamilyName),
+ (2, "Regular"),
+ (3, this.FamilyName),
+ (4, this.FamilyName),
+ (5, "Version 1.000"),
+ (6, this.FamilyName.Replace(" ", string.Empty)),
+ ];
+
+ int stringLength = entries.Sum(entry => entry.Value.Length * 2);
+ byte[] name = new byte[6 + (entries.Length * 12) + stringLength];
+ BinaryPrimitives.WriteUInt16BigEndian(name.AsSpan(2), (ushort)entries.Length);
+ BinaryPrimitives.WriteUInt16BigEndian(name.AsSpan(4), (ushort)(6 + (entries.Length * 12)));
+
+ int record = 6;
+ int offset = 0;
+ int storage = 6 + (entries.Length * 12);
+ foreach ((ushort id, string value) in entries)
+ {
+ BinaryPrimitives.WriteUInt16BigEndian(name.AsSpan(record), 3);
+ BinaryPrimitives.WriteUInt16BigEndian(name.AsSpan(record + 2), 1);
+ BinaryPrimitives.WriteUInt16BigEndian(name.AsSpan(record + 4), 0x0409);
+ BinaryPrimitives.WriteUInt16BigEndian(name.AsSpan(record + 6), id);
+ BinaryPrimitives.WriteUInt16BigEndian(name.AsSpan(record + 8), (ushort)(value.Length * 2));
+ BinaryPrimitives.WriteUInt16BigEndian(name.AsSpan(record + 10), (ushort)offset);
+ foreach (char c in value)
+ {
+ BinaryPrimitives.WriteUInt16BigEndian(name.AsSpan(storage + offset), c);
+ offset += 2;
+ }
+
+ record += 12;
+ }
+
+ return name;
+ }
+
+ ///
+ /// One glyph's closed contours and advance width.
+ ///
+ private sealed class GlyphRecord
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The closed contours, each a list of on-curve points.
+ /// The advance width in font units.
+ public GlyphRecord(IReadOnlyList> contours, ushort advanceWidth)
+ {
+ this.Contours = contours;
+ this.AdvanceWidth = advanceWidth;
+ }
+
+ ///
+ /// Gets the closed contours, each a list of on-curve points.
+ ///
+ public IReadOnlyList> Contours { get; }
+
+ ///
+ /// Gets the advance width in font units.
+ ///
+ public ushort AdvanceWidth { get; }
+
+ ///
+ /// Reports the bounding box of the contours, or a zero box for the empty glyph.
+ ///
+ /// The minimum x of the contours.
+ /// The minimum y of the contours.
+ /// The maximum x of the contours.
+ /// The maximum y of the contours.
+ public void GetBounds(out short xMin, out short yMin, out short xMax, out short yMax)
+ {
+ xMin = short.MaxValue;
+ yMin = short.MaxValue;
+ xMax = short.MinValue;
+ yMax = short.MinValue;
+ foreach (IReadOnlyList<(short X, short Y)> contour in this.Contours)
+ {
+ foreach ((short x, short y) in contour)
+ {
+ xMin = Math.Min(xMin, x);
+ yMin = Math.Min(yMin, y);
+ xMax = Math.Max(xMax, x);
+ yMax = Math.Max(yMax, y);
+ }
+ }
+
+ if (xMin == short.MaxValue)
+ {
+ xMin = 0;
+ yMin = 0;
+ xMax = 0;
+ yMax = 0;
+ }
+ }
+ }
+}
diff --git a/src/ImageSharp.Drawing.WebGPU.ShaderGen/ImageSharp.Drawing.WebGPU.ShaderGen.csproj b/src/ImageSharp.Drawing.WebGPU.ShaderGen/ImageSharp.Drawing.WebGPU.ShaderGen.csproj
index 2fbb26a1..1aa2b63d 100644
--- a/src/ImageSharp.Drawing.WebGPU.ShaderGen/ImageSharp.Drawing.WebGPU.ShaderGen.csproj
+++ b/src/ImageSharp.Drawing.WebGPU.ShaderGen/ImageSharp.Drawing.WebGPU.ShaderGen.csproj
@@ -14,7 +14,6 @@
false
false
false
- false
$(NoWarn);RS1036;SA1513;SA1600;SA1633
false
diff --git a/src/ImageSharp.Drawing.WebGPU.ShaderGen/WgslSourceGenerator.cs b/src/ImageSharp.Drawing.WebGPU.ShaderGen/WgslSourceGenerator.cs
index 73ba63ab..a80c5d94 100644
--- a/src/ImageSharp.Drawing.WebGPU.ShaderGen/WgslSourceGenerator.cs
+++ b/src/ImageSharp.Drawing.WebGPU.ShaderGen/WgslSourceGenerator.cs
@@ -146,7 +146,12 @@ private static string GenerateSource(SourceProductionContext context, Dictionary
_ = builder.AppendLine(" 0,");
_ = builder.AppendLine(" ];");
_ = builder.AppendLine();
- _ = builder.Append(" public static ReadOnlySpan ").Append(propertyName).Append("Code => ").Append(propertyName).AppendLine("CodeBytes;");
+ _ = builder.Append(" public static ReadOnlySpan ")
+ .Append(propertyName)
+ .Append("Code => ")
+ .Append(propertyName)
+ .AppendLine("CodeBytes;");
+
_ = builder.AppendLine();
}
}
diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/FineAreaComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/FineAreaComputeShader.cs
index 1070eeca..ac790c15 100644
--- a/src/ImageSharp.Drawing.WebGPU/Shaders/FineAreaComputeShader.cs
+++ b/src/ImageSharp.Drawing.WebGPU/Shaders/FineAreaComputeShader.cs
@@ -33,7 +33,9 @@ internal static class FineAreaComputeShader
///
/// Specialized shader bytes per target texture format and alpha representation, guarded by its own monitor.
///
- private static readonly Dictionary<(WGPUTextureFormat TextureFormat, PixelAlphaRepresentation AlphaRepresentation, WebGPUTargetNumericEncoding NumericEncoding), byte[]> ShaderCache = [];
+ private static readonly Dictionary<
+ (WGPUTextureFormat TextureFormat, PixelAlphaRepresentation AlphaRepresentation, WebGPUTargetNumericEncoding NumericEncoding),
+ byte[]> ShaderCache = [];
///
/// Gets the WGSL entry point used by this shader.
@@ -167,7 +169,8 @@ private static ShaderTraits GetTraits(
{
WGPUTextureFormat.RGBA8Unorm or WGPUTextureFormat.BGRA8Unorm => "return unpack4x8unorm(pack4x8unorm(color));",
WGPUTextureFormat.RGBA8Snorm => "return unpack4x8snorm(pack4x8snorm(color));",
- WGPUTextureFormat.RGBA16Float => "return vec4(quantize_f16_rtne(color.x), quantize_f16_rtne(color.y), quantize_f16_rtne(color.z), quantize_f16_rtne(color.w));"
+ WGPUTextureFormat.RGBA16Float =>
+ "return vec4(quantize_f16_rtne(color.x), quantize_f16_rtne(color.y), quantize_f16_rtne(color.z), quantize_f16_rtne(color.w));"
};
// Fine shading uses associated colors internally. Unassociated targets cross the storage
diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUColorMatrixLayerEffect.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUColorMatrixLayerEffect.cs
index 51c68dbe..5a1d1b75 100644
--- a/src/ImageSharp.Drawing.WebGPU/WebGPUColorMatrixLayerEffect.cs
+++ b/src/ImageSharp.Drawing.WebGPU/WebGPUColorMatrixLayerEffect.cs
@@ -49,7 +49,23 @@ public WebGPUColorMatrixLayerEffect(ColorMatrix matrix)
// ImageSharp stores its affine colour transform as a 5x4 matrix. The upper 4x4 block maps
// directly to WGSL's row-vector multiplication; M51-M54 form the separate translation row.
- Matrix4x4 transform = new(matrix.M11, matrix.M12, matrix.M13, matrix.M14, matrix.M21, matrix.M22, matrix.M23, matrix.M24, matrix.M31, matrix.M32, matrix.M33, matrix.M34, matrix.M41, matrix.M42, matrix.M43, matrix.M44);
+ Matrix4x4 transform = new(
+ matrix.M11,
+ matrix.M12,
+ matrix.M13,
+ matrix.M14,
+ matrix.M21,
+ matrix.M22,
+ matrix.M23,
+ matrix.M24,
+ matrix.M31,
+ matrix.M32,
+ matrix.M33,
+ matrix.M34,
+ matrix.M41,
+ matrix.M42,
+ matrix.M43,
+ matrix.M44);
// A pass retains an immutable uniform snapshot, so later instances cannot alter this matrix.
this.AddShaderPass(uniforms =>
diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CompositePixels.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CompositePixels.cs
index 4ad9bf93..6d7c9d33 100644
--- a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CompositePixels.cs
+++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CompositePixels.cs
@@ -31,8 +31,14 @@ public sealed partial class WebGPUDrawingBackend
private static readonly CompositePixelRegistration[] CompositePixelRegistrations =
[
- CompositePixelRegistration.Create(WebGPUTextureFormat.Rgba8Snorm, PixelAlphaRepresentation.Unassociated, WebGPUTargetNumericEncoding.SignedUnit),
- CompositePixelRegistration.Create(WebGPUTextureFormat.Rgba8Snorm, PixelAlphaRepresentation.Associated, WebGPUTargetNumericEncoding.SignedUnit),
+ CompositePixelRegistration.Create(
+ WebGPUTextureFormat.Rgba8Snorm,
+ PixelAlphaRepresentation.Unassociated,
+ WebGPUTargetNumericEncoding.SignedUnit),
+ CompositePixelRegistration.Create(
+ WebGPUTextureFormat.Rgba8Snorm,
+ PixelAlphaRepresentation.Associated,
+ WebGPUTargetNumericEncoding.SignedUnit),
CompositePixelRegistration.Create(WebGPUTextureFormat.Rgba16Float, PixelAlphaRepresentation.Unassociated, WebGPUTargetNumericEncoding.Unit),
CompositePixelRegistration.Create(WebGPUTextureFormat.Rgba16Float, PixelAlphaRepresentation.Associated, WebGPUTargetNumericEncoding.Unit),
CompositePixelRegistration.Create(WebGPUTextureFormat.Rgba8Unorm, PixelAlphaRepresentation.Unassociated, WebGPUTargetNumericEncoding.Unit),
diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.Readback.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.Readback.cs
index f9311cfc..c5854175 100644
--- a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.Readback.cs
+++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.Readback.cs
@@ -90,7 +90,8 @@ public void ReadRegion(
if (requiredFeature != default
&& !WebGPURuntime.GetOrCreateDeviceState(api, nativeTarget.DeviceHandle).HasFeature(requiredFeature))
{
- throw new NotSupportedException($"The target device does not support WebGPU feature '{requiredFeature}' required to read back pixel type '{typeof(TPixel).Name}'.");
+ throw new NotSupportedException(
+ $"The target device does not support WebGPU feature '{requiredFeature}' required to read back pixel type '{typeof(TPixel).Name}'.");
}
WGPUQueueImpl* queue = (WGPUQueueImpl*)queueReference.Handle;
diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUNativeSurface.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUNativeSurface.cs
index b9c41398..22c0417d 100644
--- a/src/ImageSharp.Drawing.WebGPU/WebGPUNativeSurface.cs
+++ b/src/ImageSharp.Drawing.WebGPU/WebGPUNativeSurface.cs
@@ -167,12 +167,14 @@ public static unsafe WebGPUNativeSurface Create(
if (requiredFeature != default &&
!WebGPURuntime.GetOrCreateDeviceState(api, deviceHandle).HasFeature(requiredFeature))
{
- throw new NotSupportedException($"The WebGPU device does not support required feature '{requiredFeature}' for texture format '{targetDescriptor.Format}'.");
+ throw new NotSupportedException(
+ $"The WebGPU device does not support required feature '{requiredFeature}' for texture format '{targetDescriptor.Format}'.");
}
WGPUTextureDescriptor textureDescriptor = new()
{
- usage = (ulong)(TextureUsage.RenderAttachment | TextureUsage.CopySrc | TextureUsage.CopyDst | TextureUsage.TextureBinding | TextureUsage.StorageBinding),
+ usage = (ulong)(TextureUsage.RenderAttachment | TextureUsage.CopySrc | TextureUsage.CopyDst |
+ TextureUsage.TextureBinding | TextureUsage.StorageBinding),
dimension = WGPUTextureDimension._2D,
size = new WGPUExtent3D((uint)width, (uint)height, 1),
format = textureFormat,
diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPURenderTarget.cs b/src/ImageSharp.Drawing.WebGPU/WebGPURenderTarget.cs
index 4a6f3daa..fe962350 100644
--- a/src/ImageSharp.Drawing.WebGPU/WebGPURenderTarget.cs
+++ b/src/ImageSharp.Drawing.WebGPU/WebGPURenderTarget.cs
@@ -93,7 +93,13 @@ public WebGPURenderTarget(
/// The target width in pixels.
/// The target height in pixels.
public WebGPURenderTarget(Configuration configuration, WebGPUTextureFormat format, PixelAlphaRepresentation alphaRepresentation, int width, int height)
- : this(new WebGPUDeviceContext(configuration), true, WebGPUDrawingBackend.CreateOffscreenTargetDescriptor(format, alphaRepresentation), width, height, isPresentationSurface: false)
+ : this(
+ new WebGPUDeviceContext(configuration),
+ true,
+ WebGPUDrawingBackend.CreateOffscreenTargetDescriptor(format, alphaRepresentation),
+ width,
+ height,
+ isPresentationSurface: false)
{
}
diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPURuntime.DeviceSharedState.cs b/src/ImageSharp.Drawing.WebGPU/WebGPURuntime.DeviceSharedState.cs
index 33bc0542..67748799 100644
--- a/src/ImageSharp.Drawing.WebGPU/WebGPURuntime.DeviceSharedState.cs
+++ b/src/ImageSharp.Drawing.WebGPU/WebGPURuntime.DeviceSharedState.cs
@@ -379,7 +379,8 @@ public void CompletePipelineWarmup()
/// The pipeline cache key.
/// when the pipeline exists; otherwise, .
public bool HasCompositeComputePipeline(string pipelineKey)
- => this.compositeComputePipelines.TryGetValue(pipelineKey, out CompositeComputePipelineInfrastructure? infrastructure) && infrastructure.Pipeline is not null;
+ => this.compositeComputePipelines.TryGetValue(pipelineKey, out CompositeComputePipelineInfrastructure? infrastructure) &&
+ infrastructure.Pipeline is not null;
///
/// Rents a pooled map-readable status buffer, or creates one when the pool has no buffer
diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPURuntime.cs b/src/ImageSharp.Drawing.WebGPU/WebGPURuntime.cs
index 03ecbefc..199c2147 100644
--- a/src/ImageSharp.Drawing.WebGPU/WebGPURuntime.cs
+++ b/src/ImageSharp.Drawing.WebGPU/WebGPURuntime.cs
@@ -683,13 +683,15 @@ public static string CreateEnvironmentExceptionMessage(WebGPUEnvironmentError er
WebGPUEnvironmentError.InstanceCreationFailed => "The WebGPU runtime could not create an instance.",
WebGPUEnvironmentError.AdapterRequestTimedOut => "Timed out while waiting for the WebGPU adapter request callback.",
WebGPUEnvironmentError.AdapterRequestFailed => "The WebGPU runtime failed to acquire a WebGPU adapter.",
- WebGPUEnvironmentError.SoftwareAdapterUnsupported => "The only available WebGPU adapter is a software rasterizer, which this backend does not support; use the CPU drawing backend instead.",
+ WebGPUEnvironmentError.SoftwareAdapterUnsupported =>
+ "The only available WebGPU adapter is a software rasterizer, which this backend does not support; use the CPU drawing backend instead.",
WebGPUEnvironmentError.DeviceRequestTimedOut => "Timed out while waiting for the WebGPU device request callback.",
WebGPUEnvironmentError.DeviceRequestFailed => "The WebGPU runtime failed to acquire a WebGPU device.",
WebGPUEnvironmentError.QueueAcquisitionFailed => "The WebGPU runtime acquired a device but could not retrieve its default queue.",
WebGPUEnvironmentError.DeviceAcquisitionFailed => "The WebGPU runtime failed to provision a WebGPU device and queue.",
WebGPUEnvironmentError.ComputePipelineCreationFailed => "The isolated WebGPU compute-pipeline probe reported failure.",
- WebGPUEnvironmentError.ComputePipelineProbeProcessFailed => "The isolated WebGPU compute-pipeline probe process terminated before it could report a result.",
+ WebGPUEnvironmentError.ComputePipelineProbeProcessFailed =>
+ "The isolated WebGPU compute-pipeline probe process terminated before it could report a result.",
WebGPUEnvironmentError.WgpuExtensionUnavailable => "The required WGPU extension is unavailable.",
_ => "The WebGPU runtime failed for an unknown reason."
};
diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUSceneConfig.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUSceneConfig.cs
index 027e39ca..ec86e669 100644
--- a/src/ImageSharp.Drawing.WebGPU/WebGPUSceneConfig.cs
+++ b/src/ImageSharp.Drawing.WebGPU/WebGPUSceneConfig.cs
@@ -18,7 +18,11 @@ internal readonly struct WebGPUSceneConfig
/// The planned GPU buffer sizes for this flush.
/// The current dynamic scratch capacities to expose to the shaders.
/// The tile-row window rendered by this staged-scene attempt.
- public WebGPUSceneConfig(WebGPUSceneWorkgroupCounts workgroupCounts, WebGPUSceneBufferSizes bufferSizes, WebGPUSceneBumpSizes bumpSizes, WebGPUSceneChunkWindow chunkWindow)
+ public WebGPUSceneConfig(
+ WebGPUSceneWorkgroupCounts workgroupCounts,
+ WebGPUSceneBufferSizes bufferSizes,
+ WebGPUSceneBumpSizes bumpSizes,
+ WebGPUSceneChunkWindow chunkWindow)
{
this.WorkgroupCounts = workgroupCounts;
this.BufferSizes = bufferSizes;
@@ -913,7 +917,11 @@ public static WebGPUSceneBufferSizes Create(WebGPUEncodedScene scene, WebGPUScen
/// The current dynamic scratch capacities for the staged pipeline.
/// The tile-row window rendered by this staged-scene attempt.
/// The planned GPU buffer sizes for this encoded range and chunk window.
- public static WebGPUSceneBufferSizes Create(WebGPUEncodedScene scene, WebGPUSceneRange range, WebGPUSceneBumpSizes bumpSizes, WebGPUSceneChunkWindow chunkWindow)
+ public static WebGPUSceneBufferSizes Create(
+ WebGPUEncodedScene scene,
+ WebGPUSceneRange range,
+ WebGPUSceneBumpSizes bumpSizes,
+ WebGPUSceneChunkWindow chunkWindow)
{
WebGPUSceneWorkgroupCounts workgroupCounts = WebGPUSceneWorkgroupCounts.Create(range, chunkWindow);
uint pathTagWgs = workgroupCounts.PathReduceX;
diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUSceneDispatch.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUSceneDispatch.cs
index 40ab9f87..a46b3d78 100644
--- a/src/ImageSharp.Drawing.WebGPU/WebGPUSceneDispatch.cs
+++ b/src/ImageSharp.Drawing.WebGPU/WebGPUSceneDispatch.cs
@@ -208,7 +208,12 @@ public static WebGPUStagedScene CreateStagedScene(
throw new InvalidOperationException(error);
}
- if (!TryValidateBindingSizes(encodedScene, config, flushContext.ScratchBufferBindingSizeLimit, out BindingLimitFailure bindingLimitFailure, out error))
+ if (!TryValidateBindingSizes(
+ encodedScene,
+ config,
+ flushContext.ScratchBufferBindingSizeLimit,
+ out BindingLimitFailure bindingLimitFailure,
+ out error))
{
if (!IsChunkableBindingFailure(bindingLimitFailure.Buffer))
{
@@ -232,7 +237,14 @@ public static WebGPUStagedScene CreateStagedScene(
true);
}
- if (!WebGPUSceneResources.TryCreate(flushContext, encodedScene, config, baseColor, ref resourceArena, out WebGPUSceneResourceSet resources, out error))
+ if (!WebGPUSceneResources.TryCreate(
+ flushContext,
+ encodedScene,
+ config,
+ baseColor,
+ ref resourceArena,
+ out WebGPUSceneResourceSet resources,
+ out error))
{
throw new InvalidOperationException(error ?? "Failed to create WebGPU scene resources.");
}
@@ -401,7 +413,8 @@ private static bool TryValidateEncodedClipStream(
{
if (drawTagStart < 0 || drawTagCount < 0 || drawTagStart + drawTagCount > encodedScene.DrawTagCount)
{
- error = $"The WebGPU {label} draw-tag range [{drawTagStart}, {drawTagStart + drawTagCount}) is outside the encoded draw-tag stream length {encodedScene.DrawTagCount}.";
+ error = $"The WebGPU {label} draw-tag range [{drawTagStart}, {drawTagStart + drawTagCount}) " +
+ $"is outside the encoded draw-tag stream length {encodedScene.DrawTagCount}.";
return false;
}
@@ -435,13 +448,15 @@ private static bool TryValidateEncodedClipStream(
if (sceneOffset + drawMonoid.SceneOffset > expectedDrawDataWordCount)
{
- error = $"The WebGPU {label} draw tag {drawTagStart + i} reads draw-data words [{sceneOffset}, {sceneOffset + drawMonoid.SceneOffset}), but the stream declares {expectedDrawDataWordCount} words.";
+ error = $"The WebGPU {label} draw tag {drawTagStart + i} reads draw-data words [{sceneOffset}, {sceneOffset + drawMonoid.SceneOffset}), " +
+ $"but the stream declares {expectedDrawDataWordCount} words.";
return false;
}
if (infoOffset + drawMonoid.InfoOffset > expectedInfoWordCount)
{
- error = $"The WebGPU {label} draw tag {drawTagStart + i} writes info words [{infoOffset}, {infoOffset + drawMonoid.InfoOffset}), but the stream declares {expectedInfoWordCount} words.";
+ error = $"The WebGPU {label} draw tag {drawTagStart + i} writes info words [{infoOffset}, {infoOffset + drawMonoid.InfoOffset}), " +
+ $"but the stream declares {expectedInfoWordCount} words.";
return false;
}
@@ -475,13 +490,15 @@ private static bool TryValidateEncodedClipStream(
long parentDrawIndex = clipDrawStack[depth];
if (parentPathIndex < 0 || parentPathIndex >= expectedPathCount)
{
- error = $"The WebGPU {label} EndClip at draw tag {drawTagStart + i} resolves to path {parentPathIndex}, but the stream declares {expectedPathCount} paths.";
+ error = $"The WebGPU {label} EndClip at draw tag {drawTagStart + i} resolves to path {parentPathIndex}, " +
+ $"but the stream declares {expectedPathCount} paths.";
return false;
}
if (parentDrawIndex < 0 || parentDrawIndex >= drawTagCount)
{
- error = $"The WebGPU {label} EndClip at draw tag {drawTagStart + i} resolves to draw tag {parentDrawIndex}, but the stream contains {drawTagCount} draw tags.";
+ error = $"The WebGPU {label} EndClip at draw tag {drawTagStart + i} resolves to draw tag {parentDrawIndex}, " +
+ $"but the stream contains {drawTagCount} draw tags.";
return false;
}
@@ -557,32 +574,186 @@ public static bool TryValidateBindingSizes(
{
bindingLimitFailure = BindingLimitFailure.None;
WebGPUSceneBufferSizes bufferSizes = config.BufferSizes;
- nuint infoBinDataByteLength = checked(config.BufferSizes.Info.ByteLength + config.BufferSizes.BinData.ByteLength + config.BufferSizes.BinHeaders.ByteLength);
- if (!TryValidateBufferSize(GetBindingByteLength(1), "scene config", BindingLimitBuffer.None, maxStorageBufferBindingSize, out bindingLimitFailure, out error) ||
- !TryValidateBufferSize(GetBindingByteLength(encodedScene.SceneWordCount), "scene data", BindingLimitBuffer.None, maxStorageBufferBindingSize, out bindingLimitFailure, out error) ||
- !TryValidateBufferSize(bufferSizes.PathReduced.ByteLength, "path reduced", BindingLimitBuffer.None, maxStorageBufferBindingSize, out bindingLimitFailure, out error) ||
- !TryValidateBufferSize(bufferSizes.PathReduced2.ByteLength, "path reduced2", BindingLimitBuffer.None, maxStorageBufferBindingSize, out bindingLimitFailure, out error) ||
- !TryValidateBufferSize(bufferSizes.PathReducedScan.ByteLength, "path reduced scan", BindingLimitBuffer.None, maxStorageBufferBindingSize, out bindingLimitFailure, out error) ||
- !TryValidateBufferSize(bufferSizes.PathMonoids.ByteLength, "path monoids", BindingLimitBuffer.None, maxStorageBufferBindingSize, out bindingLimitFailure, out error) ||
- !TryValidateBufferSize(bufferSizes.PathBboxes.ByteLength, "path bboxes", BindingLimitBuffer.None, maxStorageBufferBindingSize, out bindingLimitFailure, out error) ||
- !TryValidateBufferSize(bufferSizes.DrawReduced.ByteLength, "draw reduced", BindingLimitBuffer.None, maxStorageBufferBindingSize, out bindingLimitFailure, out error) ||
- !TryValidateBufferSize(bufferSizes.DrawMonoids.ByteLength, "draw monoids", BindingLimitBuffer.None, maxStorageBufferBindingSize, out bindingLimitFailure, out error) ||
- !TryValidateBufferSize(infoBinDataByteLength, "scene info/bin data", BindingLimitBuffer.None, maxStorageBufferBindingSize, out bindingLimitFailure, out error) ||
- !TryValidateBufferSize(bufferSizes.ClipInputs.ByteLength, "clip inputs", BindingLimitBuffer.None, maxStorageBufferBindingSize, out bindingLimitFailure, out error) ||
- !TryValidateBufferSize(bufferSizes.ClipElements.ByteLength, "clip elements", BindingLimitBuffer.None, maxStorageBufferBindingSize, out bindingLimitFailure, out error) ||
- !TryValidateBufferSize(bufferSizes.ClipBics.ByteLength, "clip bics", BindingLimitBuffer.None, maxStorageBufferBindingSize, out bindingLimitFailure, out error) ||
- !TryValidateBufferSize(bufferSizes.ClipBboxes.ByteLength, "clip bboxes", BindingLimitBuffer.None, maxStorageBufferBindingSize, out bindingLimitFailure, out error) ||
- !TryValidateBufferSize(bufferSizes.DrawBboxes.ByteLength, "draw bboxes", BindingLimitBuffer.None, maxStorageBufferBindingSize, out bindingLimitFailure, out error) ||
- !TryValidateBufferSize(bufferSizes.Paths.ByteLength, "scene paths", BindingLimitBuffer.None, maxStorageBufferBindingSize, out bindingLimitFailure, out error) ||
- !TryValidateBufferSize(bufferSizes.Lines.ByteLength, "scene lines", BindingLimitBuffer.None, maxStorageBufferBindingSize, out bindingLimitFailure, out error) ||
- !TryValidateBufferSize(bufferSizes.BinHeaders.ByteLength, "bin headers", BindingLimitBuffer.None, maxStorageBufferBindingSize, out bindingLimitFailure, out error) ||
- !TryValidateBufferSize(bufferSizes.IndirectCount.ByteLength, "indirect count", BindingLimitBuffer.None, maxStorageBufferBindingSize, out bindingLimitFailure, out error) ||
- !TryValidateBufferSize(bufferSizes.PathRows.ByteLength, "path rows", BindingLimitBuffer.PathRows, maxStorageBufferBindingSize, out bindingLimitFailure, out error) ||
- !TryValidateBufferSize(bufferSizes.PathTiles.ByteLength, "path tiles", BindingLimitBuffer.PathTiles, maxStorageBufferBindingSize, out bindingLimitFailure, out error) ||
- !TryValidateBufferSize(bufferSizes.SegCounts.ByteLength, "segment counts", BindingLimitBuffer.SegCounts, maxStorageBufferBindingSize, out bindingLimitFailure, out error) ||
- !TryValidateBufferSize(bufferSizes.Segments.ByteLength, "segments", BindingLimitBuffer.Segments, maxStorageBufferBindingSize, out bindingLimitFailure, out error) ||
- !TryValidateBufferSize(bufferSizes.BlendSpill.ByteLength, "blend spill", BindingLimitBuffer.BlendSpill, maxStorageBufferBindingSize, out bindingLimitFailure, out error) ||
- !TryValidateBufferSize(bufferSizes.Ptcl.ByteLength, "ptcl", BindingLimitBuffer.Ptcl, maxStorageBufferBindingSize, out bindingLimitFailure, out error))
+ nuint infoBinDataByteLength = checked(
+ config.BufferSizes.Info.ByteLength +
+ config.BufferSizes.BinData.ByteLength +
+ config.BufferSizes.BinHeaders.ByteLength);
+
+ if (!TryValidateBufferSize(
+ GetBindingByteLength(1),
+ "scene config",
+ BindingLimitBuffer.None,
+ maxStorageBufferBindingSize,
+ out bindingLimitFailure,
+ out error) ||
+ !TryValidateBufferSize(
+ GetBindingByteLength(encodedScene.SceneWordCount),
+ "scene data",
+ BindingLimitBuffer.None,
+ maxStorageBufferBindingSize,
+ out bindingLimitFailure,
+ out error) ||
+ !TryValidateBufferSize(
+ bufferSizes.PathReduced.ByteLength,
+ "path reduced",
+ BindingLimitBuffer.None,
+ maxStorageBufferBindingSize,
+ out bindingLimitFailure,
+ out error) ||
+ !TryValidateBufferSize(
+ bufferSizes.PathReduced2.ByteLength,
+ "path reduced2",
+ BindingLimitBuffer.None,
+ maxStorageBufferBindingSize,
+ out bindingLimitFailure,
+ out error) ||
+ !TryValidateBufferSize(
+ bufferSizes.PathReducedScan.ByteLength,
+ "path reduced scan",
+ BindingLimitBuffer.None,
+ maxStorageBufferBindingSize,
+ out bindingLimitFailure,
+ out error) ||
+ !TryValidateBufferSize(
+ bufferSizes.PathMonoids.ByteLength,
+ "path monoids",
+ BindingLimitBuffer.None,
+ maxStorageBufferBindingSize,
+ out bindingLimitFailure,
+ out error) ||
+ !TryValidateBufferSize(
+ bufferSizes.PathBboxes.ByteLength,
+ "path bboxes",
+ BindingLimitBuffer.None,
+ maxStorageBufferBindingSize,
+ out bindingLimitFailure,
+ out error) ||
+ !TryValidateBufferSize(
+ bufferSizes.DrawReduced.ByteLength,
+ "draw reduced",
+ BindingLimitBuffer.None,
+ maxStorageBufferBindingSize,
+ out bindingLimitFailure,
+ out error) ||
+ !TryValidateBufferSize(
+ bufferSizes.DrawMonoids.ByteLength,
+ "draw monoids",
+ BindingLimitBuffer.None,
+ maxStorageBufferBindingSize,
+ out bindingLimitFailure,
+ out error) ||
+ !TryValidateBufferSize(
+ infoBinDataByteLength,
+ "scene info/bin data",
+ BindingLimitBuffer.None,
+ maxStorageBufferBindingSize,
+ out bindingLimitFailure,
+ out error) ||
+ !TryValidateBufferSize(
+ bufferSizes.ClipInputs.ByteLength,
+ "clip inputs",
+ BindingLimitBuffer.None,
+ maxStorageBufferBindingSize,
+ out bindingLimitFailure,
+ out error) ||
+ !TryValidateBufferSize(
+ bufferSizes.ClipElements.ByteLength,
+ "clip elements",
+ BindingLimitBuffer.None,
+ maxStorageBufferBindingSize,
+ out bindingLimitFailure,
+ out error) ||
+ !TryValidateBufferSize(
+ bufferSizes.ClipBics.ByteLength,
+ "clip bics",
+ BindingLimitBuffer.None,
+ maxStorageBufferBindingSize,
+ out bindingLimitFailure,
+ out error) ||
+ !TryValidateBufferSize(
+ bufferSizes.ClipBboxes.ByteLength,
+ "clip bboxes",
+ BindingLimitBuffer.None,
+ maxStorageBufferBindingSize,
+ out bindingLimitFailure,
+ out error) ||
+ !TryValidateBufferSize(
+ bufferSizes.DrawBboxes.ByteLength,
+ "draw bboxes",
+ BindingLimitBuffer.None,
+ maxStorageBufferBindingSize,
+ out bindingLimitFailure,
+ out error) ||
+ !TryValidateBufferSize(
+ bufferSizes.Paths.ByteLength,
+ "scene paths",
+ BindingLimitBuffer.None,
+ maxStorageBufferBindingSize,
+ out bindingLimitFailure,
+ out error) ||
+ !TryValidateBufferSize(
+ bufferSizes.Lines.ByteLength,
+ "scene lines",
+ BindingLimitBuffer.None,
+ maxStorageBufferBindingSize,
+ out bindingLimitFailure,
+ out error) ||
+ !TryValidateBufferSize(
+ bufferSizes.BinHeaders.ByteLength,
+ "bin headers",
+ BindingLimitBuffer.None,
+ maxStorageBufferBindingSize,
+ out bindingLimitFailure,
+ out error) ||
+ !TryValidateBufferSize(
+ bufferSizes.IndirectCount.ByteLength,
+ "indirect count",
+ BindingLimitBuffer.None,
+ maxStorageBufferBindingSize,
+ out bindingLimitFailure,
+ out error) ||
+ !TryValidateBufferSize(
+ bufferSizes.PathRows.ByteLength,
+ "path rows",
+ BindingLimitBuffer.PathRows,
+ maxStorageBufferBindingSize,
+ out bindingLimitFailure,
+ out error) ||
+ !TryValidateBufferSize(
+ bufferSizes.PathTiles.ByteLength,
+ "path tiles",
+ BindingLimitBuffer.PathTiles,
+ maxStorageBufferBindingSize,
+ out bindingLimitFailure,
+ out error) ||
+ !TryValidateBufferSize(
+ bufferSizes.SegCounts.ByteLength,
+ "segment counts",
+ BindingLimitBuffer.SegCounts,
+ maxStorageBufferBindingSize,
+ out bindingLimitFailure,
+ out error) ||
+ !TryValidateBufferSize(
+ bufferSizes.Segments.ByteLength,
+ "segments",
+ BindingLimitBuffer.Segments,
+ maxStorageBufferBindingSize,
+ out bindingLimitFailure,
+ out error) ||
+ !TryValidateBufferSize(
+ bufferSizes.BlendSpill.ByteLength,
+ "blend spill",
+ BindingLimitBuffer.BlendSpill,
+ maxStorageBufferBindingSize,
+ out bindingLimitFailure,
+ out error) ||
+ !TryValidateBufferSize(
+ bufferSizes.Ptcl.ByteLength,
+ "ptcl",
+ BindingLimitBuffer.Ptcl,
+ maxStorageBufferBindingSize,
+ out bindingLimitFailure,
+ out error))
{
return false;
}
@@ -1645,7 +1816,13 @@ public static unsafe bool TryRenderStagedScene(
int targetWidth = target.Bounds.Width;
int targetHeight = target.Bounds.Height;
- if (!WebGPUDrawingBackend.TryCreateCompositionTexture(flushContext, targetWidth, targetHeight, out WGPUTextureImpl* outputTexture, out WGPUTextureViewImpl* outputTextureView, out error))
+ if (!WebGPUDrawingBackend.TryCreateCompositionTexture(
+ flushContext,
+ targetWidth,
+ targetHeight,
+ out WGPUTextureImpl* outputTexture,
+ out WGPUTextureViewImpl* outputTextureView,
+ out error))
{
return false;
}
@@ -1688,7 +1865,12 @@ public static unsafe bool TryRenderStagedScene(
// The readback map blocks until the GPU finishes everything.
if (!TryEnqueueSchedulingStatusReadback(flushContext, scheduling.BumpBuffer, currentArena.ReadbackBuffer, 0, out error) ||
!WebGPUDrawingBackend.TrySubmitWithIndex(flushContext, out ulong schedulingSubmissionIndex) ||
- !TryReadSchedulingStatus(flushContext, currentArena.ReadbackBuffer, schedulingSubmissionIndex, out GpuSceneBumpAllocators bumpAllocators, out error))
+ !TryReadSchedulingStatus(
+ flushContext,
+ currentArena.ReadbackBuffer,
+ schedulingSubmissionIndex,
+ out GpuSceneBumpAllocators bumpAllocators,
+ out error))
{
return false;
}
@@ -1834,7 +2016,13 @@ private static unsafe bool TryRenderSegmentChunkedStagedScene(
int targetHeight = target.Bounds.Height;
nuint maxStorageBufferBindingSize = flushContext.ScratchBufferBindingSizeLimit;
- if (!WebGPUDrawingBackend.TryCreateCompositionTexture(flushContext, targetWidth, targetHeight, out WGPUTextureImpl* outputTexture, out WGPUTextureViewImpl* outputTextureView, out error))
+ if (!WebGPUDrawingBackend.TryCreateCompositionTexture(
+ flushContext,
+ targetWidth,
+ targetHeight,
+ out WGPUTextureImpl* outputTexture,
+ out WGPUTextureViewImpl* outputTextureView,
+ out error))
{
return false;
}
@@ -1879,7 +2067,9 @@ private static unsafe bool TryRenderSegmentChunkedStagedScene(
try
{
// Track each chunk's config sizes and tile heights for the batch readback at the end.
- using IMemoryOwner chunkBumpSizeOwner = flushContext.MemoryAllocator.Allocate(checked((int)Math.Max(totalTileHeight, 1U)));
+ using IMemoryOwner chunkBumpSizeOwner =
+ flushContext.MemoryAllocator.Allocate(checked((int)Math.Max(totalTileHeight, 1U)));
+
using IMemoryOwner chunkTileHeightOwner = flushContext.MemoryAllocator.Allocate(checked((int)Math.Max(totalTileHeight, 1U)));
Span chunkAttemptBumpSizes = chunkBumpSizeOwner.Memory.Span;
Span chunkAttemptTileHeights = chunkTileHeightOwner.Memory.Span;
@@ -1910,7 +2100,12 @@ private static unsafe bool TryRenderSegmentChunkedStagedScene(
? WebGPUSceneConfig.Create(encodedScene, stagedScene.Range.Value, chunkBumpSize, chunkWindow)
: WebGPUSceneConfig.Create(encodedScene, chunkBumpSize, chunkWindow);
- if (!TryValidateBindingSizes(encodedScene, chunkConfig, maxStorageBufferBindingSize, out BindingLimitFailure bindingLimitFailure, out error))
+ if (!TryValidateBindingSizes(
+ encodedScene,
+ chunkConfig,
+ maxStorageBufferBindingSize,
+ out BindingLimitFailure bindingLimitFailure,
+ out error))
{
if (IsChunkableBindingFailure(bindingLimitFailure.Buffer))
{
@@ -1930,9 +2125,20 @@ private static unsafe bool TryRenderSegmentChunkedStagedScene(
// Chunk fits. Re-ensure the arena for this chunk's buffer sizes (may be
// different from the previous chunk if the scene has non-uniform density).
WebGPUStagedScene chunkScene = stagedScene.Range.HasValue
- ? new WebGPUStagedScene(flushContext, encodedScene, stagedScene.Range.Value, chunkConfig, stagedScene.Resources, BindingLimitFailure.None)
+ ? new WebGPUStagedScene(
+ flushContext,
+ encodedScene,
+ stagedScene.Range.Value,
+ chunkConfig,
+ stagedScene.Resources,
+ BindingLimitFailure.None)
: new WebGPUStagedScene(flushContext, encodedScene, chunkConfig, stagedScene.Resources, BindingLimitFailure.None, false, false);
- WebGPUSceneSchedulingArena currentArena = EnsureSchedulingArena(flushContext, chunkConfig.BufferSizes, readbackByteLength, ref schedulingArena);
+ WebGPUSceneSchedulingArena currentArena = EnsureSchedulingArena(
+ flushContext,
+ chunkConfig.BufferSizes,
+ readbackByteLength,
+ ref schedulingArena);
+
WGPUBufferImpl* statusReadbackBuffer = deferOverflowCheck ? deferredReadbackBuffer : currentArena.ReadbackBuffer;
// A first chunk that already covers the full tile height renders through the
@@ -2108,7 +2314,12 @@ private static unsafe bool TryRenderChunkAttempt(
if (reuseSharedSchedulingState)
{
- if (!TryDispatchChunkLocalSchedulingStages(ref stagedScene, schedulingArena, resetChunkLocalBumpAllocators, out WebGPUSceneSchedulingResources sharedScheduling, out error))
+ if (!TryDispatchChunkLocalSchedulingStages(
+ ref stagedScene,
+ schedulingArena,
+ resetChunkLocalBumpAllocators,
+ out WebGPUSceneSchedulingResources sharedScheduling,
+ out error))
{
return false;
}
@@ -2178,7 +2389,12 @@ private static unsafe bool TryRecordChunkAttempt(
WGPUBufferImpl* headerSourceBuffer = CreateAndUploadCopySourceBuffer(stagedScene.FlushContext, in header);
if (!TryEnqueueSceneHeaderCopy(stagedScene.FlushContext, headerSourceBuffer, stagedScene.Resources.HeaderBuffer, out error) ||
- !TryDispatchChunkLocalSchedulingStages(ref stagedScene, schedulingArena, resetChunkLocalBumpAllocators, out WebGPUSceneSchedulingResources scheduling, out error))
+ !TryDispatchChunkLocalSchedulingStages(
+ ref stagedScene,
+ schedulingArena,
+ resetChunkLocalBumpAllocators,
+ out WebGPUSceneSchedulingResources scheduling,
+ out error))
{
return false;
}
@@ -2559,7 +2775,8 @@ private static uint ClampEstimate(long estimate, ulong maxStorageBufferBindingSi
/// when the failed binding shrinks with the chunk window; otherwise, .
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsChunkableBindingFailure(BindingLimitBuffer buffer)
- => buffer is BindingLimitBuffer.PathRows or BindingLimitBuffer.PathTiles or BindingLimitBuffer.SegCounts or BindingLimitBuffer.Segments or BindingLimitBuffer.BlendSpill or BindingLimitBuffer.Ptcl;
+ => buffer is BindingLimitBuffer.PathRows or BindingLimitBuffer.PathTiles or BindingLimitBuffer.SegCounts or
+ BindingLimitBuffer.Segments or BindingLimitBuffer.BlendSpill or BindingLimitBuffer.Ptcl;
///
/// Scales one count by the chunk's tile-height ratio while keeping the result non-zero.
@@ -2829,7 +3046,14 @@ private static unsafe bool TryDispatchPathtagScan(
entries[2] = CreateBufferBinding(2, parentBuffer, parentBufferSize);
entries[3] = CreateBufferBinding(3, resources.PathMonoidBuffer, pathMonoidBufferSize);
- return recording.TryRecord(useSmallVariant ? WebGPUSceneShaderId.PathtagScanSmall : WebGPUSceneShaderId.PathtagScan, entries, 4, dispatchX, 1, 1, out error);
+ return recording.TryRecord(
+ useSmallVariant ? WebGPUSceneShaderId.PathtagScanSmall : WebGPUSceneShaderId.PathtagScan,
+ entries,
+ 4,
+ dispatchX,
+ 1,
+ 1,
+ out error);
}
///
@@ -3311,7 +3535,8 @@ void Callback(WGPUMapAsyncStatus status, void* userData)
flushContext.Api.BufferMapAsync(readbackBuffer, MapMode.Read, 0, (nuint)sizeof(GpuSceneBumpAllocators), callback, null);
using (WebGPUHandle.HandleReference deviceReference = flushContext.DeviceHandle.AcquireReference())
{
- if (!WaitForMapSignal(flushContext.Api, (WGPUDeviceImpl*)deviceReference.Handle, mapReady, submissionIndex) || mapStatus != WGPUMapAsyncStatus.Success)
+ if (!WaitForMapSignal(flushContext.Api, (WGPUDeviceImpl*)deviceReference.Handle, mapReady, submissionIndex) ||
+ mapStatus != WGPUMapAsyncStatus.Success)
{
error = $"Failed to map staged-scene scheduling status with status '{mapStatus}'.";
return false;
@@ -3450,7 +3675,8 @@ void Callback(WGPUMapAsyncStatus status, void* userData)
flushContext.Api.BufferMapAsync(readbackBuffer, MapMode.Read, 0, mappedByteLength, callback, null);
using (WebGPUHandle.HandleReference deviceReference = flushContext.DeviceHandle.AcquireReference())
{
- if (!WaitForMapSignal(flushContext.Api, (WGPUDeviceImpl*)deviceReference.Handle, mapReady, submissionIndex) || mapStatus != WGPUMapAsyncStatus.Success)
+ if (!WaitForMapSignal(flushContext.Api, (WGPUDeviceImpl*)deviceReference.Handle, mapReady, submissionIndex) ||
+ mapStatus != WGPUMapAsyncStatus.Success)
{
error = $"Failed to map staged-scene chunk scheduling status with status '{mapStatus}'.";
return false;
@@ -4054,7 +4280,11 @@ private static unsafe bool TryDispatchFineArea(
entries[0] = CreateBufferBinding(0, resources.HeaderBuffer, (nuint)sizeof(GpuSceneConfig));
entries[1] = CreateBufferBinding(1, scheduling.SegmentBuffer, bufferSizes.Segments.ByteLength);
entries[2] = CreateBufferBinding(2, scheduling.PtclBuffer, bufferSizes.Ptcl.ByteLength);
- entries[3] = CreateBufferBinding(3, resources.InfoBinDataBuffer, checked(bufferSizes.Info.ByteLength + bufferSizes.BinData.ByteLength + bufferSizes.BinHeaders.ByteLength));
+ entries[3] = CreateBufferBinding(
+ 3,
+ resources.InfoBinDataBuffer,
+ checked(bufferSizes.Info.ByteLength + bufferSizes.BinData.ByteLength + bufferSizes.BinHeaders.ByteLength));
+
entries[4] = CreateBufferBinding(4, scheduling.BlendBuffer, bufferSizes.BlendSpill.ByteLength);
entries[5] = new WGPUBindGroupEntry { binding = 5, textureView = outputTextureView };
entries[6] = new WGPUBindGroupEntry { binding = 6, textureView = resources.GradientTextureView };
@@ -4170,7 +4400,12 @@ private static unsafe bool TryExecuteComputeRecording(
foreach (WebGPUSceneComputeCommand command in recording.Commands)
{
string shaderName = GetShaderDebugName(command.ShaderId);
- if (!TryResolveComputeShader(flushContext.DeviceState, command.ShaderId, out WGPUBindGroupLayoutImpl* bindGroupLayout, out WGPUComputePipelineImpl* pipeline, out error))
+ if (!TryResolveComputeShader(
+ flushContext.DeviceState,
+ command.ShaderId,
+ out WGPUBindGroupLayoutImpl* bindGroupLayout,
+ out WGPUComputePipelineImpl* pipeline,
+ out error))
{
error = error is null ? null : $"{error} Stage: {shaderName}.";
return false;
@@ -4306,7 +4541,8 @@ bool LayoutFactory(WebGPU api, WGPUDeviceImpl* device, out WGPUBindGroupLayoutIm
WebGPUSceneShaderId.PathtagReduce => PathtagReduceComputeShader.TryCreateBindGroupLayout(api, device, out layout, out layoutError),
WebGPUSceneShaderId.PathtagReduce2 => PathtagReduce2ComputeShader.TryCreateBindGroupLayout(api, device, out layout, out layoutError),
WebGPUSceneShaderId.PathtagScan1 => PathtagScan1ComputeShader.TryCreateBindGroupLayout(api, device, out layout, out layoutError),
- WebGPUSceneShaderId.PathtagScan or WebGPUSceneShaderId.PathtagScanSmall => PathtagScanComputeShader.TryCreateBindGroupLayout(api, device, out layout, out layoutError),
+ WebGPUSceneShaderId.PathtagScan or WebGPUSceneShaderId.PathtagScanSmall =>
+ PathtagScanComputeShader.TryCreateBindGroupLayout(api, device, out layout, out layoutError),
WebGPUSceneShaderId.BboxClear => BboxClearComputeShader.TryCreateBindGroupLayout(api, device, out layout, out layoutError),
WebGPUSceneShaderId.PathLowering => PathLoweringComputeShader.TryCreateBindGroupLayout(api, device, out layout, out layoutError),
WebGPUSceneShaderId.DrawReduce => DrawReduceComputeShader.TryCreateBindGroupLayout(api, device, out layout, out layoutError),
@@ -4468,13 +4704,22 @@ public static unsafe void BeginFinePipelineWarmup(WebGPURuntime.DeviceSharedStat
}
deviceState.RegisterPipelineWarmup();
- (WebGPURuntime.DeviceSharedState DeviceState, WGPUTextureFormat TextureFormat, WebGPUTargetDescriptor Descriptor) warmupState = (deviceState, textureFormat, targetDescriptor);
+ (WebGPURuntime.DeviceSharedState DeviceState, WGPUTextureFormat TextureFormat, WebGPUTargetDescriptor Descriptor) warmupState =
+ (deviceState, textureFormat, targetDescriptor);
+
bool queued = ThreadPool.UnsafeQueueUserWorkItem(
static (state) =>
{
try
{
- _ = TryResolveFinePipeline(state.DeviceState, state.TextureFormat, state.Descriptor.AlphaRepresentation, state.Descriptor.NumericEncoding, out _, out _, out _);
+ _ = TryResolveFinePipeline(
+ state.DeviceState,
+ state.TextureFormat,
+ state.Descriptor.AlphaRepresentation,
+ state.Descriptor.NumericEncoding,
+ out _,
+ out _,
+ out _);
}
catch
{
@@ -4502,7 +4747,10 @@ public static unsafe void BeginFinePipelineWarmup(WebGPURuntime.DeviceSharedStat
/// The target numeric encoding.
/// The cache key.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static string GetFinePipelineKey(WGPUTextureFormat textureFormat, PixelAlphaRepresentation alphaRepresentation, WebGPUTargetNumericEncoding numericEncoding)
+ private static string GetFinePipelineKey(
+ WGPUTextureFormat textureFormat,
+ PixelAlphaRepresentation alphaRepresentation,
+ WebGPUTargetNumericEncoding numericEncoding)
=> $"{FineAreaPipelineKey}/{textureFormat}/{alphaRepresentation}/{numericEncoding}";
///
@@ -4771,7 +5019,8 @@ private static bool TryValidateBufferSize(
if (byteLength > maxStorageBufferBindingSize)
{
bindingLimitFailure = new BindingLimitFailure(bindingLimitBuffer, byteLength, maxStorageBufferBindingSize);
- error = $"The staged-scene {bufferName} buffer requires {byteLength} bytes, exceeding the current WebGPU binding limit of {maxStorageBufferBindingSize} bytes.";
+ error = $"The staged-scene {bufferName} buffer requires {byteLength} bytes, " +
+ $"exceeding the current WebGPU binding limit of {maxStorageBufferBindingSize} bytes.";
return false;
}
diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUSceneEncoder.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUSceneEncoder.cs
index 73de865d..91fdd852 100644
--- a/src/ImageSharp.Drawing.WebGPU/WebGPUSceneEncoder.cs
+++ b/src/ImageSharp.Drawing.WebGPU/WebGPUSceneEncoder.cs
@@ -236,7 +236,10 @@ public static bool TryEncode(
// Boundary-snapped ranges can be empty; empty ranges skip clip seeding so they
// do not emit balanced-but-useless clip records.
int commandStart = partitionBoundaries is null ? (partitionIndex * commandCount) / partitionCount : partitionBoundaries[partitionIndex];
- int commandEnd = partitionBoundaries is null ? ((partitionIndex + 1) * commandCount) / partitionCount : partitionBoundaries[partitionIndex + 1];
+ int commandEnd = partitionBoundaries is null
+ ? ((partitionIndex + 1) * commandCount) / partitionCount
+ : partitionBoundaries[partitionIndex + 1];
+
SceneEncodingPlan partitionPlan = plan.GetPartitionPlan(partitionIndex);
SupportedSubsetSceneEncoding partitionEncoding = new(allocator, targetRectangle, partitionPlan);
@@ -358,7 +361,17 @@ public static bool TryEncodeOrdered(
try
{
SceneEncodingCheckpoint rangeStart = encoding.BeginPackedIndependentRange(targetBounds, ClipBlendMode, replayActiveClips: true);
- if (!TryEncodeOrderedOperations(scene, 0, scene.CommandCount, targetBounds, 0, ref encoding, operations, ref rangeStart, ref nextLayerTargetId, out error))
+ if (!TryEncodeOrderedOperations(
+ scene,
+ 0,
+ scene.CommandCount,
+ targetBounds,
+ 0,
+ ref encoding,
+ operations,
+ ref rangeStart,
+ ref nextLayerTargetId,
+ out error))
{
encodedScene = WebGPUEncodedScene.Empty;
return false;
@@ -445,7 +458,17 @@ private static bool TryEncodeOrderedOperations(
operations.Add(new WebGPUSceneOperation(layerBounds, layerTargetId, targetId));
SceneEncodingCheckpoint childStart = encoding.BeginPackedIndependentRange(layerBounds, ClipBlendMode, replayActiveClips: true);
- if (!TryEncodeOrderedOperations(scene, i + 1, layerEnd, layerBounds, layerTargetId, ref encoding, operations, ref childStart, ref nextLayerTargetId, out error))
+ if (!TryEncodeOrderedOperations(
+ scene,
+ i + 1,
+ layerEnd,
+ layerBounds,
+ layerTargetId,
+ ref encoding,
+ operations,
+ ref childStart,
+ ref nextLayerTargetId,
+ out error))
{
return false;
}
@@ -598,7 +621,12 @@ private static bool TryAppendApplyCommand(
IWebGPUShaderEffectSource? shaderEffect = source.ApplyEffect as IWebGPUShaderEffectSource;
bool hasNativeEffect = shaderEffect is not null;
GpuImageSource imageSource = hasNativeEffect
- ? new GpuImageSource(new Size(outputRect.Width, outputRect.Height), brushOffset, WrapMode.Repeat, WrapMode.Repeat, WebGPUShaderEffectWorkingTexture.Descriptor)
+ ? new GpuImageSource(
+ new Size(outputRect.Width, outputRect.Height),
+ brushOffset,
+ WrapMode.Repeat,
+ WrapMode.Repeat,
+ WebGPUShaderEffectWorkingTexture.Descriptor)
: new GpuImageSource(new Size(outputRect.Width, outputRect.Height), brushOffset, WrapMode.Repeat, WrapMode.Repeat);
RasterizerOptions rasterizerOptions = source.RasterizerOptions;
@@ -620,7 +648,8 @@ private static bool TryAppendApplyCommand(
WebGPUSceneRange drawRange = encoding.EndPackedIndependentRange(targetBounds, drawStart, closeActiveClips: true);
operation = hasNativeEffect
? new WebGPUSceneOperation(new WebGPUShaderEffectSceneItem(shaderEffect!, outputRect, outputRect, source.ApplyWriteBackOffset, drawRange))
- : new WebGPUSceneOperation(new WebGPUApplySceneItem(source.ApplyOperation, source.ApplyEffect, outputRect, outputRect, source.ApplyWriteBackOffset, drawRange));
+ : new WebGPUSceneOperation(
+ new WebGPUApplySceneItem(source.ApplyOperation, source.ApplyEffect, outputRect, outputRect, source.ApplyWriteBackOffset, drawRange));
error = null;
return true;
}
@@ -1101,7 +1130,10 @@ public static SceneEncodingPlan Create(
partitionIndex =>
{
int commandStart = partitionBoundaries is null ? (partitionIndex * commandCount) / partitionCount : partitionBoundaries[partitionIndex];
- int commandEnd = partitionBoundaries is null ? ((partitionIndex + 1) * commandCount) / partitionCount : partitionBoundaries[partitionIndex + 1];
+ int commandEnd = partitionBoundaries is null
+ ? ((partitionIndex + 1) * commandCount) / partitionCount
+ : partitionBoundaries[partitionIndex + 1];
+
SceneCapacityEstimate estimate = default;
for (int i = commandStart; i < commandEnd; i++)
@@ -2541,7 +2573,8 @@ private void PadPathTagsToSegmentBoundary()
/// The scene transform to classify.
/// when the transform is a pure translation; otherwise .
private static bool IsProfileCompatibleTransform(in GpuSceneTransform transform)
- => transform.M11 == 1F && transform.M22 == 1F && transform.M12 == 0F && transform.M21 == 0F && transform.M14 == 0F && transform.M24 == 0F && transform.M44 == 1F;
+ => transform.M11 == 1F && transform.M22 == 1F && transform.M12 == 0F && transform.M21 == 0F &&
+ transform.M14 == 0F && transform.M24 == 0F && transform.M44 == 1F;
///
/// Encodes one visible fill command into the path, draw, style, and auxiliary payload streams.
@@ -4628,13 +4661,16 @@ public static WebGPUEncodedScene Resolve(
if (partition.GradientRowCount > 0)
{
- partition.GradientPixels.CopyTo(gradientPixelsOwner!.Memory.Span.Slice(gradientPixelOffset, partition.GradientRowCount * GradientRowWordCount));
+ partition.GradientPixels.CopyTo(
+ gradientPixelsOwner!.Memory.Span.Slice(gradientPixelOffset, partition.GradientRowCount * GradientRowWordCount));
+
gradientPixelOffset += partition.GradientRowCount * GradientRowWordCount;
}
if (partition.PathGradientDataWordCount > 0)
{
- partition.PathGradientData.CopyTo(pathGradientDataOwner!.Memory.Span.Slice(pathGradientDataOffset, partition.PathGradientDataWordCount));
+ partition.PathGradientData.CopyTo(
+ pathGradientDataOwner!.Memory.Span.Slice(pathGradientDataOffset, partition.PathGradientDataWordCount));
}
gradientRowOffset += partition.GradientRowCount;
@@ -5991,7 +6027,13 @@ private static int EncodeRectangleClipPath(
continue;
}
- AppendRectangleContour(new RectangleF(rectangle.X, rectangle.Y, rectangle.Width, rectangle.Height), translateX, translateY, ref pathTags, ref pathData);
+ AppendRectangleContour(
+ new RectangleF(rectangle.X, rectangle.Y, rectangle.Width, rectangle.Height),
+ translateX,
+ translateY,
+ ref pathTags,
+ ref pathData);
+
lineCount += 2;
}
@@ -7770,7 +7812,9 @@ public ReadOnlyMemory SceneData
/// pixels wide, matching the encoder's fixed gradient ramp width.
///
public ReadOnlyMemory GradientPixels
- => this.gradientPixelsOwner is null ? ReadOnlyMemory.Empty : this.gradientPixelsOwner.Memory[..(this.GradientRowCount * WebGPUSceneEncoder.GradientRowWordCount)];
+ => this.gradientPixelsOwner is null
+ ? ReadOnlyMemory.Empty
+ : this.gradientPixelsOwner.Memory[..(this.GradientRowCount * WebGPUSceneEncoder.GradientRowWordCount)];
///
/// Gets the encoded path-gradient payload stream.
diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUSceneOperations.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUSceneOperations.cs
index 04503e2b..4874a501 100644
--- a/src/ImageSharp.Drawing.WebGPU/WebGPUSceneOperations.cs
+++ b/src/ImageSharp.Drawing.WebGPU/WebGPUSceneOperations.cs
@@ -450,7 +450,12 @@ internal sealed class WebGPUShaderEffectSceneItem
/// The bounds within which the working texture is written back.
/// The offset subtracted from when reading source pixels.
/// The encoded working-texture write-back range.
- public WebGPUShaderEffectSceneItem(IWebGPUShaderEffectSource effect, Rectangle inputRect, Rectangle outputRect, Point readOffset, WebGPUSceneRange drawRange)
+ public WebGPUShaderEffectSceneItem(
+ IWebGPUShaderEffectSource effect,
+ Rectangle inputRect,
+ Rectangle outputRect,
+ Point readOffset,
+ WebGPUSceneRange drawRange)
{
this.Effect = effect;
this.InputRect = inputRect;
diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUSceneResources.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUSceneResources.cs
index d0180ef2..bf4b879e 100644
--- a/src/ImageSharp.Drawing.WebGPU/WebGPUSceneResources.cs
+++ b/src/ImageSharp.Drawing.WebGPU/WebGPUSceneResources.cs
@@ -158,7 +158,14 @@ private static bool TryCreateCore(
return false;
}
- if (!TryCreateImageAtlasTexture(flushContext, scene, range, flushContext.TextureFormat, externalTextureView, out WGPUTextureViewImpl* imageAtlasTextureView, out error))
+ if (!TryCreateImageAtlasTexture(
+ flushContext,
+ scene,
+ range,
+ flushContext.TextureFormat,
+ externalTextureView,
+ out WGPUTextureViewImpl* imageAtlasTextureView,
+ out error))
{
return false;
}
@@ -166,7 +173,11 @@ private static bool TryCreateCore(
// Compute byte lengths for the two variable-size data buffers. The combined
// info/bin-data buffer is laid out as [info | brush data | bin data | bin headers],
// matching config.bin_data_start and config.brush_data_base in Shared/config.wgsl.
- nuint infoBinDataByteLength = checked(GetBindingByteLength(infoWordCount + scene.PathGradientDataWordCount) + config.BufferSizes.BinData.ByteLength + config.BufferSizes.BinHeaders.ByteLength);
+ nuint infoBinDataByteLength = checked(
+ GetBindingByteLength(infoWordCount + scene.PathGradientDataWordCount) +
+ config.BufferSizes.BinData.ByteLength +
+ config.BufferSizes.BinHeaders.ByteLength);
+
nuint sceneByteLength = GetBindingByteLength(scene.SceneWordCount);
// Reuse arena buffers if all capacities fit this scene.
diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUShaderModuleSource.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUShaderModuleSource.cs
index 1bd4fc2a..ae9dacac 100644
--- a/src/ImageSharp.Drawing.WebGPU/WebGPUShaderModuleSource.cs
+++ b/src/ImageSharp.Drawing.WebGPU/WebGPUShaderModuleSource.cs
@@ -123,7 +123,8 @@ public static WebGPUShaderModuleSource Create(
_ = builder.AppendLine("@group(0) @binding(3) var imagesharp_filtering_sampler: sampler;");
_ = builder.AppendLine();
_ = builder.AppendLine("fn imagesharp_layer_load_scaled(position: vec2) -> vec4 {");
- _ = builder.AppendLine(" if (any(position < imagesharp_framework.imagesharp_valid_min) || any(position >= imagesharp_framework.imagesharp_valid_max)) {");
+ _ = builder.AppendLine(
+ " if (any(position < imagesharp_framework.imagesharp_valid_min) || any(position >= imagesharp_framework.imagesharp_valid_max)) {");
_ = builder.AppendLine(" return vec4(0.0);");
_ = builder.AppendLine(" }");
_ = builder.AppendLine();
@@ -233,10 +234,13 @@ public static WebGPUShaderModuleSource Create(
_ = builder.AppendLine(" let imagesharp_minimum = vec2(floor(imagesharp_texel_position));");
_ = builder.AppendLine(" let imagesharp_maximum = imagesharp_minimum + vec2(1);");
_ = builder.AppendLine();
- _ = builder.AppendLine(" if (all(imagesharp_minimum >= imagesharp_framework.imagesharp_valid_min) && all(imagesharp_maximum < imagesharp_framework.imagesharp_valid_max)) {");
+ _ = builder.AppendLine(
+ " if (all(imagesharp_minimum >= imagesharp_framework.imagesharp_valid_min) && " +
+ "all(imagesharp_maximum < imagesharp_framework.imagesharp_valid_max)) {");
_ = builder.AppendLine(" let imagesharp_source_position = vec2(imagesharp_framework.imagesharp_source_origin) + position;");
_ = builder.AppendLine(" let imagesharp_coordinate = imagesharp_source_position / vec2(textureDimensions(imagesharp_source));");
- _ = builder.AppendLine(" let imagesharp_native = textureSampleLevel(imagesharp_source, imagesharp_filtering_sampler, imagesharp_coordinate, 0.0);");
+ _ = builder.AppendLine(
+ " let imagesharp_native = textureSampleLevel(imagesharp_source, imagesharp_filtering_sampler, imagesharp_coordinate, 0.0);");
if (sourceDescriptor.NumericEncoding == WebGPUTargetNumericEncoding.SignedUnit)
{
@@ -342,7 +346,9 @@ private static void AppendBorderCoordinateFunction(
_ = builder.AppendLine(" let imagesharp_period = imagesharp_extent * 2;");
_ = builder.AppendLine(" let imagesharp_remainder = (value - imagesharp_minimum) % imagesharp_period;");
_ = builder.AppendLine(" let imagesharp_offset = (imagesharp_remainder + imagesharp_period) % imagesharp_period;");
- _ = builder.AppendLine(" let imagesharp_reflected = select(imagesharp_period - 1 - imagesharp_offset, imagesharp_offset, imagesharp_offset < imagesharp_extent);");
+ _ = builder.AppendLine(
+ " let imagesharp_reflected = select(imagesharp_period - 1 - imagesharp_offset, " +
+ "imagesharp_offset, imagesharp_offset < imagesharp_extent);");
_ = builder.AppendLine(" return imagesharp_minimum + imagesharp_reflected;");
break;
@@ -355,7 +361,8 @@ private static void AppendBorderCoordinateFunction(
_ = builder.AppendLine(" let imagesharp_period = (imagesharp_extent * 2) - 2;");
_ = builder.AppendLine(" let imagesharp_remainder = (value - imagesharp_minimum) % imagesharp_period;");
_ = builder.AppendLine(" let imagesharp_offset = (imagesharp_remainder + imagesharp_period) % imagesharp_period;");
- _ = builder.AppendLine(" let imagesharp_reflected = select(imagesharp_period - imagesharp_offset, imagesharp_offset, imagesharp_offset < imagesharp_extent);");
+ _ = builder.AppendLine(
+ " let imagesharp_reflected = select(imagesharp_period - imagesharp_offset, imagesharp_offset, imagesharp_offset < imagesharp_extent);");
_ = builder.AppendLine(" return imagesharp_minimum + imagesharp_reflected;");
break;
}
diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUShaderProgram.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUShaderProgram.cs
index b73f925e..41ce5fd1 100644
--- a/src/ImageSharp.Drawing.WebGPU/WebGPUShaderProgram.cs
+++ b/src/ImageSharp.Drawing.WebGPU/WebGPUShaderProgram.cs
@@ -23,7 +23,10 @@ internal sealed class WebGPUShaderProgram
private readonly object moduleSourceSync = new();
private readonly Dictionary<
- (PixelAlphaRepresentation AlphaRepresentation, WebGPUTargetNumericEncoding NumericEncoding, BorderWrappingMode? XBorderMode, BorderWrappingMode? YBorderMode),
+ (PixelAlphaRepresentation AlphaRepresentation,
+ WebGPUTargetNumericEncoding NumericEncoding,
+ BorderWrappingMode? XBorderMode,
+ BorderWrappingMode? YBorderMode),
WebGPUShaderModuleSource> moduleSources = [];
///
diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUShaderSourceValidator.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUShaderSourceValidator.cs
index c20f7dd8..20ebf86a 100644
--- a/src/ImageSharp.Drawing.WebGPU/WebGPUShaderSourceValidator.cs
+++ b/src/ImageSharp.Drawing.WebGPU/WebGPUShaderSourceValidator.cs
@@ -411,7 +411,9 @@ public static void Validate(string source, string parameterName)
bool isPublicUniformBinding = name.SequenceEqual("imagesharp_uniforms");
if (hasReservedPrefix && (!isPublicUniformBinding || isDeclarationName))
{
- throw new ArgumentException("WGSL identifiers beginning with 'imagesharp_' are reserved by ImageSharp; only imagesharp_uniforms may be read by user source.", parameterName);
+ throw new ArgumentException(
+ "WGSL identifiers beginning with 'imagesharp_' are reserved by ImageSharp; only imagesharp_uniforms may be read by user source.",
+ parameterName);
}
// Function parameters and structure members have no declaration keyword. Retain
diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUShaderUniformLayout.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUShaderUniformLayout.cs
index c8ed499d..498b5c41 100644
--- a/src/ImageSharp.Drawing.WebGPU/WebGPUShaderUniformLayout.cs
+++ b/src/ImageSharp.Drawing.WebGPU/WebGPUShaderUniformLayout.cs
@@ -94,7 +94,9 @@ public WebGPUShaderUniformLayout(ReadOnlySpan uniforms)
// limit here keeps every successfully constructed program portable across conforming devices.
if (this.ByteLength > MaximumByteLength)
{
- throw new ArgumentException($"The uniform layout requires {this.ByteLength} bytes; WebGPU layer-effect uniforms are limited to {MaximumByteLength} bytes.", nameof(uniforms));
+ throw new ArgumentException(
+ $"The uniform layout requires {this.ByteLength} bytes; WebGPU layer-effect uniforms are limited to {MaximumByteLength} bytes.",
+ nameof(uniforms));
}
this.WgslStructureDeclaration = CreateWgslStructureDeclaration(this.members);
diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUSurfaceResources.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUSurfaceResources.cs
index f3dc8af7..72eaa28e 100644
--- a/src/ImageSharp.Drawing.WebGPU/WebGPUSurfaceResources.cs
+++ b/src/ImageSharp.Drawing.WebGPU/WebGPUSurfaceResources.cs
@@ -666,7 +666,8 @@ private static void NegotiateSurfaceConfiguration(
TextureUsage supportedUsage = (TextureUsage)capabilities.usages;
if ((supportedUsage & RequiredSurfaceUsage) != RequiredSurfaceUsage)
{
- throw new NotSupportedException($"The WebGPU surface does not support the texture usages required by the drawing backend. Supported usages: '{supportedUsage}'.");
+ throw new NotSupportedException(
+ $"The WebGPU surface does not support the texture usages required by the drawing backend. Supported usages: '{supportedUsage}'.");
}
// A same-format native copy avoids the presentation shader entirely. Surfaces that
@@ -716,7 +717,10 @@ private static void NegotiateSurfaceConfiguration(
foreach (WebGPUTextureFormat fallbackFormat in fallbackFormats)
{
- WebGPUDrawingBackend.GetCompositeTextureFormatInfo(fallbackFormat, out WGPUTextureFormat fallbackNativeFormat, out WGPUFeatureName fallbackFeature);
+ WebGPUDrawingBackend.GetCompositeTextureFormatInfo(
+ fallbackFormat,
+ out WGPUTextureFormat fallbackNativeFormat,
+ out WGPUFeatureName fallbackFeature);
if (fallbackFeature != default && !api.AdapterHasFeature(adapter, fallbackFeature))
{
diff --git a/src/ImageSharp.Drawing/Barcodes/AlphanumericCharacterSet.cs b/src/ImageSharp.Drawing/Barcodes/AlphanumericCharacterSet.cs
new file mode 100644
index 00000000..7e50de50
--- /dev/null
+++ b/src/ImageSharp.Drawing/Barcodes/AlphanumericCharacterSet.cs
@@ -0,0 +1,59 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Buffers;
+
+namespace SixLabors.ImageSharp.Drawing.Barcodes;
+
+///
+/// The 43 character alphanumeric set that several bar code standards define identically: the digits, the capital letters, the space and the symbols
+/// -.$/+%. ISO/IEC 16388 Table A.1 gives it for Code 39, ANSI/AIM BC5-1995 gives the same 43 as the first part of the Code 93 set, and the Health
+/// Industry Bar Code standard gives the same again. Each standard values a character by its position in the set, and each takes its check character modulo the
+/// set size.
+///
+internal static class AlphanumericCharacterSet
+{
+ ///
+ /// The characters in value order, so the value of a character is its index.
+ ///
+ private const string Characters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ-. $/+%";
+
+ ///
+ /// The characters of the set, for one vectorized search over a whole text.
+ ///
+ private static readonly SearchValues Set = SearchValues.Create(Characters);
+
+ ///
+ /// Returns the index of the first character of a text that is outside the set.
+ ///
+ /// The text.
+ /// The index, or -1 when the set holds every character.
+ public static int IndexOfAnyExcept(ReadOnlySpan text) => text.IndexOfAnyExcept(Set);
+
+ ///
+ /// Gets the value the set assigns the given code point, or a negative number when the code point is outside the set. Taking a code point rather than a
+ /// UTF-16 unit keeps a surrogate pair from truncating onto a character the set carries.
+ ///
+ /// The code point to value.
+ /// The value, or a negative number.
+ public static int Value(int codePoint) => codePoint switch
+ {
+ >= '0' and <= '9' => codePoint - '0',
+ >= 'A' and <= 'Z' => codePoint - 'A' + 10,
+ '-' => 36,
+ '.' => 37,
+ ' ' => 38,
+ '$' => 39,
+ '/' => 40,
+ '+' => 41,
+ '%' => 42,
+ _ => -1,
+ };
+
+ ///
+ /// Returns the check character of a sum of character values: the character whose value is the sum modulo the set size.
+ ///
+ /// The sum of the values of the characters the check character covers.
+ /// The check character.
+ public static char CheckCharacter(int sum) => Characters[sum % Characters.Length];
+}
diff --git a/src/ImageSharp.Drawing/Barcodes/Aztec/AztecCodeCompactSymbology.cs b/src/ImageSharp.Drawing/Barcodes/Aztec/AztecCodeCompactSymbology.cs
new file mode 100644
index 00000000..d48e8165
--- /dev/null
+++ b/src/ImageSharp.Drawing/Barcodes/Aztec/AztecCodeCompactSymbology.cs
@@ -0,0 +1,66 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Drawing.Barcodes;
+
+///
+/// Compact Aztec Code, ISO/IEC 24778: an Aztec Code symbol in the compact format, 15 x 15 to 27 x 27 modules with a 2-ring bullseye and 1 to 4 data layers. By
+/// default the symbol is the smallest compact symbol that holds the data with 23 percent of its capacity plus 3 codewords for error correction.
+///
+public sealed class AztecCodeCompactSymbology : BarcodeSymbology
+{
+ ///
+ /// Initializes a new instance of the class at the recommended error correction level of 23 percent.
+ ///
+ public AztecCodeCompactSymbology()
+ : this(AztecEncoder.DefaultErrorCorrectionPercentage)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The minimum percentage of the symbol capacity that error correction takes, 5 to 95. Three more check codewords
+ /// are always added.
+ public AztecCodeCompactSymbology(int errorCorrectionPercentage)
+ {
+ Guard.MustBeBetweenOrEqualTo(errorCorrectionPercentage, 5, 95, nameof(errorCorrectionPercentage));
+ this.ErrorCorrectionPercentage = errorCorrectionPercentage;
+ }
+
+ ///
+ /// Initializes a new instance of the class with a fixed number of data layers.
+ ///
+ /// The minimum percentage of the symbol capacity that error correction takes, 5 to 95. Three more check codewords
+ /// are always added.
+ /// The number of data layers, 1 to 4.
+ public AztecCodeCompactSymbology(int errorCorrectionPercentage, int layers)
+ : this(errorCorrectionPercentage)
+ {
+ Guard.MustBeBetweenOrEqualTo(layers, 1, 4, nameof(layers));
+ this.Layers = layers;
+ }
+
+ ///
+ /// Gets the minimum error correction percentage.
+ ///
+ public int ErrorCorrectionPercentage { get; }
+
+ ///
+ /// Gets the number of data layers, or when the symbol is the smallest compact symbol that holds the data.
+ ///
+ public int? Layers { get; }
+
+ ///
+ internal override void Encode(ReadOnlySpan text, BarcodeOptions options, ref BarcodeSymbol symbol)
+ {
+ // Clause 12 of ISO/IEC 24778: "This symbology specification places no limits on the size of X."
+ AztecEncoder.BuildSymbol(
+ text,
+ AztecFormat.Compact,
+ this.ErrorCorrectionPercentage,
+ this.Layers ?? 0,
+ options.ResolveXDimension(0, DefaultMatrixXDimension, double.PositiveInfinity),
+ ref symbol);
+ }
+}
diff --git a/src/ImageSharp.Drawing/Barcodes/Aztec/AztecCodeSymbology.cs b/src/ImageSharp.Drawing/Barcodes/Aztec/AztecCodeSymbology.cs
new file mode 100644
index 00000000..c9b8b56c
--- /dev/null
+++ b/src/ImageSharp.Drawing/Barcodes/Aztec/AztecCodeSymbology.cs
@@ -0,0 +1,67 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Drawing.Barcodes;
+
+///
+/// The Aztec Code symbology, ISO/IEC 24778: a square matrix of 15 x 15 to 151 x 151 modules around a bullseye finder, with no quiet zone. It carries the
+/// characters U+0000 to U+00FF with Reed-Solomon error correction. By default the symbol is the smallest that holds the data with 23 percent of its capacity
+/// plus 3 codewords for error correction: a compact symbol of 1 to 4 layers, or a full-range symbol of 4 to 32 layers.
+///
+public sealed class AztecCodeSymbology : BarcodeSymbology
+{
+ ///
+ /// Initializes a new instance of the class at the recommended error correction level of 23 percent.
+ ///
+ public AztecCodeSymbology()
+ : this(AztecEncoder.DefaultErrorCorrectionPercentage)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The minimum percentage of the symbol capacity that error correction takes, 5 to 95. Three more check codewords
+ /// are always added.
+ public AztecCodeSymbology(int errorCorrectionPercentage)
+ {
+ Guard.MustBeBetweenOrEqualTo(errorCorrectionPercentage, 5, 95, nameof(errorCorrectionPercentage));
+ this.ErrorCorrectionPercentage = errorCorrectionPercentage;
+ }
+
+ ///
+ /// Initializes a new instance of the class with a full-range symbol of a fixed number of data layers.
+ ///
+ /// The minimum percentage of the symbol capacity that error correction takes, 5 to 95. Three more check codewords
+ /// are always added.
+ /// The number of data layers, 1 to 32.
+ public AztecCodeSymbology(int errorCorrectionPercentage, int layers)
+ : this(errorCorrectionPercentage)
+ {
+ Guard.MustBeBetweenOrEqualTo(layers, 1, 32, nameof(layers));
+ this.Layers = layers;
+ }
+
+ ///
+ /// Gets the minimum error correction percentage.
+ ///
+ public int ErrorCorrectionPercentage { get; }
+
+ ///
+ /// Gets the number of data layers of the full-range symbol, or when the symbol is the smallest that holds the data.
+ ///
+ public int? Layers { get; }
+
+ ///
+ internal override void Encode(ReadOnlySpan text, BarcodeOptions options, ref BarcodeSymbol symbol)
+ {
+ // Clause 12 of ISO/IEC 24778: "This symbology specification places no limits on the size of X."
+ AztecEncoder.BuildSymbol(
+ text,
+ this.Layers is null ? AztecFormat.Auto : AztecFormat.Full,
+ this.ErrorCorrectionPercentage,
+ this.Layers ?? 0,
+ options.ResolveXDimension(0, DefaultMatrixXDimension, double.PositiveInfinity),
+ ref symbol);
+ }
+}
diff --git a/src/ImageSharp.Drawing/Barcodes/Aztec/AztecEncoder.cs b/src/ImageSharp.Drawing/Barcodes/Aztec/AztecEncoder.cs
new file mode 100644
index 00000000..0c159e66
--- /dev/null
+++ b/src/ImageSharp.Drawing/Barcodes/Aztec/AztecEncoder.cs
@@ -0,0 +1,1253 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Diagnostics;
+using SixLabors.ImageSharp.Drawing.Helpers;
+
+namespace SixLabors.ImageSharp.Drawing.Barcodes;
+
+///
+/// Encodes Aztec Code symbols, ISO/IEC 24778. The text becomes a bit stream in the five code sets and the byte shift of Table 2. The bit stream becomes B-bit
+/// codewords with no codeword of all zeros or all ones. Reed-Solomon check codewords fill the symbol. The codewords spiral outward from the Core Symbol in
+/// reverse order. The mode search finds the shortest sequence of latches, shifts and characters for the whole message.
+///
+internal static class AztecEncoder
+{
+ ///
+ /// The recommended error correction level of section 11.3: "23% of symbol capacity plus 3 codewords more".
+ ///
+ public const int DefaultErrorCorrectionPercentage = 23;
+
+ ///
+ /// The longest input. The largest symbol holds 1664 codewords of 12 bits, 19968 bits, and no character takes fewer than 2.5 bits: the punctuation pairs of
+ /// Table 2 put two characters in 5 bits.
+ ///
+ public const int MaximumLength = 19968 * 2 / 5;
+
+ ///
+ /// The check codewords added to the percentage, section 11.3: "plus always, for data security, three additional checkwords".
+ ///
+ private const int ExtraCheckCodewords = 3;
+
+ ///
+ /// The most bytes one B/S carries, section 7.3.2.2: 31 with a 5-bit length, or 31 more than the 11-bit length, at most 2047.
+ ///
+ private const int MaximumByteRun = 31 + 2047;
+
+ ///
+ /// The number of states of the search: the five code sets of Table 2 and the byte state.
+ ///
+ private const int StateCount = 6;
+
+ ///
+ /// The number of code sets that hold characters: every state but .
+ ///
+ private const int CharacterSetCount = 5;
+
+ ///
+ /// The number of compact sizes, 1 to 4 layers, which lead .
+ ///
+ private const int CompactSizeCount = 4;
+
+ ///
+ /// The cost of a state that the search has not reached.
+ ///
+ private const int Unreached = 1_000_000;
+
+ ///
+ /// The sequence element of U/L, the latch to the upper code set.
+ ///
+ private const int LatchUpper = -2;
+
+ ///
+ /// The sequence element of L/L, the latch to the lower code set.
+ ///
+ private const int LatchLower = -3;
+
+ ///
+ /// The sequence element of M/L, the latch to the mixed code set.
+ ///
+ private const int LatchMixed = -4;
+
+ ///
+ /// The sequence element of P/L, the latch to the punctuation code set.
+ ///
+ private const int LatchPunct = -5;
+
+ ///
+ /// The sequence element of D/L, the latch to the digit code set.
+ ///
+ private const int LatchDigit = -6;
+
+ ///
+ /// The sequence element of U/S, the shift to the upper code set for one character.
+ ///
+ private const int ShiftUpper = -7;
+
+ ///
+ /// The sequence element of P/S, the shift to the punctuation code set for one character.
+ ///
+ private const int ShiftPunct = -8;
+
+ ///
+ /// The sequence element of B/S, the shift to the byte state.
+ ///
+ private const int ShiftBinary = -9;
+
+ ///
+ /// The sequence element of the punctuation pair CR LF.
+ ///
+ private const int PairCrLf = -11;
+
+ ///
+ /// The sequence element of the punctuation pair period and space.
+ ///
+ private const int PairPeriodSpace = -12;
+
+ ///
+ /// The sequence element of the punctuation pair comma and space.
+ ///
+ private const int PairCommaSpace = -13;
+
+ ///
+ /// The sequence element of the punctuation pair colon and space.
+ ///
+ private const int PairColonSpace = -14;
+
+ ///
+ /// The field of the mode message, section 7.2.4: GF(16) with the prime modulus polynomial x^4 + x + 1.
+ ///
+ private static readonly GaloisField ModeField = new(16, 19);
+
+ ///
+ /// The field of the 6-bit data codewords, Table 3: GF(64) with the polynomial 67. Each data field builds its tables on first use, so a symbol builds only
+ /// the field of its codeword size. The GF(4096) tables alone hold 12286 ints.
+ ///
+ private static readonly Lazy Field6 = new(() => new GaloisField(64, 67));
+
+ ///
+ /// The field of the 8-bit data codewords, Table 3: GF(256) with the polynomial 301.
+ ///
+ private static readonly Lazy Field8 = new(() => new GaloisField(256, 301));
+
+ ///
+ /// The field of the 10-bit data codewords, Table 3: GF(1024) with the polynomial 1033.
+ ///
+ private static readonly Lazy Field10 = new(() => new GaloisField(1024, 1033));
+
+ ///
+ /// The field of the 12-bit data codewords, Table 3.
+ ///
+ private static readonly Lazy Field12 = new(() => new GaloisField(4096, 4201));
+
+ ///
+ /// The states of the search, in the row and column order of the latch and shift tables: the five code sets of Table 2, then the byte state of a B/S.
+ ///
+ private enum CodeSet
+ {
+ ///
+ /// The upper code set of Table 2.
+ ///
+ Upper,
+
+ ///
+ /// The lower code set of Table 2.
+ ///
+ Lower,
+
+ ///
+ /// The mixed code set of Table 2.
+ ///
+ Mixed,
+
+ ///
+ /// The punctuation code set of Table 2.
+ ///
+ Punct,
+
+ ///
+ /// The digit code set of Table 2.
+ ///
+ Digit,
+
+ ///
+ /// The byte state of a B/S.
+ ///
+ Binary,
+ }
+
+ ///
+ /// Gets the bits each code set writes per character, latch or shift: 5, and 4 in the digit code set, and 8 for each byte of a byte shift.
+ ///
+ private static ReadOnlySpan CharacterBits => [5, 5, 5, 5, 4, 8];
+
+ ///
+ /// Gets the bits of the cheapest latch from each state, row, to each state, column. A latch into the byte state is a B/S and its 5-bit length, and the
+ /// return from it costs nothing because the length ends it.
+ ///
+ private static ReadOnlySpan LatchBits =>
+ [
+ 0, 5, 5, 10, 5, 10,
+ 9, 0, 5, 10, 5, 10,
+ 5, 5, 0, 5, 10, 10,
+ 5, 10, 10, 0, 10, 15,
+ 4, 9, 9, 14, 0, 14,
+ 0, 0, 0, 0, 0, 0,
+ ];
+
+ ///
+ /// Gets the latch codes of each latch of , up to three each, 0 where none.
+ ///
+ private static ReadOnlySpan LatchCodes =>
+ [
+ 0, 0, 0, LatchLower, 0, 0, LatchMixed, 0, 0, LatchMixed, LatchPunct, 0, LatchDigit, 0, 0, ShiftBinary, 0, 0,
+ LatchDigit, LatchUpper, 0, 0, 0, 0, LatchMixed, 0, 0, LatchMixed, LatchPunct, 0, LatchDigit, 0, 0, ShiftBinary, 0, 0,
+ LatchUpper, 0, 0, LatchLower, 0, 0, 0, 0, 0, LatchPunct, 0, 0, LatchUpper, LatchDigit, 0, ShiftBinary, 0, 0,
+ LatchUpper, 0, 0, LatchUpper, LatchLower, 0, LatchUpper, LatchMixed, 0, 0, 0, 0, LatchUpper, LatchDigit, 0, LatchUpper, ShiftBinary, 0,
+ LatchUpper, 0, 0, LatchUpper, LatchLower, 0, LatchUpper, LatchMixed, 0, LatchUpper, LatchMixed, LatchPunct, 0, 0, 0, LatchUpper, ShiftBinary, 0,
+ LatchUpper, 0, 0, LatchLower, 0, 0, LatchMixed, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ ];
+
+ ///
+ /// Gets the bits of a shift from each state, row, to each state, column: U/S from the lower and digit code sets and P/S from every code set but
+ /// punctuation. Unreached marks no shift.
+ ///
+ private static ReadOnlySpan ShiftBits =>
+ [
+ Unreached, Unreached, Unreached, 5, Unreached,
+ 5, Unreached, Unreached, 5, Unreached,
+ Unreached, Unreached, Unreached, 5, Unreached,
+ Unreached, Unreached, Unreached, Unreached, Unreached,
+ 4, Unreached, Unreached, 4, Unreached,
+ ];
+
+ ///
+ /// Gets the codeword count of Table 1 for each symbol in the order of section 11.5: the compact symbols of 1 to 4 layers, then the full-range symbols of 1
+ /// to 32 layers.
+ ///
+ private static ReadOnlySpan SymbolCodewords =>
+ [
+ 17, 40, 51, 76,
+ 21, 48, 60, 88, 120, 156, 196, 240, 230, 272, 316, 364, 416, 470, 528, 588, 652, 720, 790, 864, 940, 1020,
+ 920, 992, 1066, 1144, 1224, 1306, 1392, 1480, 1570, 1664,
+ ];
+
+ ///
+ /// Gets the orientation patterns of section 7.1.3 for a finder of half width F, as x and y offsets from F and the module: the six dark modules, then the
+ /// six light ones. An offset of 1 is F + 1, 0 is F, and the signs follow.
+ ///
+ private static ReadOnlySpan Orientation =>
+ [
+ -2, 1, 1, -2, 2, 1, -1, 2, 1, 2, 2, 1, 2, 1, 1, 2, -1, 1,
+ 1, 2, 0, 2, -2, 0, 1, -2, 0, -1, -2, 0, -2, -2, 0, -2, -1, 0,
+ ];
+
+ ///
+ /// Gets the value of each ASCII character in each code set of Table 2, at index code set x 128 + character, or 0xFF where the code set has no such
+ /// character. The upper, lower, mixed, punctuation and digit code sets follow in turn, 128 characters each. No code set holds a byte above 127.
+ ///
+ private static ReadOnlySpan CharacterValues =>
+ [
+ 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
+ 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
+ 1, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
+ 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
+ 255, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
+ 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 255, 255, 255, 255, 255,
+ 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
+ 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
+ 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
+ 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
+ 1, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
+ 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
+ 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
+ 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
+ 255, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
+ 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 255, 255, 255, 255, 255,
+ 255, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 255, 255,
+ 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 15, 16, 17, 18, 19,
+ 1, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
+ 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
+ 20, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
+ 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 21, 255, 22, 23,
+ 24, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
+ 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 25, 255, 26, 27,
+ 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 1, 255, 255,
+ 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
+ 255, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
+ 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 21, 22, 23, 24, 25, 26,
+ 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
+ 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 27, 255, 28, 255, 255,
+ 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
+ 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 29, 255, 30, 255, 255,
+ 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
+ 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
+ 1, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 12, 255, 13, 255,
+ 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 255, 255, 255, 255, 255, 255,
+ 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
+ 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
+ 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
+ 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
+ ];
+
+ ///
+ /// Builds an Aztec Code symbol.
+ ///
+ /// The text, characters U+0000 to U+00FF.
+ /// The symbol format.
+ /// The minimum error correction percentage, 5 to 95.
+ /// The number of data layers, or 0 for the smallest symbol that holds the data.
+ /// The X dimension in millimetres.
+ /// The symbol to write.
+ /// The text is empty, has a character above U+00FF, or does not fit.
+ public static void BuildSymbol(
+ ReadOnlySpan text,
+ AztecFormat format,
+ int errorCorrectionPercentage,
+ int layers,
+ double xDimension,
+ ref BarcodeSymbol symbol)
+ {
+ if (text.IsEmpty)
+ {
+ throw new ArgumentException("Aztec Code needs at least one character.", nameof(text));
+ }
+
+ Guard.MustBeLessThanOrEqualTo(text.Length, MaximumLength, nameof(text));
+
+ // Section 4.1 a): "All 8-bit values can be encoded", with values 128 to 255 read as ISO/IEC 8859-1.
+ int invalid = text.IndexOfAnyExceptInRange('\0', '\u00FF');
+ if (invalid >= 0)
+ {
+ throw new ArgumentException($"Aztec Code encodes U+0000 to U+00FF; got {text.CodePointAt(invalid).ToDisplayString()}.", nameof(text));
+ }
+
+ PooledBuffer bits = default;
+ PooledBuffer codewords = default;
+ try
+ {
+ int bitCount = EncodeBits(text, ref bits);
+ ReadOnlySpan stream = bits.Span[..bitCount];
+
+ // The codeword count after stuffing depends only on the codeword size, so the encoder lays out each size once. Every symbol of that size reads the
+ // count.
+ InlineArray14 dataCountsStorage = default;
+ Span dataCounts = ((Span)dataCountsStorage)[..13];
+ dataCounts.Fill(-1);
+
+ SymbolSize chosen = default;
+ bool found = false;
+ int start = format == AztecFormat.Full ? CompactSizeCount : 0;
+ int end = format == AztecFormat.Compact ? CompactSizeCount : SymbolCodewords.Length;
+ for (int index = start; index < end; index++)
+ {
+ SymbolSize size = SymbolSize.FromIndex(index);
+
+ // Section 11.5: printing programs "print the smallest symbol for a given data set and error correction percentage in the data layer sequence
+ // 1*,2*,3*,4*,4,5,6,...,32". Full-range symbols of 1 to 3 layers are "useful only for reader initialization", so the search skips them unless
+ // the caller asks for that size.
+ if (layers > 0 ? size.Layers != layers : (format == AztecFormat.Auto && !size.IsCompact && size.Layers < 4))
+ {
+ continue;
+ }
+
+ int total = size.Codewords;
+ int codewordBits = size.CodewordBits;
+ if (dataCounts[codewordBits] < 0)
+ {
+ dataCounts[codewordBits] = Stuff(stream, codewordBits, []);
+ }
+
+ // Section 11.3: the percentage of the symbol capacity plus three codewords is the minimum number of check codewords.
+ int check = (((total * errorCorrectionPercentage) + 99) / 100) + ExtraCheckCodewords;
+ int data = dataCounts[codewordBits];
+ if (data + check <= total && data <= size.MaximumDataCodewords)
+ {
+ chosen = size;
+ found = true;
+ break;
+ }
+ }
+
+ if (!found)
+ {
+ throw new ArgumentException("The text does not fit in an Aztec Code symbol of the requested size.", nameof(text));
+ }
+
+ codewords = new PooledBuffer(chosen.Codewords);
+ Span all = codewords.Span;
+ int dataCount = Stuff(stream, chosen.CodewordBits, all);
+ AddCheckCodewords(all, dataCount, chosen.CodewordBits);
+
+ InlineArray64 modeStorage = default;
+ Span mode = ((Span)modeStorage)[..40];
+ int modeLength = WriteModeMessage(chosen, dataCount, false, mode);
+ Layout(chosen, all, mode[..modeLength], xDimension, ref symbol);
+ }
+ finally
+ {
+ bits.Dispose();
+ codewords.Dispose();
+ }
+ }
+
+ ///
+ /// Builds an Aztec Rune: the Core Symbol of a compact symbol whose mode message carries one value of 0 to 255.
+ ///
+ /// The value, 0 to 255.
+ /// The X dimension in millimetres.
+ /// The symbol to write.
+ public static void BuildRune(int value, double xDimension, ref BarcodeSymbol symbol)
+ {
+ InlineArray32 modeStorage = default;
+ Span mode = ((Span)modeStorage)[..28];
+ SymbolSize core = new(true, 0);
+ WriteModeMessage(core, value, true, mode);
+ Layout(core, [], mode, xDimension, ref symbol);
+ }
+
+ ///
+ /// Translates the text into the bit stream of section 7.3.2.2. The search keeps, for each code set, the cheapest sequence of characters, latches and shifts
+ /// that ends in it. The sequences share their common start: each is a node that points back to the element before it, so a copy is one index and an
+ /// extension is one new node.
+ ///
+ /// The text.
+ /// The buffer the bits are written to, one bit per byte, rented here.
+ /// The number of bits.
+ private static int EncodeBits(ReadOnlySpan text, ref PooledBuffer bits)
+ {
+ NodeArena arena = new(text.Length * 8);
+ try
+ {
+ // The costs and sequence heads of each state, indexed by CodeSet, before and after the current character.
+ InlineArray6 currentCostStorage = default;
+ Span currentCost = currentCostStorage;
+ InlineArray6 nextCostStorage = default;
+ Span nextCost = nextCostStorage;
+ InlineArray6 currentHeadStorage = default;
+ Span currentHead = currentHeadStorage;
+ InlineArray6 nextHeadStorage = default;
+ Span nextHead = nextHeadStorage;
+ InlineArray16 afterStorage = default;
+ Span after = afterStorage;
+ currentCost.Fill(Unreached);
+ currentCost[(int)CodeSet.Upper] = 0;
+ currentHead.Fill(-1);
+
+ // Encoding starts in the upper code set. The byte state returns to the code set it shifted from, which the punctuation and digit code sets reach
+ // through upper.
+ CodeSet backTo = CodeSet.Upper;
+ int lastCharacter = -1;
+ for (int t = 0; t < text.Length; t++)
+ {
+ int character = text[t];
+
+ // A latch can make the sequence of another state cheaper. Each improvement can enable another, so the pass repeats until none applies.
+ bool improved;
+ do
+ {
+ improved = false;
+ for (CodeSet x = CodeSet.Upper; x <= CodeSet.Binary; x++)
+ {
+ for (CodeSet y = CodeSet.Upper; y <= CodeSet.Binary; y++)
+ {
+ if (x == CodeSet.Binary && y != backTo)
+ {
+ continue;
+ }
+
+ int latch = LatchIndex(x, y);
+ int cost = currentCost[(int)x] + LatchBits[latch];
+ if (cost < currentCost[(int)y])
+ {
+ currentCost[(int)y] = cost;
+ int head = currentHead[(int)x];
+ foreach (sbyte code in LatchCodes.Slice(latch * 3, 3))
+ {
+ if (code == 0)
+ {
+ break;
+ }
+
+ head = arena.Append(head, code);
+ }
+
+ currentHead[(int)y] = head;
+ if (y == CodeSet.Binary)
+ {
+ backTo = x is CodeSet.Punct or CodeSet.Digit ? CodeSet.Upper : x;
+ }
+
+ improved = true;
+ }
+ }
+ }
+ }
+ while (improved);
+
+ // Each state that holds the character extends its own sequence, and the other code sets can reach it with a shift.
+ nextCost.Fill(Unreached);
+ nextHead.Fill(-1);
+ for (CodeSet x = CodeSet.Upper; x <= CodeSet.Binary; x++)
+ {
+ if (x != CodeSet.Binary && (character > 127 || CharacterValues[((int)x << 7) | character] == 0xFF))
+ {
+ continue;
+ }
+
+ int cost = currentCost[(int)x] + CharacterBits[(int)x];
+ int head = currentHead[(int)x];
+ if (x == CodeSet.Binary && head >= 0 && arena[head].Run == MaximumByteRun)
+ {
+ // A full byte run returns to the code set it shifted from and shifts again: the return marker, then a new B/S and its 5-bit length.
+ cost += LatchBits[LatchIndex(backTo, CodeSet.Binary)];
+ head = arena.Append(arena.Append(head, LatchCodes[LatchIndex(CodeSet.Binary, backTo) * 3]), ShiftBinary);
+ }
+
+ if (cost < nextCost[(int)x])
+ {
+ nextCost[(int)x] = cost;
+ nextHead[(int)x] = arena.Append(head, character);
+ }
+
+ if (x == CodeSet.Binary)
+ {
+ continue;
+ }
+
+ for (CodeSet y = CodeSet.Upper; y < CodeSet.Binary; y++)
+ {
+ if (x == y)
+ {
+ continue;
+ }
+
+ cost = currentCost[(int)y] + ShiftBits[((int)y * CharacterSetCount) + (int)x] + CharacterBits[(int)x];
+ if (cost < nextCost[(int)y])
+ {
+ nextCost[(int)y] = cost;
+ nextHead[(int)y] = arena.Append(arena.Append(currentHead[(int)y], x == CodeSet.Upper ? ShiftUpper : ShiftPunct), character);
+ }
+ }
+ }
+
+ // A punctuation pair of Table 2 puts the previous character and this one in one 5-bit value.
+ int pair = Pair(lastCharacter, character);
+ if (pair != 0)
+ {
+ for (CodeSet state = CodeSet.Upper; state < CodeSet.Binary; state++)
+ {
+ int i = (int)state;
+
+ // The mixed code set holds CR and the digit code set holds the comma and the period themselves, so a sequence in those states did not
+ // reach the first character through punctuation.
+ bool inPunct = !(state == CodeSet.Mixed && lastCharacter == '\r') && !(state == CodeSet.Digit && lastCharacter is ',' or '.');
+ if (!inPunct || currentCost[i] >= nextCost[i])
+ {
+ continue;
+ }
+
+ // The scan walks back over the latches this step added to the previous character. The marker before it is the latch that set the code
+ // set it was written in, and a byte shift there means it was a byte.
+ bool latchDigit = false;
+ int found = currentHead[i];
+ while (found >= 0 && arena[found].Value != lastCharacter)
+ {
+ found = arena[found].Previous;
+ }
+
+ if (found >= 0)
+ {
+ int previous = arena[found].Previous;
+ bool shifted = previous >= 0 && arena[previous].Value == ShiftPunct;
+ int marker = previous >= 0 ? arena[previous].Marker : 0;
+ if (marker == ShiftBinary)
+ {
+ found = -1;
+ }
+ else if (marker != 0)
+ {
+ if (state == CodeSet.Punct)
+ {
+ latchDigit = marker == LatchDigit;
+ }
+ else if (marker != LatchPunct)
+ {
+ inPunct = shifted;
+ }
+ }
+ }
+
+ if (!inPunct || found < 0)
+ {
+ continue;
+ }
+
+ nextCost[i] = currentCost[i];
+
+ // The elements after the previous character are the latches this step added. The pair replaces the character where it stands, except in
+ // punctuation, where it follows the latches into that code set.
+ int count = 0;
+ for (int n = currentHead[i]; n != found; n = arena[n].Previous)
+ {
+ after[count++] = arena[n].Value;
+ }
+
+ int rebuilt = arena[found].Previous;
+ if (state != CodeSet.Punct)
+ {
+ rebuilt = arena.Append(rebuilt, pair);
+ }
+ else if (latchDigit && count > 0)
+ {
+ // The character took 4 bits in the digit code set, and the pair takes 5.
+ nextCost[i]++;
+ }
+
+ for (int c = count - 1; c >= 0; c--)
+ {
+ rebuilt = arena.Append(rebuilt, after[c]);
+ }
+
+ if (state == CodeSet.Punct)
+ {
+ rebuilt = arena.Append(rebuilt, pair);
+ }
+
+ nextHead[i] = rebuilt;
+ }
+ }
+
+ // Section 7.3.2.2: a byte shift of more than 31 bytes takes an 11-bit length after a zero.
+ if (nextHead[(int)CodeSet.Binary] >= 0 && arena[nextHead[(int)CodeSet.Binary]].Run == 32)
+ {
+ nextCost[(int)CodeSet.Binary] += 11;
+ }
+
+ nextCost.CopyTo(currentCost);
+ nextHead.CopyTo(currentHead);
+ lastCharacter = character;
+ }
+
+ int best = 0;
+ for (int i = 1; i < StateCount; i++)
+ {
+ if (currentCost[i] < currentCost[best])
+ {
+ best = i;
+ }
+ }
+
+ int length = 0;
+ for (int n = currentHead[best]; n >= 0; n = arena[n].Previous)
+ {
+ length++;
+ }
+
+ using PooledBuffer sequenceBuffer = new(length);
+ Span sequence = sequenceBuffer.Span;
+ int position = length;
+ for (int n = currentHead[best]; n >= 0; n = arena[n].Previous)
+ {
+ sequence[--position] = arena[n].Value;
+ }
+
+ bits = new PooledBuffer(currentCost[best]);
+ return WriteBits(sequence, bits.Span);
+ }
+ finally
+ {
+ arena.Dispose();
+ }
+ }
+
+ ///
+ /// Writes the bits of a sequence of characters, latches and shifts, most significant bit first.
+ ///
+ /// The sequence.
+ /// The buffer, one bit per byte.
+ /// The number of bits written.
+ private static int WriteBits(ReadOnlySpan sequence, Span bits)
+ {
+ CodeSet state = CodeSet.Upper;
+ int written = 0;
+ int i = 0;
+ while (i < sequence.Length)
+ {
+ if (state != CodeSet.Binary)
+ {
+ int element = sequence[i++];
+ written = Write(bits, written, Value(state, element), CharacterBits[(int)state]);
+ if (element is ShiftUpper or ShiftPunct)
+ {
+ CodeSet shiftState = element == ShiftUpper ? CodeSet.Upper : CodeSet.Punct;
+ written = Write(bits, written, Value(shiftState, sequence[i++]), 5);
+ }
+
+ state = element switch
+ {
+ LatchUpper => CodeSet.Upper,
+ LatchLower => CodeSet.Lower,
+ LatchMixed => CodeSet.Mixed,
+ LatchPunct => CodeSet.Punct,
+ LatchDigit => CodeSet.Digit,
+ ShiftBinary => CodeSet.Binary,
+ _ => state,
+ };
+
+ continue;
+ }
+
+ // Section 7.3.2.2: the B/S is followed by the byte count, 1 to 31 in 5 bits, or 0 and the count less 31 in 11 bits. The bytes run to the next
+ // element that is not a character, a latch or shift code below 0.
+ int byteCount = sequence[i..].IndexOfAnyExceptInRange(0, 255);
+ if (byteCount < 0)
+ {
+ byteCount = sequence.Length - i;
+ }
+
+ if (byteCount <= 31)
+ {
+ written = Write(bits, written, byteCount, 5);
+ }
+ else
+ {
+ written = Write(bits, written, 0, 5);
+ written = Write(bits, written, byteCount - 31, 11);
+ }
+
+ for (int b = 0; b < byteCount; b++)
+ {
+ written = Write(bits, written, sequence[i++], 8);
+ }
+
+ // "At the end of the byte string, encoding returns to the mode from which B/S was invoked." The sequence marks that mode with the latch code the
+ // search added.
+ if (i < sequence.Length)
+ {
+ state = sequence[i++] switch
+ {
+ LatchLower => CodeSet.Lower,
+ LatchMixed => CodeSet.Mixed,
+ _ => CodeSet.Upper,
+ };
+ }
+ }
+
+ return written;
+ }
+
+ ///
+ /// Lays the bit stream into codewords, section 7.3.2.3: "whenever the first B-1 bits placed in a codeword are all 0's, then a dummy 1 is inserted into that
+ /// codeword's LSB and the following message bit starts off the next codeword", and the same with a dummy 0 after B-1 ones. The final codeword is "padded
+ /// out with 1's (and possibly a final dummy 0 if necessary)".
+ ///
+ /// The bit stream.
+ /// The codeword size B.
+ /// The buffer the codewords are written to. Codewords past its end are counted and not written, so an empty buffer only
+ /// counts.
+ /// The number of codewords.
+ private static int Stuff(ReadOnlySpan bits, int size, Span codewords)
+ {
+ int count = 0;
+ int ones = (1 << size) - 1;
+ int position = 0;
+ while (position < bits.Length)
+ {
+ int codeword = 0;
+ if (bits.Length - position >= size)
+ {
+ for (int b = 0; b < size - 1; b++)
+ {
+ codeword = (codeword << 1) | bits[position + b];
+ }
+
+ int head = codeword;
+ codeword <<= 1;
+ if (head == 0)
+ {
+ codeword |= 1;
+ position += size - 1;
+ }
+ else if (head == ones >> 1)
+ {
+ position += size - 1;
+ }
+ else
+ {
+ codeword |= bits[position + size - 1];
+ position += size;
+ }
+ }
+ else
+ {
+ int remaining = bits.Length - position;
+ for (int b = 0; b < size; b++)
+ {
+ codeword = (codeword << 1) | (b < remaining ? bits[position + b] : 1);
+ }
+
+ if (codeword == ones)
+ {
+ codeword ^= 1;
+ }
+
+ position = bits.Length;
+ }
+
+ if (count < codewords.Length)
+ {
+ codewords[count] = codeword;
+ }
+
+ count++;
+ }
+
+ return count;
+ }
+
+ ///
+ /// Adds the check codewords of section 7.3.3 after the data codewords: the remainder of the data polynomial times x^K divided by (x - 2^1) ... (x - 2^K)
+ /// over the field of Table 3.
+ ///
+ /// The data codewords followed by room for the check codewords.
+ /// The number of data codewords.
+ /// The codeword size B.
+ private static void AddCheckCodewords(Span codewords, int dataCount, int size)
+ {
+ GaloisField field = size switch
+ {
+ 6 => Field6.Value,
+ 8 => Field8.Value,
+ 10 => Field10.Value,
+ _ => Field12.Value,
+ };
+
+ int checkCount = codewords.Length - dataCount;
+ using PooledBuffer generatorBuffer = new(checkCount);
+ Span generator = generatorBuffer.Span;
+ field.Generator(1, generator);
+ field.Remainder(codewords, 1, dataCount, generator, codewords[dataCount..]);
+ }
+
+ ///
+ /// Writes the mode message bits of section 7.2: the layer count less one and the data codeword count less one, as 4-bit datawords, followed by the GF(16)
+ /// check words of section 7.2.4. A rune carries its value in place of the two counts, with alternate bits of every 4-bit word inverted.
+ ///
+ /// The symbol size.
+ /// The number of data codewords, or the rune value.
+ /// Whether the symbol is a rune.
+ /// The buffer, one bit per byte.
+ /// The number of bits: 28 compact, 40 full-range.
+ private static int WriteModeMessage(SymbolSize size, int dataCount, bool rune, Span bits)
+ {
+ bool compact = size.IsCompact;
+ int dataWords = compact ? 2 : 4;
+ int checkWords = compact ? 5 : 6;
+ int layers = size.Layers;
+ int value = rune ? dataCount : compact ? ((layers - 1) << 6) | (dataCount - 1) : ((layers - 1) << 11) | (dataCount - 1);
+
+ InlineArray10 wordsStorage = default;
+ Span words = wordsStorage;
+ for (int i = 0; i < dataWords; i++)
+ {
+ words[i] = (value >> ((dataWords - 1 - i) * 4)) & 0xF;
+ }
+
+ InlineArray6 generatorStorage = default;
+ Span generator = ((Span)generatorStorage)[..checkWords];
+ ModeField.Generator(1, generator);
+ ModeField.Remainder(words, 1, dataWords, generator, words.Slice(dataWords, checkWords));
+
+ int count = dataWords + checkWords;
+ for (int i = 0; i < count; i++)
+ {
+ int word = rune ? words[i] ^ 0b1010 : words[i];
+ for (int b = 0; b < 4; b++)
+ {
+ bits[(i * 4) + b] = (byte)((word >> (3 - b)) & 1);
+ }
+ }
+
+ return count * 4;
+ }
+
+ ///
+ /// Places the modules. The data layers spiral outward clockwise from the upper left of the Core Symbol in dominos of two modules, section 7.3.4, taking the
+ /// codewords in reverse order. A full-range symbol is first laid out without its reference grid, then the grid lines of section 7.1.4 are inserted every 16
+ /// modules from the centre, which moves every data module outward by the grid lines it crosses.
+ ///
+ /// The symbol size.
+ /// The data and check codewords.
+ /// The mode message bits.
+ /// The X dimension in millimetres.
+ /// The symbol to write.
+ private static void Layout(SymbolSize symbolSize, ReadOnlySpan codewords, ReadOnlySpan mode, double xDimension, ref BarcodeSymbol symbol)
+ {
+ bool compact = symbolSize.IsCompact;
+ int layers = symbolSize.Layers;
+ int size = symbolSize.CodewordBits;
+
+ // The core is 11 x 11 modules in a compact symbol and 15 x 15 in a full-range symbol, finder and mode message ring included. Without the grid the
+ // full-range core is 14 modules wide, because the grid line through the centre is not there yet. The core width here is the finder alone: 9 modules
+ // in a compact symbol, and 13 less the grid line through the centre, 12, in a full-range symbol. The mode message ring is between it and layer 1.
+ int coreWidth = compact ? 9 : 12;
+ int packedSide = coreWidth + (layers * 4) + 2;
+ using PooledBuffer packedBuffer = new(packedSide * packedSide);
+ Span packed = packedBuffer.Span;
+ packed.Clear();
+ int packedMiddle = ((packedSide - 1) / 2 * packedSide) + ((packedSide - 1) / 2);
+
+ // The bit stream fills the layers from the inside out with the last bit first. The codewords stand at the end of the layer capacity, so the outermost
+ // dominos left over at the end of the spiral are light. Layer l, of width coreWidth + 4l, holds 8 (coreWidth + 4l + 4) bits, so L layers hold
+ // 16 L^2 + (8 coreWidth + 16) L bits.
+ int capacityBits = (16 * layers * layers) + (((8 * coreWidth) + 16) * layers);
+ int offset = capacityBits - (codewords.Length * size);
+ int index = 0;
+ for (int layer = 1; layer <= layers; layer++)
+ {
+ int layerWidth = coreWidth + (layer * 4);
+ for (int bit = 0; bit < layerWidth * 8; bit++)
+ {
+ int t = capacityBits - 1 - index++;
+ bool dark = false;
+ if (t >= offset)
+ {
+ int codewordBit = t - offset;
+ dark = ((codewords[codewordBit / size] >> (size - 1 - (codewordBit % size))) & 1) == 1;
+ }
+
+ (int x, int y) = LayerPosition(coreWidth, layer, bit, layerWidth);
+ packed[packedMiddle + x - (y * packedSide)] = dark;
+ }
+ }
+
+ int side = packedSide;
+ int finder = compact ? 4 : 6;
+ Span modules;
+ if (compact)
+ {
+ // A compact symbol has no reference grid, so the packed layout is the module grid.
+ modules = symbol.BeginMatrix(side, side, 0, xDimension);
+ packed.CopyTo(modules);
+ }
+ else
+ {
+ // Section 7.1.4: the grid occupies "every location where x is a multiple of 16 or y is a multiple of 16", each module (x + y + 1) mod 2. The grid
+ // lines a layer crosses add to the side: one line for each 16 modules out from the centre, on both sides. The line count is (layers + 10.5) / 7.5 -
+ // 1 rounded down, which integers give exactly as (2 x layers + 21) / 15 - 1.
+ side = coreWidth + 1 + (layers * 4) + 2 + (2 * ((((2 * layers) + 21) / 15) - 1));
+ modules = symbol.BeginMatrix(side, side, 0, xDimension);
+ int half = side / 2;
+ int next = 0;
+ for (int row = 0; row < side; row++)
+ {
+ int y = half - row;
+ for (int column = 0; column < side; column++)
+ {
+ int x = column - half;
+ if (x % 16 == 0 || y % 16 == 0)
+ {
+ modules[(row * side) + column] = ((x + y + 1) & 1) == 1;
+ }
+ else
+ {
+ modules[(row * side) + column] = packed[next++];
+ }
+ }
+ }
+ }
+
+ int middle = side / 2;
+
+ // Section 7.1.2: every module of the finder is ((max(abs(x), abs(y))) + 1) mod 2.
+ for (int y = -finder; y <= finder; y++)
+ {
+ for (int x = -finder; x <= finder; x++)
+ {
+ modules[((middle - y) * side) + middle + x] = ((Math.Max(Math.Abs(x), Math.Abs(y)) + 1) & 1) == 1;
+ }
+ }
+
+ ReadOnlySpan orientation = Orientation;
+ for (int i = 0; i < orientation.Length; i += 3)
+ {
+ int x = Corner(orientation[i], finder);
+ int y = Corner(orientation[i + 1], finder);
+ modules[((middle - y) * side) + middle + x] = orientation[i + 2] == 1;
+ }
+
+ // Section 7.2.5: the mode message starts near the upper left corner of the finder and wraps clockwise, in four runs of 7 bits in a compact symbol and
+ // eight runs of 5 in a full-range symbol, which skip the grid line through the centre of each side.
+ int ring = finder + 1;
+ int perSide = mode.Length / 4;
+ for (int i = 0; i < mode.Length; i++)
+ {
+ int side4 = i / perSide;
+ int along = i % perSide;
+ int step = compact ? along - 3 : along < 5 ? along - 5 : along - 4;
+ (int x, int y) = side4 switch
+ {
+ 0 => (step, ring),
+ 1 => (ring, -step),
+ 2 => (-step, -ring),
+ _ => (-ring, step),
+ };
+
+ modules[((middle - y) * side) + middle + x] = mode[i] == 1;
+ }
+ }
+
+ ///
+ /// Returns the position of a bit of a data layer in the layout without the reference grid: two modules per domino, the more significant bit outward,
+ /// running along the top, down the right, back along the bottom and up the left.
+ ///
+ /// The width of the finder without the grid line through the centre. The mode message ring is outside it.
+ /// The layer, from 1 inside.
+ /// The bit within the layer.
+ /// The length of a side of the layer in dominos.
+ /// The x and y of the module, the y axis upward.
+ private static (int X, int Y) LayerPosition(int coreWidth, int layer, int bit, int layerWidth)
+ {
+ int along = (bit / 2) % layerWidth;
+ int depth = (layer * 2) + (bit % 2);
+ return ((bit / 2) / layerWidth) switch
+ {
+ 0 => (-((layerWidth - 1) / 2) + 1 + along, ((coreWidth - 1) / 2) + depth),
+ 1 => ((coreWidth / 2) + depth, ((layerWidth - 1) / 2) - 1 - along),
+ 2 => ((layerWidth / 2) - 1 - along, -((coreWidth / 2) + depth)),
+ _ => (-(((coreWidth - 1) / 2) + depth), -((layerWidth / 2) - 1 - along)),
+ };
+ }
+
+ ///
+ /// Returns a coordinate of the orientation table: F + 1, F, -F or -F - 1 for 2, 1, -1 and -2.
+ ///
+ /// The code.
+ /// The finder half width F.
+ /// The coordinate.
+ private static int Corner(int code, int finder)
+ => code switch
+ {
+ 2 => finder + 1,
+ 1 => finder,
+ -1 => -finder,
+ _ => -finder - 1,
+ };
+
+ ///
+ /// Returns the index of the latch from one state to another in , the rows by the state latched from and the columns by the state
+ /// latched to. Three times the index is the first latch code in .
+ ///
+ /// The state latched from.
+ /// The state latched to.
+ /// The table index.
+ private static int LatchIndex(CodeSet from, CodeSet to) => ((int)from * StateCount) + (int)to;
+
+ ///
+ /// Returns the punctuation pair code of two characters, or 0 when they are no pair of Table 2.
+ ///
+ /// The first character, or -1.
+ /// The second character.
+ /// The pair code, or 0.
+ private static int Pair(int first, int second)
+ => (first, second) switch
+ {
+ ('\r', '\n') => PairCrLf,
+ ('.', ' ') => PairPeriodSpace,
+ (',', ' ') => PairCommaSpace,
+ (':', ' ') => PairColonSpace,
+ _ => 0,
+ };
+
+ ///
+ /// Returns the Table 2 value of a sequence element in a code set.
+ ///
+ /// The code set.
+ /// The character, latch, shift or pair.
+ /// The value.
+ private static int Value(CodeSet state, int element)
+ {
+ // The search puts a character in a code set only when the code set holds it, so the character is ASCII.
+ if (element >= 0)
+ {
+ return CharacterValues[((int)state << 7) | element];
+ }
+
+ return (state, element) switch
+ {
+ (_, ShiftPunct) => 0,
+ (CodeSet.Upper, LatchLower) or (CodeSet.Mixed, LatchLower) => 28,
+ (CodeSet.Lower, ShiftUpper) => 28,
+ (CodeSet.Upper, LatchMixed) or (CodeSet.Lower, LatchMixed) => 29,
+ (CodeSet.Mixed, LatchUpper) => 29,
+ (CodeSet.Upper, LatchDigit) or (CodeSet.Lower, LatchDigit) => 30,
+ (CodeSet.Mixed, LatchPunct) => 30,
+ (_, ShiftBinary) => 31,
+ (CodeSet.Punct, LatchUpper) => 31,
+ (CodeSet.Digit, LatchUpper) => 14,
+ (CodeSet.Digit, ShiftUpper) => 15,
+ (_, PairCrLf) => 2,
+ (_, PairPeriodSpace) => 3,
+ (_, PairCommaSpace) => 4,
+ (_, PairColonSpace) => 5,
+ _ => throw new UnreachableException(),
+ };
+ }
+
+ ///
+ /// Writes a value most significant bit first.
+ ///
+ /// The buffer, one bit per byte.
+ /// The position of the first bit.
+ /// The value.
+ /// The number of bits.
+ /// The position after the last bit.
+ private static int Write(Span bits, int position, int value, int count)
+ {
+ for (int b = count - 1; b >= 0; b--)
+ {
+ bits[position++] = (byte)((value >> b) & 1);
+ }
+
+ return position;
+ }
+
+ ///
+ /// The size of a symbol: compact or full-range, and the number of data layers around the core.
+ ///
+ private readonly struct SymbolSize
+ {
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// Whether the symbol is compact.
+ /// The number of data layers, 0 for the core of a rune.
+ public SymbolSize(bool isCompact, int layers)
+ {
+ this.IsCompact = isCompact;
+ this.Layers = layers;
+ }
+
+ ///
+ /// Gets a value indicating whether the symbol is compact.
+ ///
+ public bool IsCompact { get; }
+
+ ///
+ /// Gets the number of data layers.
+ ///
+ public int Layers { get; }
+
+ ///
+ /// Gets the codeword size B of Table 3.
+ ///
+ public int CodewordBits => this.Layers <= 2 ? 6 : this.Layers <= 8 || this.IsCompact ? 8 : this.Layers <= 22 ? 10 : 12;
+
+ ///
+ /// Gets the number of codewords of Table 1, data and check together.
+ ///
+ public int Codewords => SymbolCodewords[this.IsCompact ? this.Layers - 1 : this.Layers + CompactSizeCount - 1];
+
+ ///
+ /// Gets the most data codewords the mode message can count. Section 7.2.3 gives the message length 6 bits in a compact symbol and 11 bits in a
+ /// full-range symbol.
+ ///
+ public int MaximumDataCodewords => this.IsCompact ? 64 : 2048;
+
+ ///
+ /// Returns the symbol size at an index of .
+ ///
+ /// The index, in the order of section 11.5.
+ /// The symbol size.
+ public static SymbolSize FromIndex(int index)
+ => index < CompactSizeCount ? new SymbolSize(true, index + 1) : new SymbolSize(false, index - CompactSizeCount + 1);
+ }
+
+ ///
+ /// One element of a search sequence.
+ ///
+ private struct Node
+ {
+ ///
+ /// The element: a character as its byte value, 0 to 255, or the negative code of a pair, a latch or a shift.
+ ///
+ public int Value;
+
+ ///
+ /// The node before it, or -1 at the start of the sequence.
+ ///
+ public int Previous;
+
+ ///
+ /// The number of elements since the last B/S, which is the length of the byte run when the sequence is in the byte state.
+ ///
+ public int Run;
+
+ ///
+ /// The latch code or B/S nearest before or at this node, or 0 when the sequence has none, so the code set an element was written in reads without a
+ /// walk back.
+ ///
+ public int Marker;
+ }
+
+ ///
+ /// The nodes of the search sequences in one pooled buffer that doubles when it fills.
+ ///
+ private struct NodeArena : IDisposable
+ {
+ ///
+ /// The nodes. The arena owns the buffer and returns it in .
+ ///
+ private PooledBuffer buffer;
+
+ ///
+ /// The number of nodes in use.
+ ///
+ private int count;
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The initial number of nodes.
+ public NodeArena(int capacity)
+ {
+ this.buffer = new PooledBuffer(Math.Max(capacity, 64));
+ this.count = 0;
+ }
+
+ ///
+ /// Gets a node.
+ ///
+ /// The node index.
+ /// The node.
+ public readonly ref Node this[int index] => ref this.buffer.Span[index];
+
+ ///
+ /// Adds a node after another.
+ ///
+ /// The node before it, or -1 for the start of a sequence.
+ /// The element.
+ /// The index of the new node.
+ public int Append(int previous, int value)
+ {
+ Span nodes = this.buffer.Span;
+ if (this.count == nodes.Length)
+ {
+ PooledBuffer larger = new(nodes.Length * 2);
+ nodes.CopyTo(larger.Span);
+ this.buffer.Dispose();
+ this.buffer = larger;
+ nodes = larger.Span;
+ }
+
+ int run = value == ShiftBinary ? 0 : previous < 0 ? 1 : nodes[previous].Run + 1;
+ int marker = value is (>= LatchDigit and <= LatchUpper) or ShiftBinary ? value : previous < 0 ? 0 : nodes[previous].Marker;
+ nodes[this.count] = new Node { Value = value, Previous = previous, Run = run, Marker = marker };
+ return this.count++;
+ }
+
+ ///
+ public void Dispose() => this.buffer.Dispose();
+ }
+}
diff --git a/src/ImageSharp.Drawing/Barcodes/Aztec/AztecFormat.cs b/src/ImageSharp.Drawing/Barcodes/Aztec/AztecFormat.cs
new file mode 100644
index 00000000..a96124e5
--- /dev/null
+++ b/src/ImageSharp.Drawing/Barcodes/Aztec/AztecFormat.cs
@@ -0,0 +1,25 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Drawing.Barcodes;
+
+///
+/// The formats an Aztec Code symbol can take, section 5.1 of ISO/IEC 24778.
+///
+internal enum AztecFormat
+{
+ ///
+ /// The smallest symbol in the sequence of section 11.5: compact symbols of 1 to 4 layers, then full-range symbols of 4 to 32 layers.
+ ///
+ Auto,
+
+ ///
+ /// A compact symbol of 1 to 4 layers, with a 2-ring bullseye.
+ ///
+ Compact,
+
+ ///
+ /// A full-range symbol of 1 to 32 layers, with a 3-ring bullseye and a reference grid.
+ ///
+ Full,
+}
diff --git a/src/ImageSharp.Drawing/Barcodes/Aztec/AztecRuneSymbology.cs b/src/ImageSharp.Drawing/Barcodes/Aztec/AztecRuneSymbology.cs
new file mode 100644
index 00000000..5007cf92
--- /dev/null
+++ b/src/ImageSharp.Drawing/Barcodes/Aztec/AztecRuneSymbology.cs
@@ -0,0 +1,36 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Drawing.Barcodes;
+
+///
+/// Aztec Runes of ISO/IEC 24778: 256 small marks of 11 x 11 modules, the Core Symbol of a compact Aztec Code symbol whose mode message carries one value. The
+/// text is the value, 0 to 255, in decimal digits.
+///
+public sealed class AztecRuneSymbology : BarcodeSymbology
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public AztecRuneSymbology()
+ {
+ }
+
+ ///
+ internal override void Encode(ReadOnlySpan text, BarcodeOptions options, ref BarcodeSymbol symbol)
+ {
+ if (text.Length is < 1 or > 3 || text.ContainsAnyExceptInRange('0', '9'))
+ {
+ throw new ArgumentException("An Aztec Rune carries a value of 0 to 255 in decimal digits.", nameof(text));
+ }
+
+ int value = int.Parse(text, System.Globalization.CultureInfo.InvariantCulture);
+ if (value > 255)
+ {
+ throw new ArgumentException($"An Aztec Rune carries a value of 0 to 255; got {value}.", nameof(text));
+ }
+
+ // Clause 12 of ISO/IEC 24778: "This symbology specification places no limits on the size of X."
+ AztecEncoder.BuildRune(value, options.ResolveXDimension(0, DefaultMatrixXDimension, double.PositiveInfinity), ref symbol);
+ }
+}
diff --git a/src/ImageSharp.Drawing/Barcodes/Aztec/HibcAztecCodeSymbology.cs b/src/ImageSharp.Drawing/Barcodes/Aztec/HibcAztecCodeSymbology.cs
new file mode 100644
index 00000000..3c56c1b8
--- /dev/null
+++ b/src/ImageSharp.Drawing/Barcodes/Aztec/HibcAztecCodeSymbology.cs
@@ -0,0 +1,91 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Drawing.Helpers;
+
+namespace SixLabors.ImageSharp.Drawing.Barcodes;
+
+///
+/// HIBC Aztec Code, a Health Industry Bar Code carried by an Aztec Code symbol. The symbol carries the flag character, the data and the modulo 43 check
+/// character, in the smallest symbol that holds them.
+/// The text follows a data structure of ANSI/HIBC 2.6: primary data, secondary data that ends with its link character, or primary and secondary data joined
+/// with "/", each with any supplemental data fields.
+///
+/// When a font is set, that same string prints below the symbol between delimiters, with a space printed as an underscore and a zero as "Ø". Primary and
+/// secondary data that the symbol joins with "/" print on two lines, broken after the "/". When the options set no X dimension, the X dimension
+/// is 0.015 inch, from section 5.2 of ANSI/HIBC 2.6. An X dimension under 0.010 inch draws at 0.010 inch, the minimum of the same section.
+///
+///
+public sealed class HibcAztecCodeSymbology : BarcodeSymbology
+{
+ ///
+ /// Initializes a new instance of the class at the recommended error correction level of 23 percent, calculating the
+ /// check character from the data.
+ ///
+ public HibcAztecCodeSymbology()
+ : this(AztecEncoder.DefaultErrorCorrectionPercentage, false)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class at the recommended error correction level of 23 percent.
+ ///
+ /// Whether the last character of the input is the check character, which the encoder validates against the data,
+ /// rather than data the encoder appends a check character to.
+ public HibcAztecCodeSymbology(bool validateCheckCharacter)
+ : this(AztecEncoder.DefaultErrorCorrectionPercentage, validateCheckCharacter)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The minimum percentage of the symbol capacity that error correction takes, 5 to 95. Three more check codewords
+ /// are always added.
+ /// Whether the last character of the input is the check character, which the encoder validates against the data,
+ /// rather than data the encoder appends a check character to.
+ public HibcAztecCodeSymbology(int errorCorrectionPercentage, bool validateCheckCharacter)
+ {
+ Guard.MustBeBetweenOrEqualTo(errorCorrectionPercentage, 5, 95, nameof(errorCorrectionPercentage));
+ this.ErrorCorrectionPercentage = errorCorrectionPercentage;
+ this.ValidateCheckCharacter = validateCheckCharacter;
+ }
+
+ ///
+ /// Gets the minimum error correction percentage.
+ ///
+ public int ErrorCorrectionPercentage { get; }
+
+ ///
+ /// Gets a value indicating whether the last character of the input is the check character.
+ ///
+ public bool ValidateCheckCharacter { get; }
+
+ ///
+ internal override void Encode(ReadOnlySpan text, BarcodeOptions options, ref BarcodeSymbol symbol)
+ {
+ InlineArray64 bufferStorage = default;
+ int capacity = HibcData.PreparedCapacity(text, AztecEncoder.MaximumLength);
+ ValueStringBuilder encoded = capacity <= HibcData.InlineBufferLength ? new(bufferStorage) : new(capacity);
+ try
+ {
+ HibcData.Prepare(text, this.ValidateCheckCharacter, ref encoded);
+ AztecEncoder.BuildSymbol(
+ encoded.AsSpan(),
+ AztecFormat.Auto,
+ this.ErrorCorrectionPercentage,
+ 0,
+ options.ResolveXDimension(HibcData.MinimumMatrixXDimension, HibcData.NominalMatrixXDimension, double.PositiveInfinity),
+ ref symbol);
+
+ if (options.Font is not null)
+ {
+ HibcData.AddTwoDimensionalReadable(encoded.AsSpan(), false, ref symbol);
+ }
+ }
+ finally
+ {
+ encoded.Dispose();
+ }
+ }
+}
diff --git a/src/ImageSharp.Drawing/Barcodes/BarEmitter.cs b/src/ImageSharp.Drawing/Barcodes/BarEmitter.cs
new file mode 100644
index 00000000..5812eb97
--- /dev/null
+++ b/src/ImageSharp.Drawing/Barcodes/BarEmitter.cs
@@ -0,0 +1,1033 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Numerics;
+using SixLabors.Fonts;
+using SixLabors.Fonts.Unicode;
+using SixLabors.ImageSharp.Drawing.Helpers;
+using SixLabors.ImageSharp.Drawing.Processing;
+
+namespace SixLabors.ImageSharp.Drawing.Barcodes;
+
+///
+/// Measures and draws a linear or stacked . All bars render through a single fill of one . Each text
+/// placement draws through one text command. The measure and the draw share one layout, so the measured metrics are the metrics of what is drawn.
+///
+internal static class BarEmitter
+{
+ ///
+ /// Measures the symbol without drawing anything.
+ ///
+ /// The encoded linear or stacked symbol.
+ /// The sizing and painting options.
+ /// The top left corner of the barcode, in pixels.
+ /// The measured layout of the symbol.
+ public static BarcodeMetrics Measure(ref BarcodeSymbol symbol, BarcodeOptions options, PointF origin)
+ {
+ using PooledBuffer lineBuffer = BarcodeEmitter.CreateLineBuffer(ref symbol, options);
+
+ // A TextBlock is a class, so the span holds object references and cannot come from the byte pool. A bar symbol has at most 14 text placements: an
+ // ISBN has one for each of its 13 digits and one for its caption. The 16 stack slots hold all of them.
+ InlineArray16 blockStorage = default;
+ Span blocks = ((Span)blockStorage)[..lineBuffer.Span.Length];
+ Shape(ref symbol, options, lineBuffer.Span, blocks);
+ return Measure(ref symbol, options, origin, lineBuffer.Span, out _);
+ }
+
+ ///
+ /// Draws the symbol onto the canvas.
+ ///
+ /// The canvas to render onto.
+ /// The encoded linear or stacked symbol.
+ /// The sizing and painting options.
+ /// The top left corner of the barcode, in pixels.
+ /// The brush that fills the renderable bounds before the bars draw, or for none.
+ /// The measured layout of the symbol, the same as returns.
+ public static BarcodeMetrics Draw(DrawingCanvas canvas, ref BarcodeSymbol symbol, BarcodeOptions options, PointF origin, Brush? background)
+ {
+ using PooledBuffer lineBuffer = BarcodeEmitter.CreateLineBuffer(ref symbol, options);
+
+ // A TextBlock is a class, so the span holds object references and cannot come from the byte pool. A bar symbol has at most 14 text placements: an
+ // ISBN has one for each of its 13 digits and one for its caption. The 16 stack slots hold all of them.
+ InlineArray16 blockStorage = default;
+ Span blocks = ((Span)blockStorage)[..lineBuffer.Span.Length];
+ Shape(ref symbol, options, lineBuffer.Span, blocks);
+ return Draw(canvas, ref symbol, options, origin, lineBuffer.Span, blocks, background);
+ }
+
+ ///
+ /// Shapes the text of the symbol: one text block and one measured line for each text placement. The layout reads the lines and the draw renders the
+ /// blocks, so neither shapes the text again. A symbology that limits the size of its text and a caption that spans the bars measure their lines one more
+ /// time before the blocks, to find the font size.
+ ///
+ /// The encoded linear or stacked symbol.
+ /// The sizing and painting options.
+ /// The measured lines to write, one for each text placement, or empty when the options carry no font.
+ /// The text blocks to write, one for each line.
+ public static void Shape(ref BarcodeSymbol symbol, BarcodeOptions options, Span lines, Span blocks)
+ {
+ Font? font = options.Font;
+ if (font is null || lines.IsEmpty)
+ {
+ return;
+ }
+
+ // The text spans stretch with a bar block of whole pixel runs, so they still cover it.
+ float moduleWidth = options.ModuleWidth(symbol.XDimension);
+ float stretch = symbol.UniformBars ? BlockWidth(ref symbol, moduleWidth) / (symbol.WidthInModules * moduleWidth) : 1F;
+
+ // A bar symbol has at most 14 text placements, so the scales fit inline.
+ InlineArray16 lineScaleStorage = default;
+ Span lineScales = ((Span)lineScaleStorage)[..lines.Length];
+ lineScales.Fill(1F);
+ Font digitFont = HasSizeLimits(ref symbol) ? DigitFont(ref symbol, font, options, moduleWidth, moduleWidth * stretch, lineScales) : font;
+ LineFonts fonts = new(digitFont, options.CaptionFont ?? font, symbol.CaptionMinimumSize, symbol.TextWeight, options, moduleWidth * stretch);
+
+ ReadOnlySpan text = symbol.Text;
+ for (int i = 0; i < lines.Length; i++)
+ {
+ BarcodeTextPlacement placement = text[i];
+ ReadOnlySpan characters = symbol.TextOf(in placement);
+ RichTextOptions textOptions = fonts.OptionsFor(in placement, characters, lineScales[i]);
+
+ // The rectangles of a line are anchored at its origin, so the line is measured from its top left corner and placed on the cell later. The block
+ // keeps the options it was prepared with, and the draw renders the same block at the place the layout gives it.
+ textOptions.Origin = PointF.Empty;
+ TextBlock block = new(characters, textOptions);
+ blocks[i] = block;
+ TextMetrics metrics = block.Measure(-1);
+
+ // The lines below the bars share one baseline, which the tallest of them sets, so they keep the baseline and the line height of their first line.
+ float baseline = 0;
+ float lineHeight = 0;
+ if (placement.Side != BarcodeTextSide.AboveBars)
+ {
+ LineMetrics lineMetrics = metrics.LineMetrics[0];
+ baseline = lineMetrics.Baseline;
+ lineHeight = lineMetrics.LineHeight;
+ }
+
+ lines[i] = new MeasuredLine(metrics.Advance, metrics.Bounds, metrics.RenderableBounds, baseline, lineHeight);
+ }
+ }
+
+ ///
+ /// Measures the symbol from its shaped text without drawing anything.
+ ///
+ /// The encoded linear or stacked symbol.
+ /// The sizing and painting options.
+ /// The top left corner of the barcode, in pixels.
+ /// The lines that wrote. The layout writes the position of each line.
+ /// The top left corner of the first module of the symbol, in pixels, before it rounds onto the pixel grid.
+ /// The measured layout of the symbol.
+ public static BarcodeMetrics Measure(ref BarcodeSymbol symbol, BarcodeOptions options, PointF origin, Span lines, out PointF symbolOrigin)
+ {
+ BarLayout layout = new(ref symbol, options, origin, lines);
+ symbolOrigin = layout.SymbolOrigin;
+ return layout.Metrics;
+ }
+
+ ///
+ /// Draws the symbol from its shaped text onto the canvas.
+ ///
+ /// The canvas to render onto.
+ /// The encoded linear or stacked symbol.
+ /// The sizing and painting options.
+ /// The top left corner of the barcode, in pixels.
+ /// The lines that wrote. The layout writes the position of each line.
+ /// The blocks that wrote, one for each line.
+ /// The brush that fills the renderable bounds before the bars draw, or for none.
+ /// The measured layout of the symbol.
+ public static BarcodeMetrics Draw(
+ DrawingCanvas canvas,
+ ref BarcodeSymbol symbol,
+ BarcodeOptions options,
+ PointF origin,
+ Span lines,
+ ReadOnlySpan blocks,
+ Brush? background)
+ {
+ BarLayout layout = new(ref symbol, options, origin, lines);
+ BarcodeMetrics metrics = layout.Metrics;
+
+ // The path retains the rectangles until the canvas flushes, so it owns this array.
+ RectangleF[] bars = new RectangleF[layout.BarCount(ref symbol)];
+ layout.WriteBars(ref symbol, bars);
+
+ if (background is not null)
+ {
+ RectangleF renderable = metrics.RenderableBounds;
+ canvas.Fill(background, new RectanglePolygon(renderable.X, renderable.Y, renderable.Width, renderable.Height));
+ }
+
+ canvas.Fill(options.BarBrush, new BarcodePath(bars, layout.BarInk));
+ layout.DrawText(canvas, blocks, options.BarBrush);
+ return metrics;
+ }
+
+ ///
+ /// Returns whether the symbology limits the size of its digit lines: it states a size in points or the printed height of its characters, or a line must
+ /// stay within its span.
+ ///
+ /// The encoded symbol.
+ /// when the size of the digit lines is limited.
+ private static bool HasSizeLimits(ref BarcodeSymbol symbol)
+ {
+ if (symbol.TextHeightMaximum > 0 || symbol.TextMinimumSize > 0 || symbol.TextMaximumSize > 0)
+ {
+ return true;
+ }
+
+ foreach (BarcodeTextPlacement placement in symbol.Text)
+ {
+ if (placement.WithinSpan)
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ ///
+ /// Returns the font of the digit lines of a symbology that limits their size, and the scale of each line that must fit its span. A stated size in points
+ /// moves the size of the font of the options into that range, and a stated printed height of the characters moves it into that range. A line that is wider
+ /// than its span at that size then prints at the scale that fits it.
+ ///
+ /// The encoded symbol.
+ /// The font of the options.
+ /// The sizing and painting options.
+ /// The width of one module in pixels.
+ /// The width of one module of a text span in pixels, stretched to the drawn bar block.
+ /// The scale of each line, one for each text placement, to write.
+ /// The font of the digit lines.
+ /// A line does not fit its span at the smallest printed height of the symbology.
+ private static Font DigitFont(ref BarcodeSymbol symbol, Font font, BarcodeOptions options, float moduleWidth, float spanModuleWidth, Span lineScales)
+ {
+ // A size in points needs no measure. The other limits measure the text in the font of that size.
+ float pointSize = font.Size;
+ if (symbol.TextMinimumSize > 0)
+ {
+ pointSize = MathF.Max(pointSize, symbol.TextMinimumSize);
+ }
+
+ if (symbol.TextMaximumSize > 0)
+ {
+ pointSize = MathF.Min(pointSize, symbol.TextMaximumSize);
+ }
+
+ font = pointSize == font.Size ? font : new Font(font, pointSize);
+
+ // The ink height comes from the design bounds of the glyphs, and hinting does not move a glyph on the x axis, so the measured height and widths scale
+ // with the font size. Each line gives the sizes that put it on its limits, and where the limits of two lines disagree, the minimum height wins over
+ // the maximum height. Only a line that a limit applies to is measured. A line of one character that only has to fit its span takes the width of its
+ // glyph, which needs no shaping.
+ ReadOnlySpan text = symbol.Text;
+ float minimumHeight = symbol.TextHeightMinimum * moduleWidth;
+ float maximumHeight = symbol.TextHeightMaximum * moduleWidth;
+ float sizeAtMinimum = 0;
+ float sizeAtMaximum = float.MaxValue;
+ RichTextOptions? measureOptions = null;
+ for (int i = 0; i < text.Length; i++)
+ {
+ BarcodeTextPlacement placement = text[i];
+ if (placement.IsCaption || (maximumHeight <= 0 && !placement.WithinSpan))
+ {
+ continue;
+ }
+
+ ReadOnlySpan characters = symbol.TextOf(in placement);
+ float width;
+ if (maximumHeight <= 0
+ && placement.Alignment != BarcodeTextAlignment.Centered
+ && characters.Length == 1
+ && font.TryGetGlyphs(new CodePoint(characters[0]), out Glyph? glyph)
+ && glyph is Glyph characterGlyph)
+ {
+ width = characterGlyph.BoundingBox(GlyphLayoutMode.Horizontal, Vector2.Zero, options.Dpi).Width;
+ }
+ else
+ {
+ measureOptions ??= BarcodeTextOptionsFactory.Create(font, options.Dpi, symbol.TextWeight);
+ TextMetrics metrics = TextMeasurer.Measure(characters, measureOptions);
+ float inkHeight = metrics.Bounds.Height * placement.FontScale;
+ if (maximumHeight > 0 && inkHeight > 0)
+ {
+ sizeAtMinimum = MathF.Max(sizeAtMinimum, font.Size * minimumHeight / inkHeight);
+ sizeAtMaximum = MathF.Min(sizeAtMaximum, font.Size * maximumHeight / inkHeight);
+ }
+
+ // A line aligned on an edge fits its ink to the span, and a centred line fits its renderable bounds, which hold its ink.
+ width = placement.Alignment == BarcodeTextAlignment.Centered ? metrics.RenderableBounds.Width : metrics.Bounds.Width;
+ }
+
+ // The width at the size of the options, until the size of the font is known.
+ lineScales[i] = placement.WithinSpan ? width * placement.FontScale : 1F;
+ }
+
+ float size = MathF.Max(MathF.Min(font.Size, sizeAtMaximum), sizeAtMinimum);
+ for (int i = 0; i < text.Length; i++)
+ {
+ BarcodeTextPlacement placement = text[i];
+ if (!placement.WithinSpan || placement.IsCaption)
+ {
+ continue;
+ }
+
+ float width = lineScales[i] * size / font.Size;
+ float span = (placement.Right - placement.Left) * spanModuleWidth;
+ float scale = width > span ? span / width : 1F;
+
+ // The smallest printed height and the span are both requirements, so a line that needs a smaller font than the height allows cannot be printed.
+ if (maximumHeight > 0 && size * scale < sizeAtMinimum)
+ {
+ throw new ArgumentException("A text line does not fit its span at the smallest printed height of its symbology.");
+ }
+
+ lineScales[i] = scale;
+ }
+
+ return size == font.Size ? font : new Font(font, size);
+ }
+
+ ///
+ /// Returns the width of the bar block in pixels. Uniform bars draw each run at a whole number of pixels, so their block is the sum of those runs on the
+ /// first row. Other bars draw at the exact width.
+ ///
+ /// The encoded linear or stacked symbol.
+ /// The width of one module in pixels.
+ /// The width in pixels.
+ private static float BlockWidth(ref BarcodeSymbol symbol, float moduleWidth)
+ {
+ if (!symbol.UniformBars)
+ {
+ return symbol.WidthInModules * moduleWidth;
+ }
+
+ float runWidthInPixels = symbol.RunUnit * moduleWidth;
+ BarcodeRow first = symbol.Rows[0];
+ ReadOnlySpan runs = symbol.Runs.Slice(first.RunStart, first.RunCount);
+ float blockWidth = 0;
+ for (int i = 0; i < runs.Length; i++)
+ {
+ blockWidth += RunPixels(runs[i], runWidthInPixels);
+ }
+
+ return blockWidth;
+ }
+
+ ///
+ /// Returns the pixel offset of an edge in a row of uniform bars: the sum of the whole pixel widths of the runs before the edge.
+ ///
+ /// The runs of the row.
+ /// The edge in run units from the start of the row.
+ /// The width of one run unit in pixels.
+ /// The offset in pixels.
+ private static float PixelOffset(ReadOnlySpan runs, int units, float runWidthInPixels)
+ {
+ float pixels = 0;
+ int position = 0;
+ for (int i = 0; position < units; i++)
+ {
+ pixels += RunPixels(runs[i], runWidthInPixels);
+ position += runs[i];
+ }
+
+ return pixels;
+ }
+
+ ///
+ /// Returns the whole number of pixels a run of a symbol with uniform bars draws at. A run that is not empty draws at least one pixel.
+ ///
+ /// The run width in run units.
+ /// The width of one run unit in pixels.
+ /// The run width in pixels.
+ private static float RunPixels(int units, float runWidthInPixels)
+ => units == 0 ? 0F : MathF.Max(1F, MathF.Round(units * runWidthInPixels, MidpointRounding.AwayFromZero));
+
+ ///
+ /// Resolves the font an ISBN, ISMN or ISSN caption prints in. Under the size is the one that spans the
+ /// bars. Otherwise the font keeps its own size. The minimum size of the symbology wins over both, and a caption that cannot fit at that size widens the
+ /// drawing.
+ ///
+ /// The caption text.
+ /// The width the caption prints across, in modules.
+ /// The width of one module in pixels.
+ /// The caption font of the options, or their font when they carry no caption font.
+ /// The smallest size of the caption in points, from .
+ /// The font weight of the text, from .
+ /// The options that carry the resolution and the fitting choice.
+ /// The font to print the caption with.
+ private static Font ResolveCaptionFont(
+ ReadOnlySpan caption,
+ float spanModules,
+ float moduleWidth,
+ Font font,
+ float minimumSize,
+ FontWeight? weight,
+ BarcodeOptions options)
+ {
+ float size = font.Size;
+ if (options.FitCaptionToSymbolWidth)
+ {
+ float width = TextMeasurer.Measure(caption, BarcodeTextOptionsFactory.Create(font, options.Dpi, weight)).RenderableBounds.Width;
+ size = font.Size * spanModules * moduleWidth / width;
+ }
+
+ size = MathF.Max(size, minimumSize);
+ return size == font.Size ? font : new Font(font, size);
+ }
+
+ ///
+ /// The layout of a linear or stacked symbol: where the bar block, the frame, the identification bars and each text line stand, from one origin. The
+ /// constructor places everything and measures the ink, and writes the bars for the draw, so the measure and the draw read one
+ /// layout.
+ ///
+ private ref struct BarLayout
+ {
+ ///
+ /// The width of one module in pixels.
+ ///
+ private readonly float moduleWidth;
+
+ ///
+ /// The top edge of the rows in pixels, on a whole pixel.
+ ///
+ private readonly float blockTop;
+
+ ///
+ /// The x of the first bar, in pixels.
+ ///
+ private readonly float symbolLeft;
+
+ ///
+ /// The width of the bar block in pixels.
+ ///
+ private readonly float blockWidth;
+
+ ///
+ /// Whether every run draws at a whole number of pixels of its own.
+ ///
+ private readonly bool wholePixelRuns;
+
+ ///
+ /// The width of one run unit in pixels.
+ ///
+ private readonly float runWidthInPixels;
+
+ ///
+ /// The number of run units in one module of runs that are whole modules with corrections, or 0, from .
+ ///
+ private readonly int correctedModuleUnits;
+
+ ///
+ /// The thickness of the frame in modules, or zero for none.
+ ///
+ private readonly float frame;
+
+ ///
+ /// The frame thickness in whole pixels.
+ ///
+ private readonly float frameThickness;
+
+ ///
+ /// The top edge of the upper frame section, in pixels.
+ ///
+ private readonly float frameTop;
+
+ ///
+ /// The bottom edge of the upper frame section, in pixels.
+ ///
+ private readonly float upperBottom;
+
+ ///
+ /// The top edge of the lower frame section, in pixels.
+ ///
+ private readonly float lowerTop;
+
+ ///
+ /// The left edge of the frame, in pixels.
+ ///
+ private readonly float frameLeft;
+
+ ///
+ /// The right edge of the frame, in pixels.
+ ///
+ private readonly float frameRight;
+
+ ///
+ /// Whether the frame has vertical sections.
+ ///
+ private readonly bool sides;
+
+ ///
+ /// The upper identification bar in pixels, or an empty rectangle for none.
+ ///
+ private readonly RectangleF upperIdentificationBar;
+
+ ///
+ /// The lower identification bar in pixels, or an empty rectangle for none.
+ ///
+ private readonly RectangleF lowerIdentificationBar;
+
+ ///
+ /// The measured lines, one for each text placement, or none when the options carry no font.
+ ///
+ private readonly Span lines;
+
+ ///
+ /// Initializes a new instance of the struct. A linear symbol is one row. The rows share the width, the run unit and the bar
+ /// rounding of the symbol.
+ ///
+ /// The encoded linear or stacked symbol.
+ /// The sizing and painting options.
+ /// The top left corner of the barcode, in pixels.
+ /// The shaped lines, one for each text placement, or empty when the options carry no font. The layout writes their origins.
+ public BarLayout(ref BarcodeSymbol symbol, BarcodeOptions options, PointF origin, Span lines)
+ {
+ this.lines = lines;
+ float moduleWidth = options.ModuleWidth(symbol.XDimension);
+ this.moduleWidth = moduleWidth;
+
+ ReadOnlySpan allRuns = symbol.Runs;
+ ReadOnlySpan text = symbol.Text;
+
+ // The bars that close the top and the bottom: a bearer bar on a linear symbol, which also has vertical sections when the quiet zones are drawn, or
+ // the edge bars of a stacked symbol.
+ float frame = symbol.FrameThickness;
+ this.frame = frame;
+ float widthInModules = symbol.WidthInModules;
+
+ // A symbol whose bars are one width at one pitch, the postal symbologies, draws every run at a whole number of pixels of its own, so every bar
+ // keeps that width and that pitch: Table 11 of the Mailmark barcode definition document requires that "they must be equally spaced", and page 12
+ // of the Japan Post manual gives one bar width, 0.6×a/10. Edges rounded from the accumulated exact position alternate between two widths.
+ // The text spans stretch with the bar block so they still cover it.
+ this.wholePixelRuns = symbol.UniformBars;
+ this.runWidthInPixels = symbol.RunUnit * moduleWidth;
+ this.correctedModuleUnits = symbol.CorrectedModuleUnits;
+ float blockWidth = BlockWidth(ref symbol, moduleWidth);
+ this.blockWidth = blockWidth;
+ float stretch = this.wholePixelRuns ? blockWidth / (symbol.WidthInModules * moduleWidth) : 1F;
+ float textModuleWidth = moduleWidth * stretch;
+
+ // The first bar stands after the leading quiet zone and, when the frame has vertical sections, after the frame.
+ float leadingOffset = 0;
+ bool frameSpansQuietZones = false;
+ if (options.IncludeQuietZones)
+ {
+ widthInModules += symbol.LeadingQuietZone + symbol.TrailingQuietZone;
+ leadingOffset += symbol.LeadingQuietZone * moduleWidth;
+
+ // Section 5.3.2.4 of the GS1 General Specifications runs the bearer bar around the quiet zones, so its vertical sections need the quiet zones
+ // on the page. Without them the same clause permits the horizontal sections alone. Section 2.1 of USS-16K and of USS-49 gives the top and
+ // bottom separator bars the same reach: they "extend to the ends of the quiet zones".
+ frameSpansQuietZones = frame > 0;
+ this.sides = frameSpansQuietZones && symbol.Kind == BarcodeSymbolKind.Linear;
+ if (this.sides)
+ {
+ widthInModules += frame + frame;
+ leadingOffset += frame * moduleWidth;
+ }
+ }
+
+ float sideZones = (widthInModules - symbol.WidthInModules) * moduleWidth;
+ float topZone = options.IncludeQuietZones ? symbol.TopQuietZone : 0F;
+ float bottomZone = options.IncludeQuietZones ? symbol.BottomQuietZone : 0F;
+
+ // Every line drawn in one font shares one rise, so the lines share a baseline and their glyphs sit level with each other. The rise is the
+ // tallest ink any of those lines carries, which a cap height metric misses for an ascender or the overshoot of a round glyph. A symbol prints one
+ // line below the bars and at most one above. The clear space is measured to the ink, so the line moves by the offset of its ink edge to put that
+ // edge on the gap whatever the font or its size.
+ float aboveInk = 0;
+ float aboveInkTop = float.MaxValue;
+ float belowInk = float.MaxValue;
+ float belowInkBottom = float.MinValue;
+ float belowBaseline = 0;
+ float belowHeight = 0;
+ float aboveEdge = float.NaN;
+ float belowEdge = float.NaN;
+ for (int i = 0; i < lines.Length; i++)
+ {
+ BarcodeTextPlacement placement = text[i];
+ MeasuredLine line = lines[i];
+ if (placement.Side == BarcodeTextSide.AboveBars)
+ {
+ aboveInk = MathF.Max(aboveInk, line.Bounds.Bottom);
+ aboveInkTop = MathF.Min(aboveInkTop, line.Bounds.Top);
+ aboveEdge = float.IsNaN(aboveEdge) ? placement.TextEdge : MathF.Min(aboveEdge, placement.TextEdge);
+ }
+ else
+ {
+ belowInkBottom = MathF.Max(belowInkBottom, line.Bounds.Bottom);
+ belowEdge = float.IsNaN(belowEdge) ? placement.TextEdge : MathF.Max(belowEdge, placement.TextEdge);
+
+ // The quiet zone digits of a UPC symbol print smaller than the rest of the line. The tallest line sets where the line sits, and every line
+ // on it shares that baseline.
+ if (line.LineHeight > belowHeight)
+ {
+ belowHeight = line.LineHeight;
+ belowBaseline = line.Baseline;
+ }
+
+ belowInk = MathF.Min(belowInk, line.Bounds.Top);
+ }
+ }
+
+ // The layout box: the barcode and the line box of each line, in exact positions relative to the top left corner of the barcode. The identification
+ // bars belong to the layout of the symbol, as the frame does, and they reach across the quiet zones whether or not the quiet zones are drawn.
+ // Nothing in this box rounds, as nothing in the layout box of text rounds.
+ float layoutLeft = 0;
+ float layoutTop = 0;
+ float layoutRight = sideZones + blockWidth;
+ float layoutBottom = (topZone + symbol.HeightInModules + bottomZone) * moduleWidth;
+ for (int i = 0; i < lines.Length; i++)
+ {
+ BarcodeTextPlacement placement = text[i];
+ MeasuredLine line = lines[i];
+ float cellLeft = leadingOffset + (placement.Left * textModuleWidth);
+ float cellRight = leadingOffset + (placement.Right * textModuleWidth);
+ float x = placement.Origin(cellLeft, cellRight, line.Bounds, line.RenderableBounds.Width);
+ float edge = (topZone + placement.TextEdge) * moduleWidth;
+ float y = placement.Side == BarcodeTextSide.AboveBars ? edge - aboveInk : edge - belowInk + belowBaseline - line.Baseline;
+ layoutLeft = MathF.Min(layoutLeft, x + line.Advance.Left);
+ layoutTop = MathF.Min(layoutTop, y + line.Advance.Top);
+ layoutRight = MathF.Max(layoutRight, x + line.Advance.Right);
+ layoutBottom = MathF.Max(layoutBottom, y + line.Advance.Bottom);
+ }
+
+ float identificationThickness = symbol.IdentificationBarThickness * moduleWidth;
+ float identificationClearance = symbol.IdentificationBarClearance * moduleWidth;
+ if (identificationThickness > 0)
+ {
+ layoutLeft = MathF.Min(layoutLeft, leadingOffset - (symbol.LeadingQuietZone * moduleWidth));
+ layoutRight = MathF.Max(layoutRight, leadingOffset + blockWidth + (symbol.TrailingQuietZone * moduleWidth));
+ if (!float.IsNaN(aboveEdge))
+ {
+ float bottom = ((topZone + aboveEdge) * moduleWidth) - (aboveInk - aboveInkTop) - identificationClearance;
+ layoutTop = MathF.Min(layoutTop, bottom - identificationThickness);
+ }
+
+ if (!float.IsNaN(belowEdge))
+ {
+ float top = ((topZone + belowEdge) * moduleWidth) + (belowInkBottom - belowInk) + identificationClearance;
+ layoutBottom = MathF.Max(layoutBottom, top + identificationThickness);
+ }
+ }
+
+ // The origin is the top left corner of the layout box, as it is for text. Text above the symbol or to the left of it moves the symbol down or
+ // right.
+ PointF symbolOrigin = new(origin.X - layoutLeft, origin.Y - layoutTop);
+ RectangleF advance = new(origin.X, origin.Y, layoutRight - layoutLeft, layoutBottom - layoutTop);
+
+ // The bar block starts on a whole pixel, whatever the fraction of a pixel the origin and the quiet zone end on. Each edge then rounds from a whole
+ // pixel, so a module of a whole number of pixels draws at exactly that number wherever the symbol stands. From a start on half a pixel, edges round
+ // to the even pixel and alternate, and a bar of three pixels draws as four and then two.
+ this.SymbolOrigin = new PointF(symbolOrigin.X + leadingOffset, symbolOrigin.Y + (topZone * moduleWidth));
+ float symbolLeft = MathF.Round(this.SymbolOrigin.X);
+ this.symbolLeft = symbolLeft;
+ this.blockTop = MathF.Round(this.SymbolOrigin.Y);
+
+ // The edge the reader sees is what rounds onto the device pixel grid, as the bar edges do. The ink of a line below the bars starts on its text
+ // edge, and the ink of a line above the bars ends on it.
+ RectangleF textInk = RectangleF.Empty;
+ for (int i = 0; i < lines.Length; i++)
+ {
+ BarcodeTextPlacement placement = text[i];
+ ref MeasuredLine line = ref lines[i];
+ float textEdge = MathF.Round(symbolOrigin.Y + ((topZone + placement.TextEdge) * moduleWidth));
+ float x = placement.GridOrigin(line.Bounds, line.RenderableBounds.Width, symbolLeft, textModuleWidth);
+ float y = placement.Side == BarcodeTextSide.AboveBars
+ ? textEdge - aboveInk
+ : textEdge - belowInk + belowBaseline - line.Baseline;
+
+ line.Origin = new Vector2(x, y);
+ RectangleF ink = new(x + line.Bounds.X, y + line.Bounds.Y, line.Bounds.Width, line.Bounds.Height);
+ textInk = i == 0 ? ink : RectangleF.Union(textInk, ink);
+ }
+
+ // Section 5.3.2.4 of the GS1 General Specifications gives the bearer bar "a constant thickness", so the sections of the frame share one pixel
+ // thickness. On every side where the frame is the outermost ink its outer edge snaps outward, so no background shows past it. Where text or a quiet
+ // zone follows below, the lower edge rounds as the bar edges do, which keeps the clear space. The bars that touch the frame butt against its inner
+ // edges, so their height absorbs the fraction of a pixel that the snapping moves, never the frame.
+ if (frame > 0)
+ {
+ this.frameThickness = MathF.Round(frame * moduleWidth);
+ float exactTop = this.blockTop;
+ float exactBottom = this.blockTop + (symbol.HeightInModules * moduleWidth);
+ this.frameTop = topZone > 0 ? MathF.Round(exactTop) : MathF.Floor(exactTop);
+ float frameBottom = bottomZone > 0 || !float.IsNaN(belowEdge) ? MathF.Round(exactBottom) : MathF.Ceiling(exactBottom);
+ this.upperBottom = this.frameTop + this.frameThickness;
+ this.lowerTop = frameBottom - this.frameThickness;
+
+ // When the quiet zones are drawn the frame reaches across them, and its outer edges snap outward from the exact ones, so the frame fills
+ // the measured area. The quiet zones absorb the fraction of a pixel between the snapped outer edge and the exact one. The vertical sections
+ // also stand at least the quiet zone and their thickness away from the bar block, which starts and ends on whole pixels. The thickness
+ // rounds on its own, so without that limit a quiet zone can lose a pixel under its minimum. A quiet zone is a minimum, so a quiet zone of
+ // a fraction of a pixel rounds up. Without the quiet zones the frame ends on the bars.
+ if (this.sides)
+ {
+ float leadingZone = (float)Math.Ceiling(symbol.LeadingQuietZone * (double)moduleWidth);
+ float trailingZone = (float)Math.Ceiling(symbol.TrailingQuietZone * (double)moduleWidth);
+ this.frameLeft = MathF.Min(MathF.Floor(symbolOrigin.X), symbolLeft - leadingZone - this.frameThickness);
+ float blockRight = symbolLeft + blockWidth;
+ this.frameRight = MathF.Max(MathF.Ceiling(symbolOrigin.X + sideZones + blockWidth), blockRight + trailingZone + this.frameThickness);
+ }
+ else
+ {
+ this.frameLeft = frameSpansQuietZones ? MathF.Floor(symbolOrigin.X) : MathF.Round(symbolLeft);
+ this.frameRight = frameSpansQuietZones ? MathF.Ceiling(symbolOrigin.X + sideZones + blockWidth) : MathF.Round(symbolLeft + blockWidth);
+ }
+ }
+
+ // An identification bar stands its clearance beyond the far ink edge of the line on its side, and spans the symbol and its quiet zones whether or
+ // not the quiet zones are drawn, because section 2.4.3 of USPS2000508 extends the bars "at least the total combined width of the barcode and the
+ // minimum clear zones". Section 4.9.2 of Publication 199: "Human-readable numbers cannot extend beyond the length of the identification bars", and
+ // section 4.9.4 lets the bars extend to "the entire width of the label", so the bars also span the ink of the text. The bars stand on the text
+ // edges the lines draw at, so the clearance holds to the drawn ink. The edge next to the text rounds away from it, and both bars share one pixel
+ // thickness.
+ if (identificationThickness > 0)
+ {
+ float thickness = MathF.Max(1F, MathF.Round(identificationThickness));
+ float left = MathF.Floor(MathF.Min(symbolLeft - (symbol.LeadingQuietZone * moduleWidth), textInk.Left));
+ float right = MathF.Ceiling(MathF.Max(symbolLeft + blockWidth + (symbol.TrailingQuietZone * moduleWidth), textInk.Right));
+ if (!float.IsNaN(aboveEdge))
+ {
+ float edge = MathF.Round(symbolOrigin.Y + ((topZone + aboveEdge) * moduleWidth));
+ float bottom = MathF.Floor(edge - (aboveInk - aboveInkTop) - identificationClearance);
+ this.upperIdentificationBar = RectangleF.FromLTRB(left, bottom - thickness, right, bottom);
+ }
+
+ if (!float.IsNaN(belowEdge))
+ {
+ float edge = MathF.Round(symbolOrigin.Y + ((topZone + belowEdge) * moduleWidth));
+ float top = MathF.Ceiling(edge + (belowInkBottom - belowInk) + identificationClearance);
+ this.lowerIdentificationBar = RectangleF.FromLTRB(left, top, right, top + thickness);
+ }
+ }
+
+ // The ink of the bars comes from the ink edges of each row, which the symbol records once: each edge rounds onto the pixel grid as the bar edge it
+ // names does, so the ink is the union of the bars without a visit to each bar. Uniform bars place each run at a whole number of pixels, so their
+ // edges add those pixel widths up to the edge.
+ RectangleF barInk = RectangleF.Empty;
+ ReadOnlySpan rows = symbol.Rows;
+ for (int r = 0; r < rows.Length; r++)
+ {
+ BarcodeRow row = rows[r];
+ if (row.InkStart < 0)
+ {
+ continue;
+ }
+
+ float left;
+ float right;
+ if (this.wholePixelRuns)
+ {
+ ReadOnlySpan rowRuns = allRuns.Slice(row.RunStart, row.RunCount);
+ left = symbolLeft + PixelOffset(rowRuns, row.InkStart, this.runWidthInPixels);
+ right = symbolLeft + PixelOffset(rowRuns, row.InkEnd, this.runWidthInPixels);
+ }
+ else if (this.correctedModuleUnits > 0)
+ {
+ left = this.CorrectedEdge(row.InkStart);
+ right = this.CorrectedEdge(row.InkEnd);
+ }
+ else
+ {
+ left = (float)Math.Round(symbolLeft + (row.InkStart * (double)this.runWidthInPixels));
+ right = (float)Math.Round(symbolLeft + (row.InkEnd * (double)this.runWidthInPixels));
+ }
+
+ RectangleF rowInk = RectangleF.FromLTRB(left, this.RowEdge(row.Top + row.InkTop), right, this.RowEdge(row.Top + row.Height));
+ barInk = barInk.IsEmpty ? rowInk : RectangleF.Union(barInk, rowInk);
+ }
+
+ // The frame is the outermost ink on the sides it closes, and an identification bar spans the symbol and its quiet zones.
+ if (frame > 0)
+ {
+ barInk = RectangleF.Union(barInk, RectangleF.FromLTRB(this.frameLeft, this.frameTop, this.frameRight, this.lowerTop + this.frameThickness));
+ }
+
+ if (!this.upperIdentificationBar.IsEmpty)
+ {
+ barInk = RectangleF.Union(barInk, this.upperIdentificationBar);
+ }
+
+ if (!this.lowerIdentificationBar.IsEmpty)
+ {
+ barInk = RectangleF.Union(barInk, this.lowerIdentificationBar);
+ }
+
+ this.BarInk = barInk;
+
+ // As in text measuring, the advance is relative to the origin, and the bounds and the renderable bounds are absolute.
+ RectangleF bounds = textInk.IsEmpty ? barInk : RectangleF.Union(barInk, textInk);
+ this.Metrics = new BarcodeMetrics(new RectangleF(0, 0, advance.Width, advance.Height), bounds, RectangleF.Union(advance, bounds));
+ }
+
+ ///
+ /// Gets the top left corner of the first module of the symbol, in pixels, before it rounds onto the pixel grid.
+ ///
+ public PointF SymbolOrigin { get; }
+
+ ///
+ /// Gets the ink of the bars, the frame and the identification bars, in pixels.
+ ///
+ public RectangleF BarInk { get; }
+
+ ///
+ /// Gets the metrics of the layout.
+ ///
+ public BarcodeMetrics Metrics { get; }
+
+ ///
+ /// Returns the number of bar rectangles writes: the bars, the frame sections and the identification bars.
+ ///
+ /// The encoded symbol.
+ /// The number of rectangles.
+ public readonly int BarCount(ref BarcodeSymbol symbol)
+ {
+ int count = symbol.BarHeights.Length;
+ if (this.frame > 0)
+ {
+ count += this.sides ? 4 : 2;
+ }
+
+ count += this.upperIdentificationBar.IsEmpty ? 0 : 1;
+ count += this.lowerIdentificationBar.IsEmpty ? 0 : 1;
+ return count;
+ }
+
+ ///
+ /// Returns the pixel position of an edge of runs that are whole modules with corrections. The module edge rounds onto the pixel grid as an edge without
+ /// a correction does, and the correction rounds to its own whole number of pixels, so the edges are those of the runs without corrections until a
+ /// correction is half a pixel or more. Rounded together, a correction of a fraction of a pixel can move one edge of a bar a whole pixel and its
+ /// neighbour not, and change a bar of two pixels by half its width. The corrections of a character sum to zero and are less than half a module, so the
+ /// nearest module to an edge is the module edge it corrects.
+ ///
+ /// The edge in run units from the start of the row.
+ /// The edge in pixels.
+ private readonly float CorrectedEdge(int units)
+ {
+ int unitsPerModule = this.correctedModuleUnits;
+ int modules = (units + (unitsPerModule >> 1)) / unitsPerModule;
+ int correction = units - (modules * unitsPerModule);
+ double edge = Math.Round(this.symbolLeft + (modules * (double)this.moduleWidth));
+ return (float)(edge + Math.Round(correction * (double)this.runWidthInPixels));
+ }
+
+ ///
+ /// Writes every bar rectangle: the bars, then the frame sections, then the identification bars.
+ ///
+ /// The encoded symbol.
+ /// The rectangles to write, of them.
+ public readonly void WriteBars(ref BarcodeSymbol symbol, Span bars)
+ {
+ int index = 0;
+ ReadOnlySpan rows = symbol.Rows;
+ ReadOnlySpan allRuns = symbol.Runs;
+ ReadOnlySpan barHeights = symbol.BarHeights;
+ ReadOnlySpan barTops = symbol.BarTops;
+ float frame = this.frame;
+ bool corrected = this.correctedModuleUnits > 0;
+ for (int r = 0; r < rows.Length; r++)
+ {
+ BarcodeRow row = rows[r];
+ ReadOnlySpan runs = allRuns.Slice(row.RunStart, row.RunCount);
+ ReadOnlySpan rowHeights = barHeights.Slice(row.BarStart, row.BarCount);
+ ReadOnlySpan rowBarTops = barTops.Slice(row.BarStart, row.BarCount);
+ float rowTop = row.Top;
+
+ // The position adds every run before it. A double holds each product of a whole run and the run width exactly, so the sum carries no rounding
+ // error from the runs before it.
+ double x = this.symbolLeft;
+ int units = 0;
+ for (int i = 0; i < runs.Length; i++)
+ {
+ double runWidth = this.wholePixelRuns ? RunPixels(runs[i], this.runWidthInPixels) : runs[i] * (double)this.runWidthInPixels;
+ if ((i & 1) == 0)
+ {
+ // Each bar edge rounds from its exact accumulated position onto the device pixel grid, so bars stay crisp for scanning at any module
+ // width and rounding cannot drift across the symbol, and the edge a row shares with the row below it rounds to one pixel row for both.
+ // Integral module widths, heights and origins round to themselves. Corrected runs round their module edges and corrections apart.
+ int bar = i >> 1;
+ float top = rowTop + rowBarTops[bar];
+ float bottom = top + rowHeights[bar];
+ float barTop = frame > 0 && top == frame ? this.upperBottom : this.RowEdge(top);
+ float barBottom = frame > 0 && bottom == symbol.HeightInModules - frame ? this.lowerTop : this.RowEdge(bottom);
+ float left = corrected ? this.CorrectedEdge(units) : (float)Math.Round(x);
+ float right = corrected ? this.CorrectedEdge(units + runs[i]) : (float)Math.Round(x + runWidth);
+ bars[index++] = RectangleF.FromLTRB(left, barTop, right, barBottom);
+ }
+
+ x += runWidth;
+ units += runs[i];
+ }
+ }
+
+ // The vertical sections of the frame run between the horizontal ones, so no two rectangles overlap.
+ if (frame > 0)
+ {
+ float width = this.frameRight - this.frameLeft;
+ float height = this.lowerTop - this.upperBottom;
+ bars[index++] = new RectangleF(this.frameLeft, this.frameTop, width, this.frameThickness);
+ bars[index++] = new RectangleF(this.frameLeft, this.lowerTop, width, this.frameThickness);
+ if (this.sides)
+ {
+ bars[index++] = new RectangleF(this.frameLeft, this.upperBottom, this.frameThickness, height);
+ bars[index++] = new RectangleF(this.frameRight - this.frameThickness, this.upperBottom, this.frameThickness, height);
+ }
+ }
+
+ if (!this.upperIdentificationBar.IsEmpty)
+ {
+ bars[index++] = this.upperIdentificationBar;
+ }
+
+ if (!this.lowerIdentificationBar.IsEmpty)
+ {
+ bars[index] = this.lowerIdentificationBar;
+ }
+ }
+
+ ///
+ /// Draws each text line where the layout put it. Each line renders its shaped block, so it is not shaped again.
+ ///
+ /// The canvas to render onto.
+ /// The shaped blocks, one for each line.
+ /// The brush of the text.
+ public readonly void DrawText(DrawingCanvas canvas, ReadOnlySpan blocks, Brush brush)
+ {
+ for (int i = 0; i < this.lines.Length; i++)
+ {
+ canvas.DrawText(blocks[i], this.lines[i].Origin, -1, brush, null);
+ }
+ }
+
+ ///
+ /// Returns a horizontal edge of the bar block in pixels: an exact position in modules from the symbol top, rounded onto the device pixel grid.
+ ///
+ /// The position in modules from the symbol top.
+ /// The edge in pixels.
+ private readonly float RowEdge(float modules)
+ => MathF.Round(this.blockTop + (modules * this.moduleWidth));
+ }
+
+ ///
+ /// The text options the lines of one symbol render with: the digit options, the caption options and the options of the scaled lines. The digit and the
+ /// caption options are built on first use and kept. The scaled options are kept while the next scaled line has the same font size.
+ /// reads the options of each line from here.
+ ///
+ private struct LineFonts
+ {
+ ///
+ /// The font the digit lines render in, from .
+ ///
+ private readonly Font digitFont;
+
+ ///
+ /// The font a caption starts from: the caption font of the options, or their font when they carry no caption font.
+ ///
+ private readonly Font captionFont;
+
+ ///
+ /// The smallest size of the caption in points, or 0 when the symbology states none.
+ ///
+ private readonly float captionMinimumSize;
+
+ ///
+ /// The font weight of the text, or for the weight of the fonts.
+ ///
+ private readonly FontWeight? weight;
+
+ ///
+ /// The sizing and painting options, which carry the resolution and the fitting choice.
+ ///
+ private readonly BarcodeOptions options;
+
+ ///
+ /// The width of one module in pixels, stretched to the drawn bar block.
+ ///
+ private readonly float moduleWidth;
+
+ ///
+ /// The options of the digit lines, or before first use.
+ ///
+ private RichTextOptions? digitOptions;
+
+ ///
+ /// The options of the last scaled line, or before first use.
+ ///
+ private RichTextOptions? scaledOptions;
+
+ ///
+ /// The options of the caption, or before first use.
+ ///
+ private RichTextOptions? captionOptions;
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The font the digit lines render in.
+ /// The font a caption starts from.
+ /// The smallest size of the caption in points, or 0 when the symbology states none.
+ /// The font weight of the text, from .
+ /// The sizing and painting options.
+ /// The width of one module in pixels, stretched to the drawn bar block.
+ public LineFonts(Font digitFont, Font captionFont, float captionMinimumSize, FontWeight? weight, BarcodeOptions options, float moduleWidth)
+ {
+ this.digitFont = digitFont;
+ this.captionFont = captionFont;
+ this.captionMinimumSize = captionMinimumSize;
+ this.weight = weight;
+ this.options = options;
+ this.moduleWidth = moduleWidth;
+ }
+
+ ///
+ /// Returns the text options a placement renders with. A caption renders in the caption font, resolved once for the symbol so that the measure and the
+ /// draw use one size.
+ ///
+ /// The placement to resolve for.
+ /// The characters of the placement.
+ /// The scale of a line that must fit its span, or 1.
+ /// The text options for the placement.
+ public RichTextOptions OptionsFor(in BarcodeTextPlacement placement, ReadOnlySpan characters, float lineScale)
+ {
+ float dpi = this.options.Dpi;
+ if (placement.IsCaption)
+ {
+ // The caption font resolves once for the symbol, so that the measure and the draw use one size.
+ if (this.captionOptions is null)
+ {
+ Font caption = ResolveCaptionFont(
+ characters,
+ placement.Right - placement.Left,
+ this.moduleWidth,
+ this.captionFont,
+ this.captionMinimumSize,
+ this.weight,
+ this.options);
+
+ this.captionOptions = BarcodeTextOptionsFactory.Create(caption, dpi, this.weight);
+ }
+
+ return this.captionOptions;
+ }
+
+ float scale = placement.FontScale * lineScale;
+ if (scale == 1F)
+ {
+ return this.digitOptions ??= BarcodeTextOptionsFactory.Create(this.digitFont, dpi, this.weight);
+ }
+
+ // A symbol scales at most a handful of placements, so the scaled font and its options are built once and reused while the factor holds.
+ float scaledSize = this.digitFont.Size * scale;
+ if (this.scaledOptions is null || this.scaledOptions.Font.Size != scaledSize)
+ {
+ this.scaledOptions = BarcodeTextOptionsFactory.Create(new Font(this.digitFont, scaledSize), dpi, this.weight);
+ }
+
+ return this.scaledOptions;
+ }
+ }
+}
diff --git a/src/ImageSharp.Drawing/Barcodes/BarcodeEmitter.cs b/src/ImageSharp.Drawing/Barcodes/BarcodeEmitter.cs
new file mode 100644
index 00000000..8098b6b8
--- /dev/null
+++ b/src/ImageSharp.Drawing/Barcodes/BarcodeEmitter.cs
@@ -0,0 +1,159 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.Fonts;
+using SixLabors.ImageSharp.Drawing.Helpers;
+using SixLabors.ImageSharp.Drawing.Processing;
+
+namespace SixLabors.ImageSharp.Drawing.Barcodes;
+
+///
+/// Measures or draws an encoded symbol through the emitter of its kind. The drawing endpoints, the measuring endpoints and the composites share this one choice
+/// of emitter. writes one text block and one measured line for each text placement, and the layout of each measure and draw reads them.
+///
+internal static class BarcodeEmitter
+{
+ ///
+ /// Returns the number of text lines the symbol prints: one for each text placement when the options carry a font, and none otherwise.
+ ///
+ /// The encoded symbol.
+ /// The sizing and painting options.
+ /// The number of lines.
+ public static int LineCount(ref BarcodeSymbol symbol, BarcodeOptions options)
+ => options.Font is null ? 0 : symbol.Text.Length;
+
+ ///
+ /// Rents the buffer that holds the measured lines of the symbol, of them.
+ ///
+ /// The encoded symbol.
+ /// The sizing and painting options.
+ /// The line buffer.
+ public static PooledBuffer CreateLineBuffer(ref BarcodeSymbol symbol, BarcodeOptions options)
+ {
+ int count = LineCount(ref symbol, options);
+ return count == 0 ? default : new(count);
+ }
+
+ ///
+ /// Measures the symbol without drawing anything.
+ ///
+ /// The encoded symbol.
+ /// The sizing and painting options.
+ /// The top left corner of the barcode, in pixels.
+ /// The measured layout of the symbol.
+ public static BarcodeMetrics Measure(ref BarcodeSymbol symbol, BarcodeOptions options, PointF origin)
+ {
+ if (symbol.Kind is BarcodeSymbolKind.Matrix or BarcodeSymbolKind.Dotted or BarcodeSymbolKind.Colored)
+ {
+ return MatrixEmitter.Measure(ref symbol, options, origin);
+ }
+
+ if (symbol.Kind == BarcodeSymbolKind.Hexagonal)
+ {
+ return HexagonalEmitter.Measure(ref symbol, options, origin, out _);
+ }
+
+ return BarEmitter.Measure(ref symbol, options, origin);
+ }
+
+ ///
+ /// Draws the symbol onto the canvas.
+ ///
+ /// The canvas to render onto.
+ /// The encoded symbol.
+ /// The sizing and painting options.
+ /// The top left corner of the barcode, in pixels.
+ /// The brush that fills the renderable bounds before the symbol draws, or for none.
+ /// The measured layout of the symbol, the same as