diff --git a/.gitignore b/.gitignore index 30833808..b50c9489 100644 --- a/.gitignore +++ b/.gitignore @@ -61,3 +61,6 @@ CLAUDE.md # playwright test-results/ playwright-report/ + +# Local font build tooling, kept out of the repo on purpose. +scripts/fonts/ diff --git a/package.json b/package.json index ad4e40bf..bfd2599b 100644 --- a/package.json +++ b/package.json @@ -59,9 +59,9 @@ "@tauri-apps/cli": "^2.11.4", "@testing-library/dom": "^10.4.1", "@testing-library/react": "^16.3.2", - "@testing-library/user-event": "^14.6.3", + "@testing-library/user-event": "^14.6.1", "@types/babel__core": "^7.20.5", - "@types/node": "^26.2.0", + "@types/node": "^26.1.2", "@types/papaparse": "^5.5.2", "@types/pixelmatch": "^5.2.6", "@types/pngjs": "^6.0.5", @@ -70,18 +70,18 @@ "@vitejs/plugin-react": "^6.0.5", "@vitest/coverage-v8": "^4.1.10", "babel-plugin-react-compiler": "^1.0.0", - "eslint": "^10.8.1", + "eslint": "^10.8.0", "eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-react-refresh": "^0.5.3", - "globals": "^17.9.0", + "globals": "^17.8.0", "jsdom": "^30.0.1", "pixelmatch": "^7.2.0", "pngjs": "^7.0.0", "spdx-expression-parse": "^5.0.0", "tailwindcss": "^4.3.3", "typescript": "~6.0.3", - "typescript-eslint": "^8.66.0", - "vite": "^8.2.1", + "typescript-eslint": "^8.65.0", + "vite": "^8.2.0", "vitest": "^4.1.10" } } diff --git a/packages/core/src/lib/customFonts.ts b/packages/core/src/lib/customFonts.ts index bd0bf9aa..29911dba 100644 --- a/packages/core/src/lib/customFonts.ts +++ b/packages/core/src/lib/customFonts.ts @@ -113,6 +113,26 @@ export function builtinFontFamily(fontId: string | undefined): string | undefine return fontId ? BUILTIN_FONT_FAMILY[fontId] : undefined; } +export type DeviceFontLabel = Pick; + +/** Font id whose bitmap cell grid governs a text field, or undefined for a + * scalable face (^A@ TTF, or a ^CW upload aliasing the id). Single resolver + * for render metrics and both anchor boundaries, so they can never disagree + * and anchors round-trip byte-exact. */ +export function resolveDeviceFontId( + fieldFontId: string | undefined, + printerFontName: string | undefined, + label: DeviceFontLabel, +): string | undefined { + if (!fieldFontId && printerFontName) return undefined; + const eff = fieldFontId ?? label.defaultFontId; + if (!eff) return undefined; + // Only a usable mapping shadows a built-in: FontManager's manual rows + // start as { alias, path: '' } and the export still emits the built-in + // ^A font for those, so preview and anchor must keep it too. + return resolvePreviewFontName(label, eff) !== undefined ? undefined : eff; +} + /** Canvas-only preview TTF name; emit/parse ignore this resolver. */ export function resolvePreviewFontName( label: Pick, diff --git a/packages/core/src/lib/emittedAnchor.ts b/packages/core/src/lib/emittedAnchor.ts index 524da144..2b821dc0 100644 --- a/packages/core/src/lib/emittedAnchor.ts +++ b/packages/core/src/lib/emittedAnchor.ts @@ -18,7 +18,7 @@ export function emittedAnchorDots( box?: BoundingBoxDots, ): { x: number; y: number } { if (obj.type === "text") { - const { x, y } = textZplAnchorCoords(obj as Parameters[0]); + const { x, y } = textZplAnchorCoords(obj as Parameters[0], ctx.label); return { x, y }; } if (GRAPHIC_ANCHOR_TYPES.has(obj.type)) { diff --git a/packages/core/src/lib/labelGeometry/deviceFonts.ts b/packages/core/src/lib/labelGeometry/deviceFonts.ts index e4e327ae..b5236654 100644 --- a/packages/core/src/lib/labelGeometry/deviceFonts.ts +++ b/packages/core/src/lib/labelGeometry/deviceFonts.ts @@ -24,9 +24,11 @@ interface DeviceFontSpec { capInkPerMag: number; // visible cap-ink height per magnification (Labelary) capPerEm: number; // substitute font cap-ink per em advPerEm: number; // substitute font advance per em - // Canvas position trims vs Labelary, as fraction of fontSizeDots. The - // Font-0 em-anchor lands the substitute's cap-top slightly off; these - // nudge it back (both positive = down / right). Labelary-calibrated per font. + // Labelary-calibrated canvas trims: the Font-0 em-anchor lands the + // substitute's cap-top slightly off; these nudge it back (positive = + // down / right). yOffEm is a fraction of fontSizeDots; xOffEm also scales + // with scaleX because it cancels the substitute glyph's left side bearing, + // which is horizontal geometry. yOffEm?: number; xOffEm?: number; // Glyph-shape width multiplier on scaleX: matches a single glyph's width to @@ -41,19 +43,43 @@ interface DeviceFontSpec { const MAX_MAG = 10; const DEVICE_FONTS: Record = { - A: { magStep: 9, magWidthStep: 5, advancePerMag: 6, capInkPerMag: 7.125, capPerEm: 0.757, advPerEm: 0.602, yOffEm: 0.096, xOffEm: -0.032, widthCorr: 0.964, letterSpacingEm: 0.021 }, + A: { magStep: 9, magWidthStep: 5, advancePerMag: 6, capInkPerMag: 7.125, capPerEm: 0.757, advPerEm: 0.602, yOffEm: 0.036, xOffEm: -0.032, widthCorr: 0.964, letterSpacingEm: 0.021 }, B: { magStep: 11, magWidthStep: 7, advancePerMag: 9, capInkPerMag: 11.5, capPerEm: 0.762, advPerEm: 0.602, xOffEm: -0.040 }, C: { magStep: 18, magWidthStep: 10, advancePerMag: 12, capInkPerMag: 14.75, capPerEm: 0.757, advPerEm: 0.602, yOffEm: 0.026, xOffEm: -0.043 }, D: { magStep: 18, magWidthStep: 10, advancePerMag: 12, capInkPerMag: 14.75, capPerEm: 0.757, advPerEm: 0.602, yOffEm: 0.026, xOffEm: -0.043 }, - E: { magStep: 28, magWidthStep: 15, advancePerMag: 20, capInkPerMag: 21.875, capPerEm: 0.787, advPerEm: 0.723, yOffEm: 0.198, xOffEm: -0.189 }, - F: { magStep: 26, magWidthStep: 13, advancePerMag: 16, capInkPerMag: 22, capPerEm: 0.757, advPerEm: 0.602, yOffEm: 0.034, xOffEm: -0.052 }, - G: { magStep: 60, magWidthStep: 40, advancePerMag: 48, capInkPerMag: 48, capPerEm: 0.757, advPerEm: 0.602, yOffEm: 0.055, widthCorr: 0.817, letterSpacingEm: 0.142 }, - H: { magStep: 21, magWidthStep: 13, advancePerMag: 19, capInkPerMag: 21, capPerEm: 0.780, advPerEm: 0.723, yOffEm: -0.030, xOffEm: -0.193 }, + E: { magStep: 28, magWidthStep: 15, advancePerMag: 20, capInkPerMag: 21.875, capPerEm: 0.787, advPerEm: 0.723, yOffEm: 0.198, xOffEm: -0.114 }, + F: { magStep: 26, magWidthStep: 13, advancePerMag: 16, capInkPerMag: 22, capPerEm: 0.757, advPerEm: 0.602, yOffEm: 0.034, xOffEm: -0.060 }, + G: { magStep: 60, magWidthStep: 40, advancePerMag: 48, capInkPerMag: 49, capPerEm: 0.757, advPerEm: 0.602, yOffEm: 0.025, widthCorr: 0.817, letterSpacingEm: 0.142 }, + H: { magStep: 21, magWidthStep: 13, advancePerMag: 19, capInkPerMag: 21, capPerEm: 0.780, advPerEm: 0.723, yOffEm: -0.030, xOffEm: -0.117 }, }; const magnify = (requested: number, step: number) => Math.min(MAX_MAG, Math.max(1, Math.round(requested / step))); +/** Cell grid per device font. Exported so fixtures/tests derive their ^A + * heights from the same table the snap uses instead of copying it. */ +export const DEVICE_FONT_CELLS: Readonly< + Record +> = Object.fromEntries( + Object.entries(DEVICE_FONTS).map(([id, s]) => [ + id, + { magStep: s.magStep, magWidthStep: s.magWidthStep }, + ]), +); + +/** Requested ^A height snapped to the cell grid, or null for Font 0 / + * unknown ids / non-positive heights. The firmware anchors a bitmap field + * by its snapped cell, so the anchor transform needs this too. */ +export function deviceFontSnappedHeightDots( + fontId: string | undefined, + heightDots: number, +): number | null { + if (!fontId) return null; + const spec = DEVICE_FONTS[fontId]; + if (!spec || !(heightDots > 0)) return null; + return magnify(heightDots, spec.magStep) * spec.magStep; +} + // Zebra fonts B and H (OCR-A) have no lowercase glyphs: B prints uppercase, // H drops lowercase entirely (so "Text" -> "T"). Mirror that on the canvas. const DEVICE_FONT_CASE: Record = { @@ -109,7 +135,7 @@ export function deviceFontMetrics( fontSizeDots, scaleX, yOffsetDots: (spec.yOffEm ?? 0) * fontSizeDots, - xOffsetDots: (spec.xOffEm ?? 0) * fontSizeDots, + xOffsetDots: (spec.xOffEm ?? 0) * fontSizeDots * scaleX, letterSpacingDots: (spec.letterSpacingEm ?? 0) * fontSizeDots, }; } diff --git a/packages/core/src/lib/labelGeometry/textPositionTransforms.ts b/packages/core/src/lib/labelGeometry/textPositionTransforms.ts index 92430eb6..5d617323 100644 --- a/packages/core/src/lib/labelGeometry/textPositionTransforms.ts +++ b/packages/core/src/lib/labelGeometry/textPositionTransforms.ts @@ -1,9 +1,12 @@ // EM-top-left <-> ZPL anchor (^FO cap-top / ^FT baseline). Applied only // at the zplGenerator/zplParser boundary; editor interactions skip it. +import { deviceFontSnappedHeightDots } from "./deviceFonts"; interface TextLikeProps { fontHeight: number; rotation: "N" | "R" | "I" | "B"; + /** Device font id; the pad terms use its snapped cell height. */ + fontId?: string; } export const ZPL_FONT_HEIGHT_TO_CSS_RATIO = 1.0; @@ -21,9 +24,12 @@ function zplAnchorDelta( blockExtentDots = 0, blockReadingWidthDots = 0, ): { dx: number; dy: number } { - const h = props.fontHeight / ZPL_FONT_HEIGHT_TO_CSS_RATIO; - const pad = props.fontHeight * EM_TOP_ABOVE_CAP; - const bias = props.fontHeight * RENDER_Y_BIAS; + const effHeight = + deviceFontSnappedHeightDots(props.fontId, props.fontHeight) ?? + props.fontHeight; + const h = effHeight / ZPL_FONT_HEIGHT_TO_CSS_RATIO; + const pad = effHeight * EM_TOP_ABOVE_CAP; + const bias = effHeight * RENDER_Y_BIAS; const isFT = positionType === "FT"; // FO I/B anchor at the far reading end of the field: a single line ends at // anchor+inkWidth, but an ^FB block ends at anchor+blockWidth (the frame). diff --git a/packages/core/src/lib/labelGeometry/textRenderMetrics.ts b/packages/core/src/lib/labelGeometry/textRenderMetrics.ts index c762a21c..9402d30d 100644 --- a/packages/core/src/lib/labelGeometry/textRenderMetrics.ts +++ b/packages/core/src/lib/labelGeometry/textRenderMetrics.ts @@ -1,4 +1,4 @@ -import { builtinFontFamily, resolvePreviewFontName } from "../customFonts"; +import { builtinFontFamily, resolveDeviceFontId, resolvePreviewFontName } from "../customFonts"; import { applyDeviceFontCase, deviceFontMetrics } from "./deviceFonts"; import { getFontFamily } from "../fontCache"; import type { LabelObject } from "../../types/Group"; @@ -89,16 +89,11 @@ export function getTextRenderMetrics( const defaultPrinterFontName = label ? resolvePreviewFontName(label, label.defaultFontId) : undefined; - // Device fonts (A-H) preview as their substitute face at the snapped - // magnification; a custom upload aliased to the same id (the field's, or the - // default when the field has none) wins. Canvas-only: gated on `label` so the - // emit/parse path keeps PrintLab metrics and the round-trip inkWidth matches. - const effectiveFontId = fieldFontId ?? label?.defaultFontId; - const effectiveCustomName = label - ? resolvePreviewFontName(label, effectiveFontId) + // Canvas-only: gated on `label` so the emit/parse path keeps PrintLab + // metrics and the round-trip inkWidth matches. + const deviceId = label + ? resolveDeviceFontId(fieldFontId, fieldPrinterFontName, label) : undefined; - const deviceId = - label && !effectiveCustomName ? effectiveFontId : undefined; const fontFamilyOverride = deviceId ? builtinFontFamily(deviceId) : undefined; const fontHeight = fontHeightOverride ?? p.fontHeight; const device = deviceFontMetrics(deviceId, fontHeight, p.fontWidth); diff --git a/packages/core/src/lib/zplParser/flushField.ts b/packages/core/src/lib/zplParser/flushField.ts index 254b2d8e..a9389bdc 100644 --- a/packages/core/src/lib/zplParser/flushField.ts +++ b/packages/core/src/lib/zplParser/flushField.ts @@ -19,6 +19,7 @@ import { decodeTbContent } from "../tbContent"; import { zplFdToModelContent } from "../gs1"; import { dataMatrixFdToGs1Content } from "../dataMatrixFd"; import { zplAnchorToModel } from "../labelGeometry/textPositionTransforms"; +import { resolveDeviceFontId } from "../customFonts"; import { blockInterLineExtentDots } from "../zebraTextLayout"; import { computeTextRenderMetrics } from "../labelGeometry/textRenderMetrics"; import type { TextProps } from "../../registry/text"; @@ -265,10 +266,23 @@ export function createFlushField( blockLineSpacing: s.defaults.fbSpacing, fontHeight: s.field.textH, }); + // Same resolution as the generator so anchors round-trip. + const anchorFontId = resolveDeviceFontId( + s.field.pendingFontId, + s.field.pendingPrinterFontName, + { + customFonts: s.result.labelConfig.customFonts, + defaultFontId: s.defaults.cfFontId, + }, + ); const modelPos = zplAnchorToModel( s.field.x, s.field.y, - { fontHeight: s.field.textH, rotation: s.field.textRot }, + { + fontHeight: s.field.textH, + rotation: s.field.textRot, + fontId: anchorFontId, + }, posType, inkWidthDots, blockExtentDots, diff --git a/packages/core/src/registry/text.ts b/packages/core/src/registry/text.ts index 65cb4fea..2d080390 100644 --- a/packages/core/src/registry/text.ts +++ b/packages/core/src/registry/text.ts @@ -207,7 +207,7 @@ export const text: ObjectTypeCore = { // Serial mode: plain ^A field whose ^FD is wrapped by ^SN/^SF. No block, // reverse, ^FP or variable binding (those are cleared on switch). if (p.serial) { - return `${textFieldPos(obj)}${resolveFontCmd(p, ctx)}${serialFieldData(p.content, p.serial)}`; + return `${textFieldPos(obj, ctx?.label)}${resolveFontCmd(p, ctx)}${serialFieldData(p.content, p.serial)}`; } const mode = resolveTextMode(p); const fontCmd = resolveFontCmd(p, ctx); @@ -233,7 +233,7 @@ export const text: ObjectTypeCore = { const fpDir = p.fpDirection ?? "H"; const fpGap = p.fpCharGap ?? 0; const fpCmd = fpDir !== "H" || fpGap > 0 ? `^FP${fpDir},${fpGap}` : ""; - const anchor = textFieldPos(obj); + const anchor = textFieldPos(obj, ctx?.label); const fd = fdFieldFor(content, ctx, undefined, encodeDefault); // ^FR is the spec-true reverse: it knocks the glyph ink out of whatever is // already drawn (e.g. a black ^GB placed behind the text), so we emit it as diff --git a/packages/core/src/registry/zplHelpers.ts b/packages/core/src/registry/zplHelpers.ts index 3dadfc2e..f154e8c4 100644 --- a/packages/core/src/registry/zplHelpers.ts +++ b/packages/core/src/registry/zplHelpers.ts @@ -6,6 +6,7 @@ import { hasClockMarkers, markersToTokens } from "../lib/fcTemplate"; import { hasControlMarkers, resolveControlMarkers } from "../types/controlKey"; import { classifyField } from "../lib/variableField"; import { modelToZplAnchor } from "../lib/labelGeometry/textPositionTransforms"; +import { resolveDeviceFontId, type DeviceFontLabel } from "../lib/customFonts"; import { getTextRenderMetrics } from "../lib/labelGeometry/textRenderMetrics"; import { blockInterLineExtentDots } from "../lib/zebraTextLayout"; import type { LabelObject } from "../types/Group"; @@ -117,6 +118,8 @@ interface TextLikeObjForFieldPos extends LabelObjectBase { props: { fontHeight: number; rotation: "N" | "R" | "I" | "B"; + fontId?: string; + printerFontName?: string; blockWidth?: number; blockLines?: number; blockLineSpacing?: number; @@ -164,8 +167,13 @@ export function resolveFontCmd( return `^A0${rotation},${fontHeight},${fontWidth}`; } -/** Numeric ZPL anchor (cap-top/baseline) for a text-like field. */ -export function textZplAnchorCoords(obj: TextLikeObjForFieldPos): { +/** Numeric ZPL anchor (cap-top/baseline) for a text-like field. `label` + * resolves the effective device font (^CF default, ^CW alias override) + * for the anchor snap. */ +export function textZplAnchorCoords( + obj: TextLikeObjForFieldPos, + label?: DeviceFontLabel, +): { cmd: "FO" | "FT"; x: number; y: number; @@ -174,10 +182,11 @@ export function textZplAnchorCoords(obj: TextLikeObjForFieldPos): { const metrics = getTextRenderMetrics(obj as unknown as LabelObject); const p = obj.props; const blockExtentDots = blockExtentFor(p); + const anchorFontId = resolveDeviceFontId(p.fontId, p.printerFontName, label ?? {}); const a = modelToZplAnchor( obj.x, obj.y, - obj.props, + { ...p, fontId: anchorFontId }, obj.positionType, metrics?.inkWidthDots ?? 0, blockExtentDots, @@ -188,8 +197,11 @@ export function textZplAnchorCoords(obj: TextLikeObjForFieldPos): { } /** Converts EM top-left model coord to ZPL anchor (cap-top/baseline). */ -export function textFieldPos(obj: TextLikeObjForFieldPos): string { - const a = textZplAnchorCoords(obj); +export function textFieldPos( + obj: TextLikeObjForFieldPos, + label?: DeviceFontLabel, +): string { + const a = textZplAnchorCoords(obj, label); // Same echo contract as fieldPosZ (text is never import-normalised). const z = obj.fieldJustify === "R" ? ",1" : ""; return `^${a.cmd}${a.x},${a.y}${z}`; diff --git a/packages/mcp-server/package.json b/packages/mcp-server/package.json index 08264973..04b58636 100644 --- a/packages/mcp-server/package.json +++ b/packages/mcp-server/package.json @@ -17,6 +17,6 @@ "devDependencies": { "esbuild": "^0.28.1", "postject": "^1.0.0-alpha.6", - "tsx": "^4.23.11" + "tsx": "^4.19.2" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 360f12d8..7dc578de 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -95,7 +95,7 @@ importers: version: 8.0.1 '@eslint/js': specifier: ^10.0.1 - version: 10.0.1(eslint@10.8.1(jiti@2.7.0)) + version: 10.0.1(eslint@10.8.0(jiti@2.7.0)) '@napi-rs/canvas': specifier: ^1.0.3 version: 1.0.3 @@ -104,10 +104,10 @@ importers: version: 1.62.1 '@rolldown/plugin-babel': specifier: ^0.2.3 - version: 0.2.3(@babel/core@8.0.1)(@babel/runtime@7.29.2)(rolldown@1.2.3)(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.11)) + version: 0.2.3(@babel/core@8.0.1)(@babel/runtime@7.29.2)(rolldown@1.2.2)(vite@8.2.0(@types/node@26.1.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)) '@tailwindcss/vite': specifier: ^4.3.3 - version: 4.3.3(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.11)) + version: 4.3.3(vite@8.2.0(@types/node@26.1.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)) '@tauri-apps/cli': specifier: ^2.11.4 version: 2.11.4 @@ -118,14 +118,14 @@ importers: specifier: ^16.3.2 version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@testing-library/user-event': - specifier: ^14.6.3 - version: 14.6.3(@testing-library/dom@10.4.1) + specifier: ^14.6.1 + version: 14.6.1(@testing-library/dom@10.4.1) '@types/babel__core': specifier: ^7.20.5 version: 7.20.5 '@types/node': - specifier: ^26.2.0 - version: 26.2.0 + specifier: ^26.1.2 + version: 26.1.2 '@types/papaparse': specifier: ^5.5.2 version: 5.5.2 @@ -143,7 +143,7 @@ importers: version: 19.2.4(@types/react@19.2.18) '@vitejs/plugin-react': specifier: ^6.0.5 - version: 6.0.5(@rolldown/plugin-babel@0.2.3(@babel/core@8.0.1)(@babel/runtime@7.29.2)(rolldown@1.2.3)(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.11)))(babel-plugin-react-compiler@1.0.0)(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.11)) + version: 6.0.5(@rolldown/plugin-babel@0.2.3(@babel/core@8.0.1)(@babel/runtime@7.29.2)(rolldown@1.2.2)(vite@8.2.0(@types/node@26.1.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)))(babel-plugin-react-compiler@1.0.0)(vite@8.2.0(@types/node@26.1.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)) '@vitest/coverage-v8': specifier: ^4.1.10 version: 4.1.10(vitest@4.1.10) @@ -151,17 +151,17 @@ importers: specifier: ^1.0.0 version: 1.0.0 eslint: - specifier: ^10.8.1 - version: 10.8.1(jiti@2.7.0) + specifier: ^10.8.0 + version: 10.8.0(jiti@2.7.0) eslint-plugin-react-hooks: specifier: ^7.1.1 - version: 7.1.1(eslint@10.8.1(jiti@2.7.0)) + version: 7.1.1(eslint@10.8.0(jiti@2.7.0)) eslint-plugin-react-refresh: specifier: ^0.5.3 - version: 0.5.3(eslint@10.8.1(jiti@2.7.0)) + version: 0.5.3(eslint@10.8.0(jiti@2.7.0)) globals: - specifier: ^17.9.0 - version: 17.9.0 + specifier: ^17.8.0 + version: 17.8.0 jsdom: specifier: ^30.0.1 version: 30.0.1 @@ -181,14 +181,14 @@ importers: specifier: ~6.0.3 version: 6.0.3 typescript-eslint: - specifier: ^8.66.0 - version: 8.66.0(eslint@10.8.1(jiti@2.7.0))(typescript@6.0.3) + specifier: ^8.65.0 + version: 8.65.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3) vite: - specifier: ^8.2.1 - version: 8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.11) + specifier: ^8.2.0 + version: 8.2.0(@types/node@26.1.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1) vitest: specifier: ^4.1.10 - version: 4.1.10(@types/node@26.2.0)(@vitest/coverage-v8@4.1.10)(jsdom@30.0.1)(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.11)) + version: 4.1.10(@types/node@26.1.2)(@vitest/coverage-v8@4.1.10)(jsdom@30.0.1)(vite@8.2.0(@types/node@26.1.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)) packages/core: dependencies: @@ -224,8 +224,8 @@ importers: specifier: ^1.0.0-alpha.6 version: 1.0.0-alpha.6 tsx: - specifier: ^4.23.11 - version: 4.23.11 + specifier: ^4.19.2 + version: 4.23.1 packages: @@ -832,8 +832,8 @@ packages: resolution: {integrity: sha512-OlI657a5XXvKGFX7kNeIzJ8rO7IXt87Mqu2H8rXE46viAuOfum/JA7ysX7+eBhxNKznT+RCZh418mndlcFX3+w==} engines: {node: '>= 10'} - '@oxc-project/types@0.143.0': - resolution: {integrity: sha512-u6JZdLBTLotrNC9Vd6vPssINdzcCzleKAH6EJKImQb7GtYvX5keN2dxkoK44stCc4tffE6QQRtZTXVSzsLUlWA==} + '@oxc-project/types@0.142.0': + resolution: {integrity: sha512-7W+2q5AKQVU36fkaryontrHn3YDt1RyUYXatw9i5H8ocYe2sPKSFB6eS8WNPeRKiN1qAWWZUPm7gwFzJGrccqQ==} '@playwright/test@1.62.1': resolution: {integrity: sha512-DTcUc8qii+cpHvtOwggMtBRMjKZHXYWdw8syRYu2vtzuq4Wxphqq4NfCs5Zt44L6mA8rfDfj+PHnxFc/FeK6mQ==} @@ -857,92 +857,92 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - '@rolldown/binding-android-arm64@1.2.3': - resolution: {integrity: sha512-zrJtHDcaZJ1Fp7xf4hNl+7seH9Cn/N5TwLYkhgXREtBwAd/jaqW3uqeHxpDugJLVICWg4eW44kOQEGJ1r6jCGw==} + '@rolldown/binding-android-arm64@1.2.2': + resolution: {integrity: sha512-l7x215OGvo1s52JWmR8U/DAVzEDWBCIbTm28aeJV/WDTSHgcKXaZTuBT0hJMs5NggilfJTW3clZVvd24yfKJxA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@rolldown/binding-darwin-arm64@1.2.3': - resolution: {integrity: sha512-ieIiibVCp0tX7TLu2cafoNPv8wJyYi01ekXpbf8q2j7F4rGAhhXb/eQh7ge9DRBY78GwmRQtvjZDux7EDbA8kA==} + '@rolldown/binding-darwin-arm64@1.2.2': + resolution: {integrity: sha512-9u9Xv6c1AJZT0FfwH5vrMG5Jjcwhc1MlyrPu0XfTqkzsmqfks2M6W/o5XwAJgVVN/jHpqqngC1WevHKKTIUtIA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-x64@1.2.3': - resolution: {integrity: sha512-Zh9tCon19eDXJoihx0rqKhMUlMYqzwj3aPsSuHmI4RWZh62dWUL+DJN4C5YQya5TcQBJU/Fe8+rY0jhXTQITqA==} + '@rolldown/binding-darwin-x64@1.2.2': + resolution: {integrity: sha512-9W1mbGZAfW3oqd85bhBkmpyHCCzL1TeG/zFFP3vg7b0rlly8cxOcre5nXwz+LHazCwac2MNWgPdPCHndABjpWQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rolldown/binding-freebsd-x64@1.2.3': - resolution: {integrity: sha512-nGbJWewA1wrXXZiQhjAT5rhibGfns5ZNkDVqxsO6zJ3f3YvpoDNNmGMSbbhLuXKjNScaBJVOAboztAWVespQMg==} + '@rolldown/binding-freebsd-x64@1.2.2': + resolution: {integrity: sha512-0p1lhiCSCyaerFwtrdZQUx7NqGk6LQnaRKWX7tFQqwQgvX0rjM15cIkm3pax1UpEakK14C4mOxx/jSqCBdBRqQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rolldown/binding-linux-arm-gnueabihf@1.2.3': - resolution: {integrity: sha512-QNniJr5Kml0kDEB98jiDOJjXNroxIIi0IXIbdYzY26Xt1pVbeP62+KnoIZLwirOymX/0jDk/2gI/bNUv7A7OIw==} + '@rolldown/binding-linux-arm-gnueabihf@1.2.2': + resolution: {integrity: sha512-e+cOJXrJ2L3zx6YzqPg+f6Wbk3V1cKB8bOhbaYdVYN3DdquzNdRAmrbETz1qnt5yp/c7JNlNjmITiA2cVneQ7w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm64-gnu@1.2.3': - resolution: {integrity: sha512-TkqEAcmmvH3I/q4114NB4RVt6241Dao48pF45uLcFGrwAaIn0iITgTAKP/dLjbN0R4buJjGb91+UHSoFmpgIWw==} + '@rolldown/binding-linux-arm64-gnu@1.2.2': + resolution: {integrity: sha512-JsSMsj6sNat/MuhG5fnBD7QgbtpHKVe30x5/bAVirDHdhoQRXJkF6xc0Jqk8O4fiCUQAzMOoH9wZi3m60c8wtg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-arm64-musl@1.2.3': - resolution: {integrity: sha512-NHqjnxpsndf4MPymxteFAWHHfkTL8HjWh1KB7z23ofZ6QO2euONuxDXjat69dKZRALnGypg8k8SsK8vZJoXv1Q==} + '@rolldown/binding-linux-arm64-musl@1.2.2': + resolution: {integrity: sha512-B5G/zJdHaoJn9vD50eGHWkiWfmq8Uhi3IiLPJTzmZTrAalk1bztUikSXo0qga18ibE0IXboyeMUnhPjhAJ45wQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@rolldown/binding-linux-ppc64-gnu@1.2.3': - resolution: {integrity: sha512-6tbrbwfz5GB9DQ4Jwo6hy9v+vR31xZlvzZ6n5Xut6Hhx5PvrA9q/HsK8KMaYQp063iqZGXwNvZtYNLD7EM/x0w==} + '@rolldown/binding-linux-ppc64-gnu@1.2.2': + resolution: {integrity: sha512-6mC/awzKka8W6EoekjegpfGkjz8jXWDX63pqu/HYVpyKtZfu65Jsh4QAH3Kej3CAv/c1oGX7psTmFEbr0mDxLA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-s390x-gnu@1.2.3': - resolution: {integrity: sha512-oyuXxXmoZHjXC917IAPFAAv4wWAa0cM9afk8nx1+9/jNNOX1uPf8yDA6p7G0RypOfw/X0PQt5IfoquY1um+zSg==} + '@rolldown/binding-linux-s390x-gnu@1.2.2': + resolution: {integrity: sha512-412MX9fJLdA1IK28EZnc8jYv2HRTleOZgfLQumJ5zy7OeJLZlg/CETwFaXjNmGVxG51cFHpKLqb5LKvBC+HsHA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-gnu@1.2.3': - resolution: {integrity: sha512-TytMwF2KVGqP2tgd0I1OY0PAv78dZRAYcF5ssDzjM34SUXCED3uXvSd5+lHoC0bTD6eEdFz7LdQNCO1y0oVk9w==} + '@rolldown/binding-linux-x64-gnu@1.2.2': + resolution: {integrity: sha512-Q/+HI/ToJafZ1iCqGgVQXUEkIjufHCTF0gBQ2a5o3cg7GJ2h0qyq3nvvSmU+bGda2/7ygXpTY4TM6gO9OhQ0ZA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-musl@1.2.3': - resolution: {integrity: sha512-/E9m3qstrJFVPoULV25mVQblSNExY2+kBsYe4sy0Tn0yOOgJ8wZbZt3KnRbF/XeU2Gl1STKUQnDNTqhIE5MD4A==} + '@rolldown/binding-linux-x64-musl@1.2.2': + resolution: {integrity: sha512-ZKp/w41n6wCvxzxQHtQSbuphfX3Y4cCvbjkKHusrLx4lh+JWLTU7StSltO/DKARISzbj368d+qUaCjI8K2wzXw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@rolldown/binding-openharmony-arm64@1.2.3': - resolution: {integrity: sha512-Kr0OcsoQI816i6HOl3vFHpd1K0eZyh76zgfj4c1nTyaTsd5r2Mj1lwM4R90y/qaCfmTn9eHy0SKwi98eitRxug==} + '@rolldown/binding-openharmony-arm64@1.2.2': + resolution: {integrity: sha512-pxE6xD4KS3eAROkKK5yrhB9/3+vhlhVGMvlQLbdpzrBGDbKrnzx3RLwPaHvasLo6jgaiBLn7e04Df9C4tYhjmA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@rolldown/binding-win32-arm64-msvc@1.2.3': - resolution: {integrity: sha512-hOtMwTqnME+/gJcH/PCZ0wn0zPUjiWOgkHpxbSJpfGKMezHltx1S7/k1SitzVa7Ww2cqrDDaFbZEhcJZO8o+Jw==} + '@rolldown/binding-win32-arm64-msvc@1.2.2': + resolution: {integrity: sha512-4MqEue5re+xIZzAWsB8sj0P1kqZySWqIuN4t6QaIO/YA6SFwySOLruvWQFzfmqk8LBK2P30KCSJwf6mJCZZ5/A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.2.3': - resolution: {integrity: sha512-ekcqMMkI2PlhYnfzQnB/cEdYUVVJViWvoUyLrbzgDoi3Snfc1mVBwdnc306ufA5ejy8JSPjT2RlW1nQSjW7efg==} + '@rolldown/binding-win32-x64-msvc@1.2.2': + resolution: {integrity: sha512-NweNxxD0Nf9t8v7kodun45Ijp3EIwYY+uydPP6qBEYvfBqhIjN6dZMzlQja3tqX/aLs3F3Uz+AxDpKgRhpOZQg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -1189,8 +1189,8 @@ packages: '@types/react-dom': optional: true - '@testing-library/user-event@14.6.3': - resolution: {integrity: sha512-6dBq67jT8lE+JTE8Exm02Kt6ze43hz1jdiSpSJwtTZiT1xQQ6b7nZYTTQ9njdArdU8XklOwaDp/AbT/eYSKF4g==} + '@testing-library/user-event@14.6.1': + resolution: {integrity: sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==} engines: {node: '>=12', npm: '>=6'} peerDependencies: '@testing-library/dom': '>=7.21.4' @@ -1231,8 +1231,8 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - '@types/node@26.2.0': - resolution: {integrity: sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg==} + '@types/node@26.1.2': + resolution: {integrity: sha512-Vu4a5UFA9rIIFJ7rB/Vaafh9lrCQszopTCx6KjFboXTGQbPNasehVR5TEiithSDGyd1DEiUByggTZsg8jukeIg==} '@types/papaparse@5.5.2': resolution: {integrity: sha512-gFnFp/JMzLHCwRf7tQHrNnfhN4eYBVYYI897CGX4MY1tzY9l2aLkVyx2IlKZ/SAqDbB3I1AOZW5gTMGGsqWliA==} @@ -1261,63 +1261,63 @@ packages: '@types/react@19.2.18': resolution: {integrity: sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==} - '@typescript-eslint/eslint-plugin@8.66.0': - resolution: {integrity: sha512-p088eaGrzYz1s+7cov0aMOCkNGTJlVxF4jgubf28c8L0Cv9Rloj8YBHnv4hXLq6IIEE1AsjNWavO+k+8kP2Y0A==} + '@typescript-eslint/eslint-plugin@8.65.0': + resolution: {integrity: sha512-IEgob78X12rHpUmtcwFsXhZdVGJtwTVP8FiCLZkR6GlYVrl2PcuB+KhCE5BlVC/eQpQnu8WXRtkHZuPar+gCRA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.66.0 + '@typescript-eslint/parser': ^8.65.0 eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/parser@8.66.0': - resolution: {integrity: sha512-X6ypGChaWYk6PBtUg2BwuTZEFFcHJAtGTVJ9/lCTOufhZ4i9fNolQNnktq+kkMCwMj7V8Svsq7+TxSDslmhE0g==} + '@typescript-eslint/parser@8.65.0': + resolution: {integrity: sha512-CZ4nMxWwgu1HEEFNkeaCptra9QCtkmKdgf3sWh1rl1trIhmxLilgTV4cwcbQ4wemnT4sWQN8CaKOmdYx+g2gMA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/project-service@8.66.0': - resolution: {integrity: sha512-7MthGPTt4BP69lSryqpqq8HQqxuzynssckL/jyDyk3+TNMQ3y2jFWkptCrktWvBrP+EH787Nl5N5Qpw7WZg+5g==} + '@typescript-eslint/project-service@8.65.0': + resolution: {integrity: sha512-SxnPhbTsGahizDgbu7oqFH/xVtzIqMd/s+WtnSxNxJZJpLbdT5IPdzg8EZxO3+PoKahXmwJLeNQOpKJb3/bi7Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/scope-manager@8.66.0': - resolution: {integrity: sha512-8TGcH25j9zqJ/IULB/ppyhRvxA8QYfFEZ7nfbg6/BN9spDgb8fPWQXlE5l8TWBL50EtUx007uZ1o9VOwrq2/9g==} + '@typescript-eslint/scope-manager@8.65.0': + resolution: {integrity: sha512-Esbl8OSYiVxBokYgWPf7VVWg/BE798wXhimnn9ML9Pt5qoDf8bfQlgjlKXR/k98+AcNzlLKYrpCcrcuZ9DZLgg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.66.0': - resolution: {integrity: sha512-9D5gLYZG4rOjcoag8MQ/fWI8WqA9wcPDyOGyWtWFhvM1lHRbliqUSPIY5J3zqCU1tvSwzXxnnjhQhz5Ne7mJ4g==} + '@typescript-eslint/tsconfig-utils@8.65.0': + resolution: {integrity: sha512-j6GzGqCiRdA7Qhur2VVmKZAkBLfnHFQfx4TaJGL9RMveZqCo48jSHHO0DTgizEnGhtWnqmbtCUSrqSkdiY/0Hg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/type-utils@8.66.0': - resolution: {integrity: sha512-LG2dWfjZQQp0ADtAu/EWJVayefGL2UEZ3CDeI44D9v3rXB/WYUqE/jpO28KrEKul5AySrmI+Zh1v6v+xW2U9+g==} + '@typescript-eslint/type-utils@8.65.0': + resolution: {integrity: sha512-YjaZ7PRI5qY7ax2L3PbvX0rRyGtipAReCWs0mhhDBHjH/vl0g0BonaGXrKdKpMbIIsMIwDgbk/xzkBTyAltS5g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/types@8.66.0': - resolution: {integrity: sha512-H6gcYaSDOyvL3AD/jHUtUFo2jqGgn/F6nuyuZSu0QTesxL+cP4dQoIMrODRofuJC09g64+WgZ6tE19Y1N2YIFQ==} + '@typescript-eslint/types@8.65.0': + resolution: {integrity: sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.66.0': - resolution: {integrity: sha512-8/x4INiiQb10jGgXYD7116/zQ+OL84ZIFn0za68wwFHCanT/VLbBEroWht8RV8fn0/ZCAoazHLQgwUC0UQcDfg==} + '@typescript-eslint/typescript-estree@8.65.0': + resolution: {integrity: sha512-JboAE2swaYt4tb1fHhHTABE2K+OLy09XfcTbhnk4Pw96f9dd2e9iYsJ28gBggHlo5z5x1rkyWvcPoTuNTd4oGg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/utils@8.66.0': - resolution: {integrity: sha512-jasearZPolBw5NJNYGMwxzHMF83niVWmMU1VdHzG1CyfI2VS7f7nZltnKtHcg20hW+7Uo5GfK4MeDPoU3qI8EA==} + '@typescript-eslint/utils@8.65.0': + resolution: {integrity: sha512-gXiwIHsYreboxeJucHKPvgwl7dXt50mF8s1/c00cP/WoVTyWKFdtfhRWwZiXYFU5H2O8vVoSLNrexFZjYS/SGA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/visitor-keys@8.66.0': - resolution: {integrity: sha512-dkKR8q+lKciskj1Y3vthHktl+3cMLWGyVUP23bRiPZ5O9BRT++4EqDDV+TVeIKBL1VXVEqrJlz8MYbcnvJcAlg==} + '@typescript-eslint/visitor-keys@8.65.0': + resolution: {integrity: sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@vitejs/plugin-react@6.0.5': @@ -1380,8 +1380,8 @@ packages: peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - acorn@8.18.0: - resolution: {integrity: sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==} + acorn@8.17.0: + resolution: {integrity: sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==} engines: {node: '>=0.4.0'} hasBin: true @@ -1440,8 +1440,8 @@ packages: resolution: {integrity: sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==} engines: {node: '>=18'} - brace-expansion@5.0.9: - resolution: {integrity: sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==} + brace-expansion@5.0.8: + resolution: {integrity: sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==} engines: {node: 20 || >=22} browserslist@4.28.2: @@ -1632,8 +1632,8 @@ packages: resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - eslint@10.8.1: - resolution: {integrity: sha512-wqA7W2jbsC/BnV9Iv1UZpKVFkO1AdNoSmYW8NWG4HNOBbkAMvIqDZ27pI2f07dqn583NcIC44ckjAcOXDL1QbQ==} + eslint@10.8.0: + resolution: {integrity: sha512-nuKKvN+oIBO0koN7Tm7dlkmnkc21mtt0QJLwAKzjLq14y6lRTdVG36MZHJ8eQHwdJMwZbQNMlPOYedMq/oVJvQ==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} hasBin: true peerDependencies: @@ -1731,8 +1731,8 @@ packages: resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} engines: {node: '>=16'} - flatted@3.4.4: - resolution: {integrity: sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==} + flatted@3.4.3: + resolution: {integrity: sha512-/zipXxyO6rGvuNGDiULY9MvEGSkb2gaG4GGH4ygMi0ZZzyMHdUZBmntJmx5x1G2VuPytCwGN4xsJP6cw+sK+vQ==} forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} @@ -1771,8 +1771,8 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} - globals@17.9.0: - resolution: {integrity: sha512-m/MvAW61QVU5VDNF1Vj8axt016h8w7L5TU1e9zlab7XIttAT2YAlCwl75K1fOqvMM9apmD7lbCIRhpfkhmxhCg==} + globals@17.8.0: + resolution: {integrity: sha512-Zz/LMDZScFmkakeL2cTHzf+PbWKdpU3uclqkZT7TjDG58j5WPt0PpA+n9uPI24fZtlw07q0OtEi84K+umsRzqQ==} engines: {node: '>=18'} gopd@1.2.0: @@ -2132,15 +2132,15 @@ packages: resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} engines: {node: '>=18'} - minimatch@10.2.6: - resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==} + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} engines: {node: 18 || 20 || >=22} ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - nanoid@3.3.18: - resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==} + nanoid@3.3.17: + resolution: {integrity: sha512-xQLf0A3HOMlgHq0n247/LRuAOYmB7dXJ/DvAxGvsSBij45XtBSmQycu+F8ODbHwns/XyFZagyL1+J0Offw1E0g==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true @@ -2243,8 +2243,8 @@ packages: resolution: {integrity: sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==} engines: {node: '>=14.19.0'} - postcss@8.5.26: - resolution: {integrity: sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==} + postcss@8.5.25: + resolution: {integrity: sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==} engines: {node: ^10 || ^12 || >=14} postject@1.0.0-alpha.6: @@ -2320,8 +2320,8 @@ packages: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} - rolldown@1.2.3: - resolution: {integrity: sha512-rn9wpmxplLf7NLNyCk9FyWh3FM43DbY8jOzCdEPzH7uflhTftRbCEpqi6Ly2osgoU8OwObtmavMbWLaWy4LX7A==} + rolldown@1.2.2: + resolution: {integrity: sha512-opwpo1tQBAcpSUJDt94B7hhLNGOKjCdE//XXjeLrnx9b83bjnw45tXdg1b09yEw/VLFBJGZpwRULMmOZo7ol+A==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true @@ -2474,8 +2474,8 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - tsx@4.23.11: - resolution: {integrity: sha512-Ry2oTEUnhBdeEdWIztY8kf3/nBGnPnjMLVGL0YfdRXMORuPER5NlKmayqxtxRxwB1xBN+RivRaJfe7PM1rtiyw==} + tsx@4.23.1: + resolution: {integrity: sha512-GQHnkIfxyx1wYCOS/wonik5MVRZU9hi1TEZmzGZSCJB1y9YgoZ8H6itNE/u4suE+yLmOzuE4E5S4TZ/ZX2wcWQ==} engines: {node: '>=18.0.0'} hasBin: true @@ -2487,8 +2487,8 @@ packages: resolution: {integrity: sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==} engines: {node: '>= 18'} - typescript-eslint@8.66.0: - resolution: {integrity: sha512-QlEbBPz/RuJ1XUHj29nm3t0F/O/cSlEnntozqPOYHnnTGAXFamnMBu5i9Vn6vhUPHGAjR+Vl+5J8vPN/BMUrJw==} + typescript-eslint@8.65.0: + resolution: {integrity: sha512-/ggrHAwyjENDusvyxbuqxAC2dTnZg/Z8F+fgQtYIz+L6n/9HfSlEZcFGV/NsMNa6CkGk0xUjUAFwC0vHOflvIA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 @@ -2528,8 +2528,8 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} - vite@8.2.1: - resolution: {integrity: sha512-EU/eS7BH3XROHh2YnBefjM6DBKA6ZeMZEYQbj7NLWg5wHYlhB8B/Mayd5XsgWq+NFYccDOTemRpdETWR6Ka/lw==} + vite@8.2.0: + resolution: {integrity: sha512-pn+CFpM0lwDeKwmOq1ZaBK/9sjorZcgqxki6MbY/jPEVd9vichIlmlD4HmQ5wdP5EgqQCFRaACBxMC7uEGc6lQ==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -3050,9 +3050,9 @@ snapshots: '@esbuild/win32-x64@0.28.1': optional: true - '@eslint-community/eslint-utils@4.10.1(eslint@10.8.1(jiti@2.7.0))': + '@eslint-community/eslint-utils@4.10.1(eslint@10.8.0(jiti@2.7.0))': dependencies: - eslint: 10.8.1(jiti@2.7.0) + eslint: 10.8.0(jiti@2.7.0) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.2': {} @@ -3061,7 +3061,7 @@ snapshots: dependencies: '@eslint/object-schema': 3.0.5 debug: 4.4.3 - minimatch: 10.2.6 + minimatch: 10.2.5 transitivePeerDependencies: - supports-color @@ -3073,9 +3073,9 @@ snapshots: dependencies: '@types/json-schema': 7.0.15 - '@eslint/js@10.0.1(eslint@10.8.1(jiti@2.7.0))': + '@eslint/js@10.0.1(eslint@10.8.0(jiti@2.7.0))': optionalDependencies: - eslint: 10.8.1(jiti@2.7.0) + eslint: 10.8.0(jiti@2.7.0) '@eslint/object-schema@3.0.5': {} @@ -3249,7 +3249,7 @@ snapshots: '@napi-rs/canvas-win32-arm64-msvc': 1.0.3 '@napi-rs/canvas-win32-x64-msvc': 1.0.3 - '@oxc-project/types@0.143.0': {} + '@oxc-project/types@0.142.0': {} '@playwright/test@1.62.1': dependencies: @@ -3274,56 +3274,56 @@ snapshots: dependencies: react: 19.2.8 - '@rolldown/binding-android-arm64@1.2.3': + '@rolldown/binding-android-arm64@1.2.2': optional: true - '@rolldown/binding-darwin-arm64@1.2.3': + '@rolldown/binding-darwin-arm64@1.2.2': optional: true - '@rolldown/binding-darwin-x64@1.2.3': + '@rolldown/binding-darwin-x64@1.2.2': optional: true - '@rolldown/binding-freebsd-x64@1.2.3': + '@rolldown/binding-freebsd-x64@1.2.2': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.2.3': + '@rolldown/binding-linux-arm-gnueabihf@1.2.2': optional: true - '@rolldown/binding-linux-arm64-gnu@1.2.3': + '@rolldown/binding-linux-arm64-gnu@1.2.2': optional: true - '@rolldown/binding-linux-arm64-musl@1.2.3': + '@rolldown/binding-linux-arm64-musl@1.2.2': optional: true - '@rolldown/binding-linux-ppc64-gnu@1.2.3': + '@rolldown/binding-linux-ppc64-gnu@1.2.2': optional: true - '@rolldown/binding-linux-s390x-gnu@1.2.3': + '@rolldown/binding-linux-s390x-gnu@1.2.2': optional: true - '@rolldown/binding-linux-x64-gnu@1.2.3': + '@rolldown/binding-linux-x64-gnu@1.2.2': optional: true - '@rolldown/binding-linux-x64-musl@1.2.3': + '@rolldown/binding-linux-x64-musl@1.2.2': optional: true - '@rolldown/binding-openharmony-arm64@1.2.3': + '@rolldown/binding-openharmony-arm64@1.2.2': optional: true - '@rolldown/binding-win32-arm64-msvc@1.2.3': + '@rolldown/binding-win32-arm64-msvc@1.2.2': optional: true - '@rolldown/binding-win32-x64-msvc@1.2.3': + '@rolldown/binding-win32-x64-msvc@1.2.2': optional: true - '@rolldown/plugin-babel@0.2.3(@babel/core@8.0.1)(@babel/runtime@7.29.2)(rolldown@1.2.3)(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.11))': + '@rolldown/plugin-babel@0.2.3(@babel/core@8.0.1)(@babel/runtime@7.29.2)(rolldown@1.2.2)(vite@8.2.0(@types/node@26.1.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1))': dependencies: '@babel/core': 8.0.1 picomatch: 4.0.4 - rolldown: 1.2.3 + rolldown: 1.2.2 optionalDependencies: '@babel/runtime': 7.29.2 - vite: 8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.11) + vite: 8.2.0(@types/node@26.1.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1) '@rolldown/pluginutils@1.0.1': {} @@ -3394,12 +3394,12 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.3.3 '@tailwindcss/oxide-win32-x64-msvc': 4.3.3 - '@tailwindcss/vite@4.3.3(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.11))': + '@tailwindcss/vite@4.3.3(vite@8.2.0(@types/node@26.1.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1))': dependencies: '@tailwindcss/node': 4.3.3 '@tailwindcss/oxide': 4.3.3 tailwindcss: 4.3.3 - vite: 8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.11) + vite: 8.2.0(@types/node@26.1.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1) '@tanstack/react-virtual@3.14.3(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: @@ -3499,7 +3499,7 @@ snapshots: '@types/react': 19.2.18 '@types/react-dom': 19.2.4(@types/react@19.2.18) - '@testing-library/user-event@14.6.3(@testing-library/dom@10.4.1)': + '@testing-library/user-event@14.6.1(@testing-library/dom@10.4.1)': dependencies: '@testing-library/dom': 10.4.1 @@ -3543,21 +3543,21 @@ snapshots: '@types/json-schema@7.0.15': {} - '@types/node@26.2.0': + '@types/node@26.1.2': dependencies: undici-types: 8.3.0 '@types/papaparse@5.5.2': dependencies: - '@types/node': 26.2.0 + '@types/node': 26.1.2 '@types/pixelmatch@5.2.6': dependencies: - '@types/node': 26.2.0 + '@types/node': 26.1.2 '@types/pngjs@6.0.5': dependencies: - '@types/node': 26.2.0 + '@types/node': 26.1.2 '@types/react-dom@19.2.4(@types/react@19.2.18)': dependencies: @@ -3575,15 +3575,15 @@ snapshots: dependencies: csstype: 3.2.3 - '@typescript-eslint/eslint-plugin@8.66.0(@typescript-eslint/parser@8.66.0(eslint@10.8.1(jiti@2.7.0))(typescript@6.0.3))(eslint@10.8.1(jiti@2.7.0))(typescript@6.0.3)': + '@typescript-eslint/eslint-plugin@8.65.0(@typescript-eslint/parser@8.65.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.66.0(eslint@10.8.1(jiti@2.7.0))(typescript@6.0.3) - '@typescript-eslint/scope-manager': 8.66.0 - '@typescript-eslint/type-utils': 8.66.0(eslint@10.8.1(jiti@2.7.0))(typescript@6.0.3) - '@typescript-eslint/utils': 8.66.0(eslint@10.8.1(jiti@2.7.0))(typescript@6.0.3) - '@typescript-eslint/visitor-keys': 8.66.0 - eslint: 10.8.1(jiti@2.7.0) + '@typescript-eslint/parser': 8.65.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/scope-manager': 8.65.0 + '@typescript-eslint/type-utils': 8.65.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/utils': 8.65.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.65.0 + eslint: 10.8.0(jiti@2.7.0) ignore: 7.0.6 natural-compare: 1.4.0 ts-api-utils: 2.5.0(typescript@6.0.3) @@ -3591,58 +3591,58 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.66.0(eslint@10.8.1(jiti@2.7.0))(typescript@6.0.3)': + '@typescript-eslint/parser@8.65.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3)': dependencies: - '@typescript-eslint/scope-manager': 8.66.0 - '@typescript-eslint/types': 8.66.0 - '@typescript-eslint/typescript-estree': 8.66.0(typescript@6.0.3) - '@typescript-eslint/visitor-keys': 8.66.0 + '@typescript-eslint/scope-manager': 8.65.0 + '@typescript-eslint/types': 8.65.0 + '@typescript-eslint/typescript-estree': 8.65.0(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.65.0 debug: 4.4.3 - eslint: 10.8.1(jiti@2.7.0) + eslint: 10.8.0(jiti@2.7.0) typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.66.0(typescript@6.0.3)': + '@typescript-eslint/project-service@8.65.0(typescript@6.0.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.66.0(typescript@6.0.3) - '@typescript-eslint/types': 8.66.0 + '@typescript-eslint/tsconfig-utils': 8.65.0(typescript@6.0.3) + '@typescript-eslint/types': 8.65.0 debug: 4.4.3 typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.66.0': + '@typescript-eslint/scope-manager@8.65.0': dependencies: - '@typescript-eslint/types': 8.66.0 - '@typescript-eslint/visitor-keys': 8.66.0 + '@typescript-eslint/types': 8.65.0 + '@typescript-eslint/visitor-keys': 8.65.0 - '@typescript-eslint/tsconfig-utils@8.66.0(typescript@6.0.3)': + '@typescript-eslint/tsconfig-utils@8.65.0(typescript@6.0.3)': dependencies: typescript: 6.0.3 - '@typescript-eslint/type-utils@8.66.0(eslint@10.8.1(jiti@2.7.0))(typescript@6.0.3)': + '@typescript-eslint/type-utils@8.65.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3)': dependencies: - '@typescript-eslint/types': 8.66.0 - '@typescript-eslint/typescript-estree': 8.66.0(typescript@6.0.3) - '@typescript-eslint/utils': 8.66.0(eslint@10.8.1(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/types': 8.65.0 + '@typescript-eslint/typescript-estree': 8.65.0(typescript@6.0.3) + '@typescript-eslint/utils': 8.65.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3) debug: 4.4.3 - eslint: 10.8.1(jiti@2.7.0) + eslint: 10.8.0(jiti@2.7.0) ts-api-utils: 2.5.0(typescript@6.0.3) typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.66.0': {} + '@typescript-eslint/types@8.65.0': {} - '@typescript-eslint/typescript-estree@8.66.0(typescript@6.0.3)': + '@typescript-eslint/typescript-estree@8.65.0(typescript@6.0.3)': dependencies: - '@typescript-eslint/project-service': 8.66.0(typescript@6.0.3) - '@typescript-eslint/tsconfig-utils': 8.66.0(typescript@6.0.3) - '@typescript-eslint/types': 8.66.0 - '@typescript-eslint/visitor-keys': 8.66.0 + '@typescript-eslint/project-service': 8.65.0(typescript@6.0.3) + '@typescript-eslint/tsconfig-utils': 8.65.0(typescript@6.0.3) + '@typescript-eslint/types': 8.65.0 + '@typescript-eslint/visitor-keys': 8.65.0 debug: 4.4.3 - minimatch: 10.2.6 + minimatch: 10.2.5 semver: 7.8.5 tinyglobby: 0.2.17 ts-api-utils: 2.5.0(typescript@6.0.3) @@ -3650,28 +3650,28 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.66.0(eslint@10.8.1(jiti@2.7.0))(typescript@6.0.3)': + '@typescript-eslint/utils@8.65.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3)': dependencies: - '@eslint-community/eslint-utils': 4.10.1(eslint@10.8.1(jiti@2.7.0)) - '@typescript-eslint/scope-manager': 8.66.0 - '@typescript-eslint/types': 8.66.0 - '@typescript-eslint/typescript-estree': 8.66.0(typescript@6.0.3) - eslint: 10.8.1(jiti@2.7.0) + '@eslint-community/eslint-utils': 4.10.1(eslint@10.8.0(jiti@2.7.0)) + '@typescript-eslint/scope-manager': 8.65.0 + '@typescript-eslint/types': 8.65.0 + '@typescript-eslint/typescript-estree': 8.65.0(typescript@6.0.3) + eslint: 10.8.0(jiti@2.7.0) typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.66.0': + '@typescript-eslint/visitor-keys@8.65.0': dependencies: - '@typescript-eslint/types': 8.66.0 + '@typescript-eslint/types': 8.65.0 eslint-visitor-keys: 5.0.1 - '@vitejs/plugin-react@6.0.5(@rolldown/plugin-babel@0.2.3(@babel/core@8.0.1)(@babel/runtime@7.29.2)(rolldown@1.2.3)(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.11)))(babel-plugin-react-compiler@1.0.0)(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.11))': + '@vitejs/plugin-react@6.0.5(@rolldown/plugin-babel@0.2.3(@babel/core@8.0.1)(@babel/runtime@7.29.2)(rolldown@1.2.2)(vite@8.2.0(@types/node@26.1.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)))(babel-plugin-react-compiler@1.0.0)(vite@8.2.0(@types/node@26.1.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1))': dependencies: '@rolldown/pluginutils': 1.0.1 - vite: 8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.11) + vite: 8.2.0(@types/node@26.1.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1) optionalDependencies: - '@rolldown/plugin-babel': 0.2.3(@babel/core@8.0.1)(@babel/runtime@7.29.2)(rolldown@1.2.3)(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.11)) + '@rolldown/plugin-babel': 0.2.3(@babel/core@8.0.1)(@babel/runtime@7.29.2)(rolldown@1.2.2)(vite@8.2.0(@types/node@26.1.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)) babel-plugin-react-compiler: 1.0.0 '@vitest/coverage-v8@4.1.10(vitest@4.1.10)': @@ -3686,7 +3686,7 @@ snapshots: obug: 2.1.3 std-env: 4.1.0 tinyrainbow: 3.1.0 - vitest: 4.1.10(@types/node@26.2.0)(@vitest/coverage-v8@4.1.10)(jsdom@30.0.1)(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.11)) + vitest: 4.1.10(@types/node@26.1.2)(@vitest/coverage-v8@4.1.10)(jsdom@30.0.1)(vite@8.2.0(@types/node@26.1.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)) '@vitest/expect@4.1.10': dependencies: @@ -3697,13 +3697,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.10(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.11))': + '@vitest/mocker@4.1.10(vite@8.2.0(@types/node@26.1.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1))': dependencies: '@vitest/spy': 4.1.10 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.11) + vite: 8.2.0(@types/node@26.1.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1) '@vitest/pretty-format@4.1.10': dependencies: @@ -3734,11 +3734,11 @@ snapshots: mime-types: 3.0.2 negotiator: 1.0.0 - acorn-jsx@5.3.2(acorn@8.18.0): + acorn-jsx@5.3.2(acorn@8.17.0): dependencies: - acorn: 8.18.0 + acorn: 8.17.0 - acorn@8.18.0: {} + acorn@8.17.0: {} ajv-formats@3.0.1(ajv@8.20.0): optionalDependencies: @@ -3804,7 +3804,7 @@ snapshots: transitivePeerDependencies: - supports-color - brace-expansion@5.0.9: + brace-expansion@5.0.8: dependencies: balanced-match: 4.0.4 @@ -3957,20 +3957,20 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-plugin-react-hooks@7.1.1(eslint@10.8.1(jiti@2.7.0)): + eslint-plugin-react-hooks@7.1.1(eslint@10.8.0(jiti@2.7.0)): dependencies: '@babel/core': 7.29.7 '@babel/parser': 7.29.3 - eslint: 10.8.1(jiti@2.7.0) + eslint: 10.8.0(jiti@2.7.0) hermes-parser: 0.25.1 zod: 4.4.3 zod-validation-error: 4.0.2(zod@4.4.3) transitivePeerDependencies: - supports-color - eslint-plugin-react-refresh@0.5.3(eslint@10.8.1(jiti@2.7.0)): + eslint-plugin-react-refresh@0.5.3(eslint@10.8.0(jiti@2.7.0)): dependencies: - eslint: 10.8.1(jiti@2.7.0) + eslint: 10.8.0(jiti@2.7.0) eslint-scope@9.1.2: dependencies: @@ -3983,9 +3983,9 @@ snapshots: eslint-visitor-keys@5.0.1: {} - eslint@10.8.1(jiti@2.7.0): + eslint@10.8.0(jiti@2.7.0): dependencies: - '@eslint-community/eslint-utils': 4.10.1(eslint@10.8.1(jiti@2.7.0)) + '@eslint-community/eslint-utils': 4.10.1(eslint@10.8.0(jiti@2.7.0)) '@eslint-community/regexpp': 4.12.2 '@eslint/config-array': 0.23.5 '@eslint/config-helpers': 0.7.0 @@ -4012,7 +4012,7 @@ snapshots: imurmurhash: 0.1.4 is-glob: 4.0.3 json-stable-stringify-without-jsonify: 1.0.1 - minimatch: 10.2.6 + minimatch: 10.2.5 natural-compare: 1.4.0 optionator: 0.9.4 optionalDependencies: @@ -4022,8 +4022,8 @@ snapshots: espree@11.2.0: dependencies: - acorn: 8.18.0 - acorn-jsx: 5.3.2(acorn@8.18.0) + acorn: 8.17.0 + acorn-jsx: 5.3.2(acorn@8.17.0) eslint-visitor-keys: 5.0.1 esquery@1.7.0: @@ -4129,10 +4129,10 @@ snapshots: flat-cache@4.0.1: dependencies: - flatted: 3.4.4 + flatted: 3.4.3 keyv: 4.5.4 - flatted@3.4.4: {} + flatted@3.4.3: {} forwarded@0.2.0: {} @@ -4170,7 +4170,7 @@ snapshots: dependencies: is-glob: 4.0.3 - globals@17.9.0: {} + globals@17.8.0: {} gopd@1.2.0: {} @@ -4455,13 +4455,13 @@ snapshots: dependencies: mime-db: 1.54.0 - minimatch@10.2.6: + minimatch@10.2.5: dependencies: - brace-expansion: 5.0.9 + brace-expansion: 5.0.8 ms@2.1.3: {} - nanoid@3.3.18: {} + nanoid@3.3.17: {} natural-compare@1.4.0: {} @@ -4538,9 +4538,9 @@ snapshots: pngjs@7.0.0: {} - postcss@8.5.26: + postcss@8.5.25: dependencies: - nanoid: 3.3.18 + nanoid: 3.3.17 picocolors: 1.1.1 source-map-js: 1.2.1 @@ -4629,25 +4629,25 @@ snapshots: require-from-string@2.0.2: {} - rolldown@1.2.3: + rolldown@1.2.2: dependencies: - '@oxc-project/types': 0.143.0 + '@oxc-project/types': 0.142.0 '@rolldown/pluginutils': 1.0.1 optionalDependencies: - '@rolldown/binding-android-arm64': 1.2.3 - '@rolldown/binding-darwin-arm64': 1.2.3 - '@rolldown/binding-darwin-x64': 1.2.3 - '@rolldown/binding-freebsd-x64': 1.2.3 - '@rolldown/binding-linux-arm-gnueabihf': 1.2.3 - '@rolldown/binding-linux-arm64-gnu': 1.2.3 - '@rolldown/binding-linux-arm64-musl': 1.2.3 - '@rolldown/binding-linux-ppc64-gnu': 1.2.3 - '@rolldown/binding-linux-s390x-gnu': 1.2.3 - '@rolldown/binding-linux-x64-gnu': 1.2.3 - '@rolldown/binding-linux-x64-musl': 1.2.3 - '@rolldown/binding-openharmony-arm64': 1.2.3 - '@rolldown/binding-win32-arm64-msvc': 1.2.3 - '@rolldown/binding-win32-x64-msvc': 1.2.3 + '@rolldown/binding-android-arm64': 1.2.2 + '@rolldown/binding-darwin-arm64': 1.2.2 + '@rolldown/binding-darwin-x64': 1.2.2 + '@rolldown/binding-freebsd-x64': 1.2.2 + '@rolldown/binding-linux-arm-gnueabihf': 1.2.2 + '@rolldown/binding-linux-arm64-gnu': 1.2.2 + '@rolldown/binding-linux-arm64-musl': 1.2.2 + '@rolldown/binding-linux-ppc64-gnu': 1.2.2 + '@rolldown/binding-linux-s390x-gnu': 1.2.2 + '@rolldown/binding-linux-x64-gnu': 1.2.2 + '@rolldown/binding-linux-x64-musl': 1.2.2 + '@rolldown/binding-openharmony-arm64': 1.2.2 + '@rolldown/binding-win32-arm64-msvc': 1.2.2 + '@rolldown/binding-win32-x64-msvc': 1.2.2 router@2.2.0: dependencies: @@ -4798,7 +4798,7 @@ snapshots: tslib@2.8.1: {} - tsx@4.23.11: + tsx@4.23.1: dependencies: esbuild: 0.28.1 optionalDependencies: @@ -4814,13 +4814,13 @@ snapshots: media-typer: 1.1.1 mime-types: 3.0.2 - typescript-eslint@8.66.0(eslint@10.8.1(jiti@2.7.0))(typescript@6.0.3): + typescript-eslint@8.65.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.66.0(@typescript-eslint/parser@8.66.0(eslint@10.8.1(jiti@2.7.0))(typescript@6.0.3))(eslint@10.8.1(jiti@2.7.0))(typescript@6.0.3) - '@typescript-eslint/parser': 8.66.0(eslint@10.8.1(jiti@2.7.0))(typescript@6.0.3) - '@typescript-eslint/typescript-estree': 8.66.0(typescript@6.0.3) - '@typescript-eslint/utils': 8.66.0(eslint@10.8.1(jiti@2.7.0))(typescript@6.0.3) - eslint: 10.8.1(jiti@2.7.0) + '@typescript-eslint/eslint-plugin': 8.65.0(@typescript-eslint/parser@8.65.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/parser': 8.65.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/typescript-estree': 8.65.0(typescript@6.0.3) + '@typescript-eslint/utils': 8.65.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3) + eslint: 10.8.0(jiti@2.7.0) typescript: 6.0.3 transitivePeerDependencies: - supports-color @@ -4849,24 +4849,24 @@ snapshots: vary@1.1.2: {} - vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.11): + vite@8.2.0(@types/node@26.1.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1): dependencies: lightningcss: 1.33.0 picomatch: 4.0.5 - postcss: 8.5.26 - rolldown: 1.2.3 + postcss: 8.5.25 + rolldown: 1.2.2 tinyglobby: 0.2.17 optionalDependencies: - '@types/node': 26.2.0 + '@types/node': 26.1.2 esbuild: 0.28.1 fsevents: 2.3.3 jiti: 2.7.0 - tsx: 4.23.11 + tsx: 4.23.1 - vitest@4.1.10(@types/node@26.2.0)(@vitest/coverage-v8@4.1.10)(jsdom@30.0.1)(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.11)): + vitest@4.1.10(@types/node@26.1.2)(@vitest/coverage-v8@4.1.10)(jsdom@30.0.1)(vite@8.2.0(@types/node@26.1.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)): dependencies: '@vitest/expect': 4.1.10 - '@vitest/mocker': 4.1.10(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.11)) + '@vitest/mocker': 4.1.10(vite@8.2.0(@types/node@26.1.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)) '@vitest/pretty-format': 4.1.10 '@vitest/runner': 4.1.10 '@vitest/snapshot': 4.1.10 @@ -4883,10 +4883,10 @@ snapshots: tinyexec: 1.2.4 tinyglobby: 0.2.17 tinyrainbow: 3.1.0 - vite: 8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.11) + vite: 8.2.0(@types/node@26.1.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1) why-is-node-running: 2.3.0 optionalDependencies: - '@types/node': 26.2.0 + '@types/node': 26.1.2 '@vitest/coverage-v8': 4.1.10(vitest@4.1.10) jsdom: 30.0.1 transitivePeerDependencies: diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 241bbb33..9b166ef2 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -261,9 +261,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64" -version = "0.23.1" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" +checksum = "b25655df2c3cdd83c5e5b293b88acd880332b2ddadd7c30ac43144fdc0033da9" [[package]] name = "bit-set" @@ -464,7 +464,7 @@ dependencies = [ "semver", "serde", "serde_json", - "thiserror 2.0.20", + "thiserror 2.0.19", ] [[package]] @@ -2128,7 +2128,7 @@ dependencies = [ "jni-sys 0.4.1", "log", "simd_cesu8", - "thiserror 2.0.20", + "thiserror 2.0.19", "walkdir", "windows-link 0.2.1", ] @@ -2430,7 +2430,7 @@ dependencies = [ "once_cell", "png 0.18.1", "serde", - "thiserror 2.0.20", + "thiserror 2.0.19", "windows-sys 0.61.2", ] @@ -2503,9 +2503,9 @@ dependencies = [ [[package]] name = "nusb" -version = "0.2.7" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18ef13beb3b3a8fc16fd7aea912ebd3d45dde00a9a5b968d0742297468065845" +checksum = "36a27f8bd2cea8b09029d2d3fa51e7aed1040c76e35a9a77de38d587849b0179" dependencies = [ "core-foundation 0.10.1", "core-foundation-sys", @@ -2782,7 +2782,7 @@ dependencies = [ "objc2-osa-kit", "serde", "serde_json", - "thiserror 2.0.20", + "thiserror 2.0.19", ] [[package]] @@ -3090,7 +3090,7 @@ dependencies = [ "rustc-hash", "rustls", "socket2", - "thiserror 2.0.20", + "thiserror 2.0.19", "tokio", "tracing", "web-time", @@ -3112,7 +3112,7 @@ dependencies = [ "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.20", + "thiserror 2.0.19", "tinyvec", "tracing", "web-time", @@ -3202,7 +3202,7 @@ checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ "getrandom 0.2.17", "libredox", - "thiserror 2.0.20", + "thiserror 2.0.19", ] [[package]] @@ -3993,7 +3993,7 @@ dependencies = [ "serde_json", "sha2 0.10.9", "smallvec", - "thiserror 2.0.20", + "thiserror 2.0.19", "tokio", "tokio-stream", "tracing", @@ -4061,7 +4061,7 @@ dependencies = [ "sha1", "sha2 0.11.0", "sqlx-core", - "thiserror 2.0.20", + "thiserror 2.0.19", "tracing", ] @@ -4095,7 +4095,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror 2.0.20", + "thiserror 2.0.19", "tracing", "whoami", ] @@ -4119,7 +4119,7 @@ dependencies = [ "percent-encoding", "serde", "sqlx-core", - "thiserror 2.0.20", + "thiserror 2.0.19", "tracing", "url", ] @@ -4362,7 +4362,7 @@ dependencies = [ "tauri-runtime", "tauri-runtime-wry", "tauri-utils", - "thiserror 2.0.20", + "thiserror 2.0.19", "tokio", "tray-icon", "url", @@ -4413,7 +4413,7 @@ dependencies = [ "sha2 0.10.9", "syn 2.0.118", "tauri-utils", - "thiserror 2.0.20", + "thiserror 2.0.19", "time", "url", "uuid", @@ -4464,7 +4464,7 @@ dependencies = [ "tauri", "tauri-plugin", "tauri-plugin-fs", - "thiserror 2.0.20", + "thiserror 2.0.19", "url", ] @@ -4487,7 +4487,7 @@ dependencies = [ "tauri", "tauri-plugin", "tauri-utils", - "thiserror 2.0.20", + "thiserror 2.0.19", "toml 1.1.2+spec-1.1.0", "url", ] @@ -4508,7 +4508,7 @@ dependencies = [ "serde_json", "tauri", "tauri-plugin", - "thiserror 2.0.20", + "thiserror 2.0.19", "url", "windows", "zbus", @@ -4549,7 +4549,7 @@ dependencies = [ "tauri", "tauri-plugin", "tempfile", - "thiserror 2.0.20", + "thiserror 2.0.19", "time", "tokio", "url", @@ -4569,7 +4569,7 @@ dependencies = [ "serde_json", "tauri", "tauri-plugin", - "thiserror 2.0.20", + "thiserror 2.0.19", ] [[package]] @@ -4590,7 +4590,7 @@ dependencies = [ "serde", "serde_json", "tauri-utils", - "thiserror 2.0.20", + "thiserror 2.0.19", "url", "webkit2gtk", "webview2-com", @@ -4653,7 +4653,7 @@ dependencies = [ "serde_json", "serde_with", "swift-rs", - "thiserror 2.0.20", + "thiserror 2.0.19", "toml 1.1.2+spec-1.1.0", "url", "urlpattern", @@ -4705,11 +4705,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.20" +version = "2.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" dependencies = [ - "thiserror-impl 2.0.20", + "thiserror-impl 2.0.19", ] [[package]] @@ -4725,9 +4725,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.20" +version = "2.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" dependencies = [ "proc-macro2", "quote", @@ -5052,7 +5052,7 @@ dependencies = [ "once_cell", "png 0.18.1", "serde", - "thiserror 2.0.20", + "thiserror 2.0.19", "windows-sys 0.61.2", ] @@ -5479,7 +5479,7 @@ version = "0.38.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" dependencies = [ - "thiserror 2.0.20", + "thiserror 2.0.19", "windows", "windows-core 0.61.2", ] @@ -6003,7 +6003,7 @@ dependencies = [ "sha2 0.10.9", "soup3", "tao-macros", - "thiserror 2.0.20", + "thiserror 2.0.19", "url", "webkit2gtk", "webkit2gtk-sys", @@ -6257,7 +6257,7 @@ dependencies = [ name = "zplab" version = "0.3.2" dependencies = [ - "base64 0.23.1", + "base64 0.23.0", "calamine", "chrono", "dunce", @@ -6277,7 +6277,7 @@ dependencies = [ "tauri-plugin-process", "tauri-plugin-updater", "tauri-plugin-window-state", - "thiserror 2.0.20", + "thiserror 2.0.19", "tokio", "tokio-stream", "windows-sys 0.61.2", diff --git a/src/assets/fonts/NOTICE.md b/src/assets/fonts/NOTICE.md index a1ce4cb3..3af02800 100644 --- a/src/assets/fonts/NOTICE.md +++ b/src/assets/fonts/NOTICE.md @@ -15,6 +15,14 @@ Relative to upstream Roboto Condensed: - Vertical outlines scaled to align cap heights with the same target. - TrueType hinting tables (`prep`, `fpgm`, `cvt`) removed so the modified outlines render unhinted. +- GPOS (kerning, capital spacing) dropped and the GSUB `liga` feature + removed: Zebra's firmware lays out Font 0 with fixed per-glyph + advances, no kerning and no ligatures. +- All outlines shifted down ~0.04 em to match Labelary's vertical + placement; individual glyphs remapped to CG Triumvirate's measured + ink spans, ascenders of `b d k l` trimmed above the x-height and the + `: ;` top dots re-anchored, all via a local fonttools pipeline + calibrated against Labelary. - Family and PostScript names renamed from "Roboto Condensed" to "PrintLab ZPL". diff --git a/src/assets/fonts/PrintLabZPL-Bold.ttf b/src/assets/fonts/PrintLabZPL-Bold.ttf index 20766543..ada2e1b1 100644 Binary files a/src/assets/fonts/PrintLabZPL-Bold.ttf and b/src/assets/fonts/PrintLabZPL-Bold.ttf differ diff --git a/src/assets/fonts/PrintLabZPL-Bold.woff2 b/src/assets/fonts/PrintLabZPL-Bold.woff2 index 6e8b3985..b6cde60c 100644 Binary files a/src/assets/fonts/PrintLabZPL-Bold.woff2 and b/src/assets/fonts/PrintLabZPL-Bold.woff2 differ diff --git a/src/lib/labelGeometry/textPositionTransforms.test.ts b/src/lib/labelGeometry/textPositionTransforms.test.ts index 9fb729b7..f34ef446 100644 --- a/src/lib/labelGeometry/textPositionTransforms.test.ts +++ b/src/lib/labelGeometry/textPositionTransforms.test.ts @@ -174,4 +174,31 @@ describe('text position transforms — model ↔ ZPL anchor', () => { } } }); + + describe('device font anchor snap', () => { + it('off-grid ^A height anchors by the snapped cell, not the request', () => { + // ^AGN,84 snaps to mag 1 (60): same delta as an exact 60-dot request. + const offGrid = zplAnchorToModel(100, 100, { fontHeight: 84, rotation: 'N', fontId: 'G' }, 'FO'); + const exact = zplAnchorToModel(100, 100, { fontHeight: 60, rotation: 'N', fontId: 'G' }, 'FO'); + expect(offGrid.y).toBeCloseTo(exact.y); + }); + it('snapping up mirrors snapping down', () => { + // 156 rounds to mag 3 (180). + const up = zplAnchorToModel(100, 100, { fontHeight: 156, rotation: 'N', fontId: 'G' }, 'FO'); + const exact = zplAnchorToModel(100, 100, { fontHeight: 180, rotation: 'N', fontId: 'G' }, 'FO'); + expect(up.y).toBeCloseTo(exact.y); + }); + it('scalable ids keep the requested height', () => { + const dev = zplAnchorToModel(100, 100, { fontHeight: 84, rotation: 'N', fontId: '0' }, 'FO'); + const plain = zplAnchorToModel(100, 100, { fontHeight: 84, rotation: 'N' }, 'FO'); + expect(dev.y).toBeCloseTo(plain.y); + }); + it('round-trips at off-grid heights', () => { + const props = { fontHeight: 84, rotation: 'N' as const, fontId: 'G' }; + const anchor = modelToZplAnchor(123, 456, props, 'FO'); + const back = zplAnchorToModel(anchor.x, anchor.y, props, 'FO'); + expect(back.x).toBeCloseTo(123); + expect(back.y).toBeCloseTo(456); + }); + }); }); diff --git a/src/lib/labelGeometry/textRenderMetrics.test.ts b/src/lib/labelGeometry/textRenderMetrics.test.ts index da9a9bf8..9dba1a28 100644 --- a/src/lib/labelGeometry/textRenderMetrics.test.ts +++ b/src/lib/labelGeometry/textRenderMetrics.test.ts @@ -47,4 +47,30 @@ describe("getTextRenderMetrics — device font resolution", () => { expect(m!.fontSizeDots).toBeUndefined(); expect(m!.letterSpacingDots).toBeUndefined(); }); + + it("keeps the built-in device font when a manual alias row has no file yet", () => { + const m = getTextRenderMetrics( + textObj("B"), + undefined, + label({ customFonts: [{ alias: "B", path: "" }] }), + ); + expect(m!.fontSizeDots).toBeDefined(); + expect(m!.content).toBe("ABC"); + }); + + it("renders an ^A@ TTF field as its printer font, not the device default", () => { + const obj = { + ...textObj(), + props: { + content: "Abc", + fontHeight: 30, + fontWidth: 0, + rotation: "N", + printerFontName: "ARIAL.TTF", + }, + } as unknown as LabelObject; + const m = getTextRenderMetrics(obj, undefined, label({ defaultFontId: "B" })); + expect(m!.fontSizeDots).toBeUndefined(); + expect(m!.content).toBe("Abc"); + }); }); diff --git a/src/test/deviceFontBoxMatch.test.ts b/src/test/deviceFontBoxMatch.test.ts new file mode 100644 index 00000000..d2595853 --- /dev/null +++ b/src/test/deviceFontBoxMatch.test.ts @@ -0,0 +1,109 @@ +import { describe, it, expect, beforeAll } from 'vitest'; +import * as fs from 'fs'; +import * as path from 'path'; +import { GlobalFonts } from '@napi-rs/canvas'; +import { PNG } from 'pngjs'; +import { deviceFontMetrics } from '@zplab/core/lib/labelGeometry/deviceFonts'; +import { zplAnchorToModel } from '@zplab/core/lib/labelGeometry/textPositionTransforms'; +import { builtinFontFamily } from '@zplab/core/lib/customFonts'; +import { deviceFontBoxMatchCases } from '../../tests/fixtures/deviceFontBoxMatchCases'; +import { darkBBox } from '../../tests/lib/darkBBox'; +import { drawKonvaText } from '../../tests/lib/drawKonvaText'; +import { inkCanvas } from '../../tests/lib/inkCanvas'; +import { expectBoxMatch } from '../../tests/lib/boxMatch'; + +/** + * Box-match regression for the bitmap device fonts (^AA-^AH) through the + * production metrics and placement chain; position is what validates the + * yOffEm/xOffEm calibration. Bboxes, not pixels, for the same reason as + * textBoxMatch: the substitute faces differ in glyph design. + */ + +const FIXTURES_DIR = path.resolve( + process.cwd(), + 'tests/fixtures/labelary_devicefont_images', +); + +const FONT_DIR = path.resolve(process.cwd(), 'src/assets/fonts'); + +/** Bundled TTF per CSS family name; the face choice itself comes from + * builtinFontFamily so a production face swap fails here loudly instead + * of silently testing the old font. */ +const FONT_FILES: Record = { + 'PrintLab Mono': 'PrintLabMono.ttf', + 'Vera Mono': 'VeraMono.ttf', + 'Vera Mono Bold': 'VeraMono-Bold.ttf', + OCRB: 'OCRB.ttf', + OCRA: 'OCRA.ttf', +}; + +function faceFor(fontId: string): { family: string; file: string } { + const family = builtinFontFamily(fontId) + ?.split(',')[0] + ?.replace(/'/g, '') + .trim(); + const file = family ? FONT_FILES[family] : undefined; + if (!family || !file) throw new Error(`no bundled face for font ${fontId}`); + return { family, file }; +} + +const FONT_IDS = [...new Set(deviceFontBoxMatchCases.map((tc) => tc.fontId))]; + +describe('Device font box-match: substitutes vs. Labelary bitmap fonts', () => { + beforeAll(() => { + for (const fontId of FONT_IDS) { + const face = faceFor(fontId); + const p = path.join(FONT_DIR, face.file); + if (!fs.existsSync(p)) throw new Error(`Font file missing at ${p}`); + GlobalFonts.registerFromPath(p, face.family); + } + }); + + it('has device-font fixtures (run fetch_labelary_default_text_fixtures.ts if this fails)', () => { + expect(deviceFontBoxMatchCases.length).toBeGreaterThan(0); + for (const tc of deviceFontBoxMatchCases) { + const fixture = path.join(FIXTURES_DIR, `${tc.id}.png`); + expect(fs.existsSync(fixture), `Missing fixture ${tc.id}.png`).toBe(true); + } + }); + + describe.each(deviceFontBoxMatchCases)('Box: $id', (tc) => { + it('bbox matches Labelary within tolerance', () => { + const metrics = deviceFontMetrics(tc.fontId, tc.fontHeight, tc.fontWidth); + expect(metrics, `no metrics for font ${tc.fontId}`).not.toBeNull(); + if (!metrics) return; + const face = faceFor(tc.fontId); + + const model = zplAnchorToModel( + tc.x, + tc.y, + { fontHeight: tc.fontHeight, rotation: tc.rotation, fontId: tc.fontId }, + 'FO', + ); + const drawX = model.x + metrics.xOffsetDots; + const drawY = model.y + metrics.yOffsetDots; + + const { canvas, ctx } = inkCanvas(); + drawKonvaText(ctx, { + text: tc.text, + x: drawX, + y: drawY, + fontSizePx: metrics.fontSizeDots, + fontFamily: face.family, + scaleX: metrics.scaleX, + letterSpacingPx: metrics.letterSpacingDots, + }); + + const localPng = PNG.sync.read(canvas.toBuffer('image/png')); + const labelaryPng = PNG.sync.read( + fs.readFileSync(path.join(FIXTURES_DIR, `${tc.id}.png`)), + ); + + const localBox = darkBBox(localPng); + const labelaryBox = darkBBox(labelaryPng); + + // Floor 2: device substitutes land within +-2 dots once calibrated. + expectBoxMatch(localBox, labelaryBox, 2, tc.id); + }); + }); +}); diff --git a/src/test/fixtureIdContract.test.ts b/src/test/fixtureIdContract.test.ts new file mode 100644 index 00000000..5eb75421 --- /dev/null +++ b/src/test/fixtureIdContract.test.ts @@ -0,0 +1,32 @@ +import { describe, it, expect } from 'vitest'; +import { textBoxMatchCases } from '../../tests/fixtures/textBoxMatchCases'; +import { font0GlyphCoverageCases } from '../../tests/fixtures/font0GlyphCoverageCases'; + +/** The coverage list deliberately shares ids with the box-match set so + * both read the same fixture PNG; that only stays correct while equal + * ids imply equal render parameters. The fetch script keeps the first + * job per id, so a silent divergence would compare one list's render + * against the other list's fixture. */ +describe('fixture id contract', () => { + it('cases sharing an id share their render parameters', () => { + const byId = new Map(); + for (const tc of [...textBoxMatchCases, ...font0GlyphCoverageCases]) { + // The fetch ZPL emits fontWidth || fontHeight; 0 and an explicit + // width equal to the height are the same fixture. + const params = JSON.stringify([ + tc.text, + tc.fontHeight, + tc.fontWidth || tc.fontHeight, + tc.rotation, + tc.x, + tc.y, + ]); + const prev = byId.get(tc.id); + if (prev !== undefined) { + expect(params, `diverging params for shared id ${tc.id}`).toBe(prev); + } else { + byId.set(tc.id, params); + } + } + }); +}); diff --git a/src/test/font0PlacementMatch.test.ts b/src/test/font0PlacementMatch.test.ts new file mode 100644 index 00000000..6df50841 --- /dev/null +++ b/src/test/font0PlacementMatch.test.ts @@ -0,0 +1,69 @@ +import { describe, it, expect, beforeAll } from 'vitest'; +import * as fs from 'fs'; +import * as path from 'path'; +import { PNG } from 'pngjs'; +import { zplAnchorToModel } from '@zplab/core/lib/labelGeometry/textPositionTransforms'; +import { charSlug } from '../../tests/fixtures/charSlug'; +import { darkBBox } from '../../tests/lib/darkBBox'; +import { drawKonvaText } from '../../tests/lib/drawKonvaText'; +import { inkCanvas } from '../../tests/lib/inkCanvas'; +import { expectBoxMatch } from '../../tests/lib/boxMatch'; +import { + PRINTLAB_FONT_FAMILY, + registerPrintLabZpl, +} from '../../tests/lib/printLabFont'; + +/** + * Font-0 placement gate: size AND position of the calibrated glyphs vs + * Labelary, through the production chain (^FO anchor -> zplAnchorToModel + * -> Konva placement). Guards the font-patch results that the drift + * report only measures; a rebuilt TTF without the patch pipeline turns + * this red. + */ + +const FIXTURES_DIR = path.resolve( + process.cwd(), + 'tests/fixtures/labelary_text_default_images', +); +const H = 50; + +// Representative subset of the font build's calibrated glyphs; '/' +// also guards the kerning strip ('///' collapses if GSUB/GPOS come +// back); 'b' guards the global vertical shift; '5' and 'M' pin the +// chain. +const GLYPHS = ['_', '<', '>', '@', '*', '$', '(', ')', '/', 'f', 'j', 'Q', 'p', 'b', '5', 'M']; + +describe('Font 0 placement match: patched glyphs vs Labelary', () => { + beforeAll(() => { + registerPrintLabZpl(); + }); + + describe.each(GLYPHS)('Glyph: %s', (glyph) => { + it('bbox and position match Labelary within tolerance', () => { + const id = `h${H}_${charSlug(glyph)}`; + const fixture = path.join(FIXTURES_DIR, `${id}.png`); + expect( + fs.existsSync(fixture), + `Missing fixture ${id}.png (run fetch_labelary_default_text_fixtures.ts)`, + ).toBe(true); + + const { canvas, ctx } = inkCanvas(); + const model = zplAnchorToModel(200, 200, { fontHeight: H, rotation: 'N' }, 'FO'); + drawKonvaText(ctx, { + text: glyph.repeat(3), + x: model.x, + y: model.y, + fontSizePx: H, + fontFamily: PRINTLAB_FONT_FAMILY, + fontStyle: 'bold', + }); + + const localBox = darkBBox(PNG.sync.read(canvas.toBuffer('image/png'))); + const labelaryBox = darkBBox(PNG.sync.read(fs.readFileSync(fixture))); + + // Floor 3: sub-10-dot punctuation boxes carry +-3 dots of AA and + // rounding noise; the regressions this gate exists for measure 4+. + expectBoxMatch(localBox, labelaryBox, 3, id); + }); + }); +}); diff --git a/src/test/textBoxMatch.test.ts b/src/test/textBoxMatch.test.ts index 84cc36da..191c7ec3 100644 --- a/src/test/textBoxMatch.test.ts +++ b/src/test/textBoxMatch.test.ts @@ -1,9 +1,16 @@ import { describe, it, expect, beforeAll } from 'vitest'; import * as fs from 'fs'; import * as path from 'path'; -import { createCanvas, GlobalFonts } from '@napi-rs/canvas'; + import { PNG } from 'pngjs'; import { textBoxMatchCases } from '../../tests/fixtures/textBoxMatchCases'; +import { darkBBox } from '../../tests/lib/darkBBox'; +import { drawKonvaText } from '../../tests/lib/drawKonvaText'; +import { inkCanvas } from '../../tests/lib/inkCanvas'; +import { + PRINTLAB_FONT_FAMILY, + registerPrintLabZpl, +} from '../../tests/lib/printLabFont'; /** * Box-match regression. @@ -31,62 +38,16 @@ const FIXTURES_DIR = path.resolve( process.cwd(), 'tests/fixtures/labelary_text_default_images', ); -const FONT_PATH = path.resolve( - process.cwd(), - 'src/assets/fonts/PrintLabZPL-Bold.ttf', -); - -const CANVAS_PX = 812; -const FONT_FAMILY = 'PrintLab ZPL'; - -/** Per-axis tolerance in dots. Cap-bound and digit content lands at - * ≤3 dots once the font's per-glyph advance and vertical scaling are - * applied. The 6-dot allowance covers ascender / descender outliers - * where Roboto's inherent vertical proportions disagree with - * CG Triumvirate's by a few dots, visible only for `i` (dot above - * x-height), `p` (descender) and `.` (baseline edge). A real layout - * regression (wrong advance class, wrong fontSize) produces drift - * >12 dots even at small sizes, well above this ceiling. */ +/** Per-axis size tolerance across the 20-80 sweep: calibrated content + * lands at <=3 dots, 6 leaves AA/rounding headroom, and a real layout + * regression (wrong advance class, wrong fontSize) produces >12 even + * at small sizes. */ const BBOX_TOLERANCE_DOTS = 6; -interface BBox { - width: number; - height: number; -} - -/** Scan a PNG for the smallest rectangle that contains all darkish - * pixels. Returns extent in pixels (= dots on our 8 dpmm canvas). */ -function darkBBox(png: PNG): BBox { - const { width, height, data } = png; - let minX = width; - let minY = height; - let maxX = -1; - let maxY = -1; - for (let y = 0; y < height; y++) { - for (let x = 0; x < width; x++) { - const idx = (y * width + x) * 4; - const lum = - 0.299 * (data[idx] ?? 0) + - 0.587 * (data[idx + 1] ?? 0) + - 0.114 * (data[idx + 2] ?? 0); - if (lum < 200) { - if (x < minX) minX = x; - if (y < minY) minY = y; - if (x > maxX) maxX = x; - if (y > maxY) maxY = y; - } - } - } - if (maxX < 0) return { width: 0, height: 0 }; - return { width: maxX - minX + 1, height: maxY - minY + 1 }; -} describe('Text Box-Match — PrintLab ZPL vs. Labelary default font', () => { beforeAll(() => { - if (!fs.existsSync(FONT_PATH)) { - throw new Error(`Font file missing at ${FONT_PATH}`); - } - GlobalFonts.registerFromPath(FONT_PATH, FONT_FAMILY); + registerPrintLabZpl(); }); it('has default-font fixtures (run fetch_labelary_default_text_fixtures.ts if this fails)', () => { @@ -99,22 +60,18 @@ describe('Text Box-Match — PrintLab ZPL vs. Labelary default font', () => { describe.each(textBoxMatchCases)('Box: $id', (tc) => { it(`bbox matches Labelary default within ±${BBOX_TOLERANCE_DOTS} dots`, () => { - // Render our font locally exactly as production does. - const canvas = createCanvas(CANVAS_PX, CANVAS_PX); - const ctx = canvas.getContext('2d'); - ctx.fillStyle = 'white'; - ctx.fillRect(0, 0, CANVAS_PX, CANVAS_PX); - ctx.fillStyle = 'black'; - ctx.font = `bold ${tc.fontHeight}px "${FONT_FAMILY}"`; - ctx.textBaseline = 'top'; - // Mirror Konva's scaleX: ZPL `^A0,h,w` with w != h stretches each - // glyph horizontally; w = 0 is the Zebra shorthand for "match h". - const scaleX = tc.fontWidth > 0 ? tc.fontWidth / tc.fontHeight : 1; - ctx.save(); - ctx.translate(tc.x, tc.y); - ctx.scale(scaleX, 1); - ctx.fillText(tc.text, 0, 0); - ctx.restore(); + // ZPL `^A0,h,w` with w != h stretches each glyph; w = 0 means + // "match h". + const { canvas, ctx } = inkCanvas(); + drawKonvaText(ctx, { + text: tc.text, + x: tc.x, + y: tc.y, + fontSizePx: tc.fontHeight, + fontFamily: PRINTLAB_FONT_FAMILY, + fontStyle: 'bold', + scaleX: tc.fontWidth > 0 ? tc.fontWidth / tc.fontHeight : 1, + }); const localPng = PNG.sync.read(canvas.toBuffer('image/png')); const labelaryPng = PNG.sync.read( diff --git a/src/test/textVisualRegression.test.ts b/src/test/textVisualRegression.test.ts index f98e7ca4..5a90433b 100644 --- a/src/test/textVisualRegression.test.ts +++ b/src/test/textVisualRegression.test.ts @@ -1,9 +1,13 @@ import { describe, it, expect, beforeAll } from 'vitest'; import * as fs from 'fs'; import * as path from 'path'; -import { createCanvas, GlobalFonts } from '@napi-rs/canvas'; import pixelmatch from 'pixelmatch'; import { PNG } from 'pngjs'; +import { inkCanvas, INK_CANVAS_PX } from '../../tests/lib/inkCanvas'; +import { + PRINTLAB_FONT_FAMILY, + registerPrintLabZpl, +} from '../../tests/lib/printLabFont'; import { textTestCases } from '../../tests/fixtures/textTestCases'; /** @@ -31,10 +35,6 @@ const FIXTURES_DIR = path.resolve( 'tests/fixtures/labelary_text_images', ); const DIFF_DIR = path.resolve(process.cwd(), 'tests/fixtures/__text_diffs__'); -const FONT_PATH = path.resolve(process.cwd(), 'src/assets/fonts/PrintLabZPL-Bold.ttf'); - -const CANVAS_PX = 812; // 4x4 inch at 8 dpmm, matches the fixture canvas. -const FONT_FAMILY = 'PrintLab ZPL'; /** Diff allowance. With both sides loading the same TTF the remaining * delta is anti-aliasing edge variance between the renderers, sized @@ -52,10 +52,7 @@ if (!fs.existsSync(DIFF_DIR)) { describe('Text renderer-pair stability (Skia vs Labelary, shared TTF)', () => { beforeAll(() => { - if (!fs.existsSync(FONT_PATH)) { - throw new Error(`Font file missing at ${FONT_PATH}.`); - } - GlobalFonts.registerFromPath(FONT_PATH, FONT_FAMILY); + registerPrintLabZpl(); }); it('has fixtures (run fetch_labelary_text_fixtures.ts if this fails)', () => { @@ -71,32 +68,27 @@ describe('Text renderer-pair stability (Skia vs Labelary, shared TTF)', () => { describe.each(textTestCases)('Text: $id', (tc) => { it('matches Labelary within anti-aliasing tolerance', () => { - // Local render: PrintLab ZPL at fontSize = fontHeight, anchored - // at the same FO origin as the fixture. Production sets - // `textBaseline = 'top'` implicitly via Konva.Text's anchor model, - // so we mirror that here. - const canvas = createCanvas(CANVAS_PX, CANVAS_PX); - const ctx = canvas.getContext('2d'); - ctx.fillStyle = 'white'; - ctx.fillRect(0, 0, CANVAS_PX, CANVAS_PX); - ctx.fillStyle = 'black'; - ctx.font = `bold ${tc.fontHeight}px "${FONT_FAMILY}"`; + // Deliberately NOT drawKonvaText: this is a pixel diff against + // fixtures rendered at textBaseline='top'; the shared helper's + // alphabetic-baseline offset would shift the render off them. + const { canvas, ctx } = inkCanvas(); + ctx.font = `bold ${tc.fontHeight}px "${PRINTLAB_FONT_FAMILY}"`; ctx.textBaseline = 'top'; ctx.fillText(tc.text, tc.x, tc.y); const localPng = PNG.sync.read(canvas.toBuffer('image/png')); const fixtureBuf = fs.readFileSync(path.join(FIXTURES_DIR, `${tc.id}.png`)); const fixturePng = PNG.sync.read(fixtureBuf); - expect(fixturePng.width).toBe(CANVAS_PX); - expect(fixturePng.height).toBe(CANVAS_PX); + expect(fixturePng.width).toBe(INK_CANVAS_PX); + expect(fixturePng.height).toBe(INK_CANVAS_PX); - const diff = new PNG({ width: CANVAS_PX, height: CANVAS_PX }); + const diff = new PNG({ width: INK_CANVAS_PX, height: INK_CANVAS_PX }); const diffCount = pixelmatch( fixturePng.data, localPng.data, diff.data, - CANVAS_PX, - CANVAS_PX, + INK_CANVAS_PX, + INK_CANVAS_PX, { threshold: 0.1 }, ); diff --git a/tests/fixtures/charSlug.ts b/tests/fixtures/charSlug.ts new file mode 100644 index 00000000..4bcd8ed0 --- /dev/null +++ b/tests/fixtures/charSlug.ts @@ -0,0 +1,34 @@ +/** Case-insensitive filesystems would collide `A` and `a`, so + * lowercase gets an `lc` prefix. */ +export const PUNCT_SLUGS: Record = { + '-': 'minus', + '.': 'dot', + '=': 'eq', + ',': 'comma', + ':': 'colon', + ';': 'semi', + '/': 'slash', + '+': 'plus', + '*': 'star', + '#': 'hash', + '&': 'amp', + '%': 'pct', + '$': 'dollar', + '(': 'lparen', + ')': 'rparen', + '!': 'bang', + '?': 'qmark', + '@': 'at', + '_': 'under', + '"': 'dquote', + "'": 'squote', + '<': 'lt', + '>': 'gt', +}; + +export function charSlug(c: string): string { + const punct = PUNCT_SLUGS[c]; + if (punct) return punct; + if (c >= 'a' && c <= 'z') return `lc${c.toUpperCase()}`; + return c; +} diff --git a/tests/fixtures/deviceFontBoxMatchCases.ts b/tests/fixtures/deviceFontBoxMatchCases.ts new file mode 100644 index 00000000..de0fece7 --- /dev/null +++ b/tests/fixtures/deviceFontBoxMatchCases.ts @@ -0,0 +1,96 @@ +/** Heights are exact cell multiples (mag 1-3) plus one off-multiple + * case per font that must snap to mag 1, proving our rounding agrees + * with the firmware's. */ +import { DEVICE_FONT_CELLS } from '@zplab/core/lib/labelGeometry/deviceFonts'; +import { charSlug } from './charSlug'; + +export interface DeviceFontBoxMatchCase { + id: string; + fontId: string; + fontHeight: number; + /** Width parameter of `^A{id},h,w`. 0 = derive from height. */ + fontWidth: number; + text: string; + rotation: 'N'; + x: number; + y: number; +} + +interface FontPlan { + fontId: string; + magStep: number; + magWidthStep: number; + chars: string[]; +} + +// Chars per font: a digit, a wide cap, and where the font has +// lowercase a descender ('p') for baseline reach. B is caps-only and +// H (OCR-A) drops lowercase, so they test 'W' instead. +const ULD_CHARS = ['5', 'M', 'p']; +const CAPS_CHARS = ['5', 'M', 'W']; + +const FONT_PLANS: FontPlan[] = Object.entries(DEVICE_FONT_CELLS).map( + ([fontId, cell]) => ({ + fontId, + magStep: cell.magStep, + magWidthStep: cell.magWidthStep, + chars: fontId === 'B' || fontId === 'H' ? CAPS_CHARS : ULD_CHARS, + }), +); + +const MAGS = [1, 2, 3]; + +const magCases: DeviceFontBoxMatchCase[] = FONT_PLANS.flatMap((plan) => + MAGS.flatMap((mag) => + plan.chars.map((c) => ({ + id: `f${plan.fontId}_m${mag}_${charSlug(c)}`, + fontId: plan.fontId, + fontHeight: plan.magStep * mag, + fontWidth: 0, + text: c.repeat(3), + rotation: 'N' as const, + x: 100, + y: 100, + })), + ), +); + +/** Off-multiple heights: 1.4x the cell must round down to mag 1 on + * both sides. */ +const snapCases: DeviceFontBoxMatchCase[] = FONT_PLANS.map((plan) => ({ + id: `f${plan.fontId}_snap`, + fontId: plan.fontId, + fontHeight: Math.round(plan.magStep * 1.4), + fontWidth: 0, + text: '505', + rotation: 'N' as const, + x: 100, + y: 100, +})); + +/** Width sweeps: explicit `^A,h,w` with w decoupled from h exercises + * the magW path (untested by the w=0 cases above), plus one high-mag + * case. Two chars and a tighter origin keep font G inside the 812-dot + * label at mag 5. */ +const sweepCases: DeviceFontBoxMatchCase[] = FONT_PLANS.flatMap((plan) => + [ + { h: 3, w: 1, slug: 'w1' }, + { h: 3, w: 5, slug: 'w5' }, + { h: 5, w: 0, slug: 'm5' }, + ].map(({ h, w, slug }) => ({ + id: `f${plan.fontId}_${slug}`, + fontId: plan.fontId, + fontHeight: plan.magStep * h, + fontWidth: w > 0 ? plan.magWidthStep * w : 0, + text: 'M5', + rotation: 'N' as const, + x: 20, + y: 20, + })), +); + +export const deviceFontBoxMatchCases: DeviceFontBoxMatchCase[] = [ + ...magCases, + ...snapCases, + ...sweepCases, +]; diff --git a/tests/fixtures/font0GlyphCoverageCases.ts b/tests/fixtures/font0GlyphCoverageCases.ts new file mode 100644 index 00000000..a05008ac --- /dev/null +++ b/tests/fixtures/font0GlyphCoverageCases.ts @@ -0,0 +1,26 @@ +/** Measurement input for the per-glyph drift report, deliberately NOT + * wired into the strict box-match test: measurement precedes gating. + * Ids reuse textBoxMatchCases' scheme (see fixtureIdContract). */ +import type { TextBoxMatchCase } from './textBoxMatchCases'; +import { PUNCT_SLUGS, charSlug } from './charSlug'; + +const ALL_CHARS = [ + ...'ABCDEFGHIJKLMNOPQRSTUVWXYZ', + ...'abcdefghijklmnopqrstuvwxyz', + ...'0123456789', + ...Object.keys(PUNCT_SLUGS), +]; + +const SIZE = 50; + +export const font0GlyphCoverageCases: TextBoxMatchCase[] = ALL_CHARS.map( + (c) => ({ + id: `h${SIZE}_${charSlug(c)}`, + fontHeight: SIZE, + fontWidth: 0, + text: c.repeat(3), + rotation: 'N' as const, + x: 200, + y: 200, + }), +); diff --git a/tests/fixtures/labelary_devicefont_images/fA_m1_5.png b/tests/fixtures/labelary_devicefont_images/fA_m1_5.png new file mode 100644 index 00000000..4c97cc2d Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fA_m1_5.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fA_m1_M.png b/tests/fixtures/labelary_devicefont_images/fA_m1_M.png new file mode 100644 index 00000000..f5af9491 Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fA_m1_M.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fA_m1_lcP.png b/tests/fixtures/labelary_devicefont_images/fA_m1_lcP.png new file mode 100644 index 00000000..fa7eb199 Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fA_m1_lcP.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fA_m2_5.png b/tests/fixtures/labelary_devicefont_images/fA_m2_5.png new file mode 100644 index 00000000..cb4ebf4e Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fA_m2_5.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fA_m2_M.png b/tests/fixtures/labelary_devicefont_images/fA_m2_M.png new file mode 100644 index 00000000..3b7dee68 Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fA_m2_M.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fA_m2_lcP.png b/tests/fixtures/labelary_devicefont_images/fA_m2_lcP.png new file mode 100644 index 00000000..17912fe1 Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fA_m2_lcP.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fA_m3_5.png b/tests/fixtures/labelary_devicefont_images/fA_m3_5.png new file mode 100644 index 00000000..5ad0d835 Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fA_m3_5.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fA_m3_M.png b/tests/fixtures/labelary_devicefont_images/fA_m3_M.png new file mode 100644 index 00000000..110234ad Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fA_m3_M.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fA_m3_lcP.png b/tests/fixtures/labelary_devicefont_images/fA_m3_lcP.png new file mode 100644 index 00000000..8a371232 Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fA_m3_lcP.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fA_m5.png b/tests/fixtures/labelary_devicefont_images/fA_m5.png new file mode 100644 index 00000000..014c8b9a Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fA_m5.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fA_snap.png b/tests/fixtures/labelary_devicefont_images/fA_snap.png new file mode 100644 index 00000000..2383e489 Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fA_snap.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fA_w1.png b/tests/fixtures/labelary_devicefont_images/fA_w1.png new file mode 100644 index 00000000..c606cd82 Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fA_w1.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fA_w5.png b/tests/fixtures/labelary_devicefont_images/fA_w5.png new file mode 100644 index 00000000..a34f931b Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fA_w5.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fB_m1_5.png b/tests/fixtures/labelary_devicefont_images/fB_m1_5.png new file mode 100644 index 00000000..2ada4123 Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fB_m1_5.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fB_m1_M.png b/tests/fixtures/labelary_devicefont_images/fB_m1_M.png new file mode 100644 index 00000000..9057c25f Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fB_m1_M.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fB_m1_W.png b/tests/fixtures/labelary_devicefont_images/fB_m1_W.png new file mode 100644 index 00000000..599bc4de Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fB_m1_W.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fB_m2_5.png b/tests/fixtures/labelary_devicefont_images/fB_m2_5.png new file mode 100644 index 00000000..01ae4b09 Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fB_m2_5.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fB_m2_M.png b/tests/fixtures/labelary_devicefont_images/fB_m2_M.png new file mode 100644 index 00000000..d56a969b Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fB_m2_M.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fB_m2_W.png b/tests/fixtures/labelary_devicefont_images/fB_m2_W.png new file mode 100644 index 00000000..531832c7 Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fB_m2_W.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fB_m3_5.png b/tests/fixtures/labelary_devicefont_images/fB_m3_5.png new file mode 100644 index 00000000..073ad54e Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fB_m3_5.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fB_m3_M.png b/tests/fixtures/labelary_devicefont_images/fB_m3_M.png new file mode 100644 index 00000000..3f664948 Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fB_m3_M.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fB_m3_W.png b/tests/fixtures/labelary_devicefont_images/fB_m3_W.png new file mode 100644 index 00000000..5ef5f9a3 Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fB_m3_W.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fB_m5.png b/tests/fixtures/labelary_devicefont_images/fB_m5.png new file mode 100644 index 00000000..c74a7a18 Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fB_m5.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fB_snap.png b/tests/fixtures/labelary_devicefont_images/fB_snap.png new file mode 100644 index 00000000..854a1740 Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fB_snap.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fB_w1.png b/tests/fixtures/labelary_devicefont_images/fB_w1.png new file mode 100644 index 00000000..5b34f65d Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fB_w1.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fB_w5.png b/tests/fixtures/labelary_devicefont_images/fB_w5.png new file mode 100644 index 00000000..6b17350a Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fB_w5.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fC_m1_5.png b/tests/fixtures/labelary_devicefont_images/fC_m1_5.png new file mode 100644 index 00000000..fa663f9e Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fC_m1_5.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fC_m1_M.png b/tests/fixtures/labelary_devicefont_images/fC_m1_M.png new file mode 100644 index 00000000..b28a5e30 Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fC_m1_M.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fC_m1_lcP.png b/tests/fixtures/labelary_devicefont_images/fC_m1_lcP.png new file mode 100644 index 00000000..ed5a81fa Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fC_m1_lcP.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fC_m2_5.png b/tests/fixtures/labelary_devicefont_images/fC_m2_5.png new file mode 100644 index 00000000..d5c40977 Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fC_m2_5.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fC_m2_M.png b/tests/fixtures/labelary_devicefont_images/fC_m2_M.png new file mode 100644 index 00000000..184700f1 Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fC_m2_M.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fC_m2_lcP.png b/tests/fixtures/labelary_devicefont_images/fC_m2_lcP.png new file mode 100644 index 00000000..d457a4b4 Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fC_m2_lcP.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fC_m3_5.png b/tests/fixtures/labelary_devicefont_images/fC_m3_5.png new file mode 100644 index 00000000..d5330fce Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fC_m3_5.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fC_m3_M.png b/tests/fixtures/labelary_devicefont_images/fC_m3_M.png new file mode 100644 index 00000000..3bee870c Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fC_m3_M.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fC_m3_lcP.png b/tests/fixtures/labelary_devicefont_images/fC_m3_lcP.png new file mode 100644 index 00000000..5ed23435 Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fC_m3_lcP.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fC_m5.png b/tests/fixtures/labelary_devicefont_images/fC_m5.png new file mode 100644 index 00000000..3b8b01cf Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fC_m5.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fC_snap.png b/tests/fixtures/labelary_devicefont_images/fC_snap.png new file mode 100644 index 00000000..9859a2c8 Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fC_snap.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fC_w1.png b/tests/fixtures/labelary_devicefont_images/fC_w1.png new file mode 100644 index 00000000..63d8272f Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fC_w1.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fC_w5.png b/tests/fixtures/labelary_devicefont_images/fC_w5.png new file mode 100644 index 00000000..103c6bcf Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fC_w5.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fD_m1_5.png b/tests/fixtures/labelary_devicefont_images/fD_m1_5.png new file mode 100644 index 00000000..6d00bdd4 Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fD_m1_5.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fD_m1_M.png b/tests/fixtures/labelary_devicefont_images/fD_m1_M.png new file mode 100644 index 00000000..5414eb29 Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fD_m1_M.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fD_m1_lcP.png b/tests/fixtures/labelary_devicefont_images/fD_m1_lcP.png new file mode 100644 index 00000000..580ae252 Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fD_m1_lcP.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fD_m2_5.png b/tests/fixtures/labelary_devicefont_images/fD_m2_5.png new file mode 100644 index 00000000..a0725108 Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fD_m2_5.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fD_m2_M.png b/tests/fixtures/labelary_devicefont_images/fD_m2_M.png new file mode 100644 index 00000000..147919ff Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fD_m2_M.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fD_m2_lcP.png b/tests/fixtures/labelary_devicefont_images/fD_m2_lcP.png new file mode 100644 index 00000000..83348051 Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fD_m2_lcP.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fD_m3_5.png b/tests/fixtures/labelary_devicefont_images/fD_m3_5.png new file mode 100644 index 00000000..48143b6a Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fD_m3_5.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fD_m3_M.png b/tests/fixtures/labelary_devicefont_images/fD_m3_M.png new file mode 100644 index 00000000..0b59b4c1 Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fD_m3_M.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fD_m3_lcP.png b/tests/fixtures/labelary_devicefont_images/fD_m3_lcP.png new file mode 100644 index 00000000..1e431cce Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fD_m3_lcP.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fD_m5.png b/tests/fixtures/labelary_devicefont_images/fD_m5.png new file mode 100644 index 00000000..85309bf3 Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fD_m5.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fD_snap.png b/tests/fixtures/labelary_devicefont_images/fD_snap.png new file mode 100644 index 00000000..d3172ec0 Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fD_snap.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fD_w1.png b/tests/fixtures/labelary_devicefont_images/fD_w1.png new file mode 100644 index 00000000..89754b49 Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fD_w1.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fD_w5.png b/tests/fixtures/labelary_devicefont_images/fD_w5.png new file mode 100644 index 00000000..02727bcf Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fD_w5.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fE_m1_5.png b/tests/fixtures/labelary_devicefont_images/fE_m1_5.png new file mode 100644 index 00000000..d3d13a3c Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fE_m1_5.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fE_m1_M.png b/tests/fixtures/labelary_devicefont_images/fE_m1_M.png new file mode 100644 index 00000000..04918de3 Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fE_m1_M.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fE_m1_lcP.png b/tests/fixtures/labelary_devicefont_images/fE_m1_lcP.png new file mode 100644 index 00000000..cb48b46b Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fE_m1_lcP.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fE_m2_5.png b/tests/fixtures/labelary_devicefont_images/fE_m2_5.png new file mode 100644 index 00000000..1427dc60 Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fE_m2_5.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fE_m2_M.png b/tests/fixtures/labelary_devicefont_images/fE_m2_M.png new file mode 100644 index 00000000..1ec72f6b Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fE_m2_M.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fE_m2_lcP.png b/tests/fixtures/labelary_devicefont_images/fE_m2_lcP.png new file mode 100644 index 00000000..aba3549e Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fE_m2_lcP.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fE_m3_5.png b/tests/fixtures/labelary_devicefont_images/fE_m3_5.png new file mode 100644 index 00000000..32beafa6 Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fE_m3_5.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fE_m3_M.png b/tests/fixtures/labelary_devicefont_images/fE_m3_M.png new file mode 100644 index 00000000..dfd119f7 Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fE_m3_M.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fE_m3_lcP.png b/tests/fixtures/labelary_devicefont_images/fE_m3_lcP.png new file mode 100644 index 00000000..115d0e31 Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fE_m3_lcP.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fE_m5.png b/tests/fixtures/labelary_devicefont_images/fE_m5.png new file mode 100644 index 00000000..e6cf6792 Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fE_m5.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fE_snap.png b/tests/fixtures/labelary_devicefont_images/fE_snap.png new file mode 100644 index 00000000..bfde2279 Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fE_snap.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fE_w1.png b/tests/fixtures/labelary_devicefont_images/fE_w1.png new file mode 100644 index 00000000..45e21bba Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fE_w1.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fE_w5.png b/tests/fixtures/labelary_devicefont_images/fE_w5.png new file mode 100644 index 00000000..5c0ec84a Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fE_w5.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fF_m1_5.png b/tests/fixtures/labelary_devicefont_images/fF_m1_5.png new file mode 100644 index 00000000..59bf7dcf Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fF_m1_5.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fF_m1_M.png b/tests/fixtures/labelary_devicefont_images/fF_m1_M.png new file mode 100644 index 00000000..9c698c78 Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fF_m1_M.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fF_m1_lcP.png b/tests/fixtures/labelary_devicefont_images/fF_m1_lcP.png new file mode 100644 index 00000000..2c889500 Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fF_m1_lcP.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fF_m2_5.png b/tests/fixtures/labelary_devicefont_images/fF_m2_5.png new file mode 100644 index 00000000..459c14d7 Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fF_m2_5.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fF_m2_M.png b/tests/fixtures/labelary_devicefont_images/fF_m2_M.png new file mode 100644 index 00000000..6c3820bf Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fF_m2_M.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fF_m2_lcP.png b/tests/fixtures/labelary_devicefont_images/fF_m2_lcP.png new file mode 100644 index 00000000..650a17ec Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fF_m2_lcP.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fF_m3_5.png b/tests/fixtures/labelary_devicefont_images/fF_m3_5.png new file mode 100644 index 00000000..4f7efaad Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fF_m3_5.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fF_m3_M.png b/tests/fixtures/labelary_devicefont_images/fF_m3_M.png new file mode 100644 index 00000000..c492d9f3 Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fF_m3_M.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fF_m3_lcP.png b/tests/fixtures/labelary_devicefont_images/fF_m3_lcP.png new file mode 100644 index 00000000..0ee1af65 Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fF_m3_lcP.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fF_m5.png b/tests/fixtures/labelary_devicefont_images/fF_m5.png new file mode 100644 index 00000000..04ffc573 Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fF_m5.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fF_snap.png b/tests/fixtures/labelary_devicefont_images/fF_snap.png new file mode 100644 index 00000000..b535ff9b Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fF_snap.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fF_w1.png b/tests/fixtures/labelary_devicefont_images/fF_w1.png new file mode 100644 index 00000000..058f538b Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fF_w1.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fF_w5.png b/tests/fixtures/labelary_devicefont_images/fF_w5.png new file mode 100644 index 00000000..5e0b1da6 Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fF_w5.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fG_m1_5.png b/tests/fixtures/labelary_devicefont_images/fG_m1_5.png new file mode 100644 index 00000000..2b2b2380 Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fG_m1_5.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fG_m1_M.png b/tests/fixtures/labelary_devicefont_images/fG_m1_M.png new file mode 100644 index 00000000..a28562da Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fG_m1_M.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fG_m1_lcP.png b/tests/fixtures/labelary_devicefont_images/fG_m1_lcP.png new file mode 100644 index 00000000..02cbbd75 Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fG_m1_lcP.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fG_m2_5.png b/tests/fixtures/labelary_devicefont_images/fG_m2_5.png new file mode 100644 index 00000000..46563bdb Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fG_m2_5.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fG_m2_M.png b/tests/fixtures/labelary_devicefont_images/fG_m2_M.png new file mode 100644 index 00000000..55b72fa0 Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fG_m2_M.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fG_m2_lcP.png b/tests/fixtures/labelary_devicefont_images/fG_m2_lcP.png new file mode 100644 index 00000000..0354c9f5 Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fG_m2_lcP.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fG_m3_5.png b/tests/fixtures/labelary_devicefont_images/fG_m3_5.png new file mode 100644 index 00000000..3c179b33 Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fG_m3_5.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fG_m3_M.png b/tests/fixtures/labelary_devicefont_images/fG_m3_M.png new file mode 100644 index 00000000..fe4612cf Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fG_m3_M.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fG_m3_lcP.png b/tests/fixtures/labelary_devicefont_images/fG_m3_lcP.png new file mode 100644 index 00000000..786800d6 Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fG_m3_lcP.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fG_m5.png b/tests/fixtures/labelary_devicefont_images/fG_m5.png new file mode 100644 index 00000000..68cf1ee6 Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fG_m5.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fG_snap.png b/tests/fixtures/labelary_devicefont_images/fG_snap.png new file mode 100644 index 00000000..69e6399b Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fG_snap.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fG_w1.png b/tests/fixtures/labelary_devicefont_images/fG_w1.png new file mode 100644 index 00000000..1237c7db Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fG_w1.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fG_w5.png b/tests/fixtures/labelary_devicefont_images/fG_w5.png new file mode 100644 index 00000000..e6596fb5 Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fG_w5.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fH_m1_5.png b/tests/fixtures/labelary_devicefont_images/fH_m1_5.png new file mode 100644 index 00000000..55abc5d9 Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fH_m1_5.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fH_m1_M.png b/tests/fixtures/labelary_devicefont_images/fH_m1_M.png new file mode 100644 index 00000000..9585966a Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fH_m1_M.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fH_m1_W.png b/tests/fixtures/labelary_devicefont_images/fH_m1_W.png new file mode 100644 index 00000000..986578da Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fH_m1_W.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fH_m2_5.png b/tests/fixtures/labelary_devicefont_images/fH_m2_5.png new file mode 100644 index 00000000..250daebe Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fH_m2_5.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fH_m2_M.png b/tests/fixtures/labelary_devicefont_images/fH_m2_M.png new file mode 100644 index 00000000..0df9121b Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fH_m2_M.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fH_m2_W.png b/tests/fixtures/labelary_devicefont_images/fH_m2_W.png new file mode 100644 index 00000000..fc9bbb47 Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fH_m2_W.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fH_m3_5.png b/tests/fixtures/labelary_devicefont_images/fH_m3_5.png new file mode 100644 index 00000000..ada5d851 Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fH_m3_5.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fH_m3_M.png b/tests/fixtures/labelary_devicefont_images/fH_m3_M.png new file mode 100644 index 00000000..1b0300c4 Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fH_m3_M.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fH_m3_W.png b/tests/fixtures/labelary_devicefont_images/fH_m3_W.png new file mode 100644 index 00000000..92c406e0 Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fH_m3_W.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fH_m5.png b/tests/fixtures/labelary_devicefont_images/fH_m5.png new file mode 100644 index 00000000..41fa208f Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fH_m5.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fH_snap.png b/tests/fixtures/labelary_devicefont_images/fH_snap.png new file mode 100644 index 00000000..51b55902 Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fH_snap.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fH_w1.png b/tests/fixtures/labelary_devicefont_images/fH_w1.png new file mode 100644 index 00000000..2d05a654 Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fH_w1.png differ diff --git a/tests/fixtures/labelary_devicefont_images/fH_w5.png b/tests/fixtures/labelary_devicefont_images/fH_w5.png new file mode 100644 index 00000000..942a10c5 Binary files /dev/null and b/tests/fixtures/labelary_devicefont_images/fH_w5.png differ diff --git a/tests/fixtures/labelary_text_default_images/h30_mixed.png b/tests/fixtures/labelary_text_default_images/h30_mixed.png new file mode 100644 index 00000000..22476ef4 Binary files /dev/null and b/tests/fixtures/labelary_text_default_images/h30_mixed.png differ diff --git a/tests/fixtures/labelary_text_default_images/h50_1.png b/tests/fixtures/labelary_text_default_images/h50_1.png new file mode 100644 index 00000000..ce2d55bb Binary files /dev/null and b/tests/fixtures/labelary_text_default_images/h50_1.png differ diff --git a/tests/fixtures/labelary_text_default_images/h50_2.png b/tests/fixtures/labelary_text_default_images/h50_2.png new file mode 100644 index 00000000..1d1d0949 Binary files /dev/null and b/tests/fixtures/labelary_text_default_images/h50_2.png differ diff --git a/tests/fixtures/labelary_text_default_images/h50_3.png b/tests/fixtures/labelary_text_default_images/h50_3.png new file mode 100644 index 00000000..ad6cc78f Binary files /dev/null and b/tests/fixtures/labelary_text_default_images/h50_3.png differ diff --git a/tests/fixtures/labelary_text_default_images/h50_4.png b/tests/fixtures/labelary_text_default_images/h50_4.png new file mode 100644 index 00000000..ddaa86bd Binary files /dev/null and b/tests/fixtures/labelary_text_default_images/h50_4.png differ diff --git a/tests/fixtures/labelary_text_default_images/h50_6.png b/tests/fixtures/labelary_text_default_images/h50_6.png new file mode 100644 index 00000000..b1dbb298 Binary files /dev/null and b/tests/fixtures/labelary_text_default_images/h50_6.png differ diff --git a/tests/fixtures/labelary_text_default_images/h50_7.png b/tests/fixtures/labelary_text_default_images/h50_7.png new file mode 100644 index 00000000..59d6b71d Binary files /dev/null and b/tests/fixtures/labelary_text_default_images/h50_7.png differ diff --git a/tests/fixtures/labelary_text_default_images/h50_8.png b/tests/fixtures/labelary_text_default_images/h50_8.png new file mode 100644 index 00000000..8860499e Binary files /dev/null and b/tests/fixtures/labelary_text_default_images/h50_8.png differ diff --git a/tests/fixtures/labelary_text_default_images/h50_B.png b/tests/fixtures/labelary_text_default_images/h50_B.png new file mode 100644 index 00000000..f4c789f3 Binary files /dev/null and b/tests/fixtures/labelary_text_default_images/h50_B.png differ diff --git a/tests/fixtures/labelary_text_default_images/h50_C.png b/tests/fixtures/labelary_text_default_images/h50_C.png new file mode 100644 index 00000000..a1e1266c Binary files /dev/null and b/tests/fixtures/labelary_text_default_images/h50_C.png differ diff --git a/tests/fixtures/labelary_text_default_images/h50_D.png b/tests/fixtures/labelary_text_default_images/h50_D.png new file mode 100644 index 00000000..10746dc3 Binary files /dev/null and b/tests/fixtures/labelary_text_default_images/h50_D.png differ diff --git a/tests/fixtures/labelary_text_default_images/h50_E.png b/tests/fixtures/labelary_text_default_images/h50_E.png new file mode 100644 index 00000000..6691ea30 Binary files /dev/null and b/tests/fixtures/labelary_text_default_images/h50_E.png differ diff --git a/tests/fixtures/labelary_text_default_images/h50_F.png b/tests/fixtures/labelary_text_default_images/h50_F.png new file mode 100644 index 00000000..bcb3cbf0 Binary files /dev/null and b/tests/fixtures/labelary_text_default_images/h50_F.png differ diff --git a/tests/fixtures/labelary_text_default_images/h50_G.png b/tests/fixtures/labelary_text_default_images/h50_G.png new file mode 100644 index 00000000..f02e5889 Binary files /dev/null and b/tests/fixtures/labelary_text_default_images/h50_G.png differ diff --git a/tests/fixtures/labelary_text_default_images/h50_H.png b/tests/fixtures/labelary_text_default_images/h50_H.png new file mode 100644 index 00000000..18730b9f Binary files /dev/null and b/tests/fixtures/labelary_text_default_images/h50_H.png differ diff --git a/tests/fixtures/labelary_text_default_images/h50_I.png b/tests/fixtures/labelary_text_default_images/h50_I.png new file mode 100644 index 00000000..b1b2ab99 Binary files /dev/null and b/tests/fixtures/labelary_text_default_images/h50_I.png differ diff --git a/tests/fixtures/labelary_text_default_images/h50_J.png b/tests/fixtures/labelary_text_default_images/h50_J.png new file mode 100644 index 00000000..81617157 Binary files /dev/null and b/tests/fixtures/labelary_text_default_images/h50_J.png differ diff --git a/tests/fixtures/labelary_text_default_images/h50_K.png b/tests/fixtures/labelary_text_default_images/h50_K.png new file mode 100644 index 00000000..029b6353 Binary files /dev/null and b/tests/fixtures/labelary_text_default_images/h50_K.png differ diff --git a/tests/fixtures/labelary_text_default_images/h50_L.png b/tests/fixtures/labelary_text_default_images/h50_L.png new file mode 100644 index 00000000..6ed847d5 Binary files /dev/null and b/tests/fixtures/labelary_text_default_images/h50_L.png differ diff --git a/tests/fixtures/labelary_text_default_images/h50_N.png b/tests/fixtures/labelary_text_default_images/h50_N.png new file mode 100644 index 00000000..62ff4ea9 Binary files /dev/null and b/tests/fixtures/labelary_text_default_images/h50_N.png differ diff --git a/tests/fixtures/labelary_text_default_images/h50_O.png b/tests/fixtures/labelary_text_default_images/h50_O.png new file mode 100644 index 00000000..591835f2 Binary files /dev/null and b/tests/fixtures/labelary_text_default_images/h50_O.png differ diff --git a/tests/fixtures/labelary_text_default_images/h50_P.png b/tests/fixtures/labelary_text_default_images/h50_P.png new file mode 100644 index 00000000..6fd0aa6a Binary files /dev/null and b/tests/fixtures/labelary_text_default_images/h50_P.png differ diff --git a/tests/fixtures/labelary_text_default_images/h50_Q.png b/tests/fixtures/labelary_text_default_images/h50_Q.png new file mode 100644 index 00000000..ab083cd6 Binary files /dev/null and b/tests/fixtures/labelary_text_default_images/h50_Q.png differ diff --git a/tests/fixtures/labelary_text_default_images/h50_R.png b/tests/fixtures/labelary_text_default_images/h50_R.png new file mode 100644 index 00000000..49e913d1 Binary files /dev/null and b/tests/fixtures/labelary_text_default_images/h50_R.png differ diff --git a/tests/fixtures/labelary_text_default_images/h50_S.png b/tests/fixtures/labelary_text_default_images/h50_S.png new file mode 100644 index 00000000..5c7fc62e Binary files /dev/null and b/tests/fixtures/labelary_text_default_images/h50_S.png differ diff --git a/tests/fixtures/labelary_text_default_images/h50_T.png b/tests/fixtures/labelary_text_default_images/h50_T.png new file mode 100644 index 00000000..3ad74fc0 Binary files /dev/null and b/tests/fixtures/labelary_text_default_images/h50_T.png differ diff --git a/tests/fixtures/labelary_text_default_images/h50_U.png b/tests/fixtures/labelary_text_default_images/h50_U.png new file mode 100644 index 00000000..e27028d6 Binary files /dev/null and b/tests/fixtures/labelary_text_default_images/h50_U.png differ diff --git a/tests/fixtures/labelary_text_default_images/h50_V.png b/tests/fixtures/labelary_text_default_images/h50_V.png new file mode 100644 index 00000000..86ce80ca Binary files /dev/null and b/tests/fixtures/labelary_text_default_images/h50_V.png differ diff --git a/tests/fixtures/labelary_text_default_images/h50_X.png b/tests/fixtures/labelary_text_default_images/h50_X.png new file mode 100644 index 00000000..2f920b32 Binary files /dev/null and b/tests/fixtures/labelary_text_default_images/h50_X.png differ diff --git a/tests/fixtures/labelary_text_default_images/h50_Y.png b/tests/fixtures/labelary_text_default_images/h50_Y.png new file mode 100644 index 00000000..d84294c8 Binary files /dev/null and b/tests/fixtures/labelary_text_default_images/h50_Y.png differ diff --git a/tests/fixtures/labelary_text_default_images/h50_Z.png b/tests/fixtures/labelary_text_default_images/h50_Z.png new file mode 100644 index 00000000..ce1689f5 Binary files /dev/null and b/tests/fixtures/labelary_text_default_images/h50_Z.png differ diff --git a/tests/fixtures/labelary_text_default_images/h50_amp.png b/tests/fixtures/labelary_text_default_images/h50_amp.png new file mode 100644 index 00000000..0b1ca2d1 Binary files /dev/null and b/tests/fixtures/labelary_text_default_images/h50_amp.png differ diff --git a/tests/fixtures/labelary_text_default_images/h50_at.png b/tests/fixtures/labelary_text_default_images/h50_at.png new file mode 100644 index 00000000..a4509c2c Binary files /dev/null and b/tests/fixtures/labelary_text_default_images/h50_at.png differ diff --git a/tests/fixtures/labelary_text_default_images/h50_bang.png b/tests/fixtures/labelary_text_default_images/h50_bang.png new file mode 100644 index 00000000..e62113e7 Binary files /dev/null and b/tests/fixtures/labelary_text_default_images/h50_bang.png differ diff --git a/tests/fixtures/labelary_text_default_images/h50_colon.png b/tests/fixtures/labelary_text_default_images/h50_colon.png new file mode 100644 index 00000000..986b3a9e Binary files /dev/null and b/tests/fixtures/labelary_text_default_images/h50_colon.png differ diff --git a/tests/fixtures/labelary_text_default_images/h50_comma.png b/tests/fixtures/labelary_text_default_images/h50_comma.png new file mode 100644 index 00000000..b6e0ae96 Binary files /dev/null and b/tests/fixtures/labelary_text_default_images/h50_comma.png differ diff --git a/tests/fixtures/labelary_text_default_images/h50_dollar.png b/tests/fixtures/labelary_text_default_images/h50_dollar.png new file mode 100644 index 00000000..6c17ab7e Binary files /dev/null and b/tests/fixtures/labelary_text_default_images/h50_dollar.png differ diff --git a/tests/fixtures/labelary_text_default_images/h50_dquote.png b/tests/fixtures/labelary_text_default_images/h50_dquote.png new file mode 100644 index 00000000..19993a19 Binary files /dev/null and b/tests/fixtures/labelary_text_default_images/h50_dquote.png differ diff --git a/tests/fixtures/labelary_text_default_images/h50_gt.png b/tests/fixtures/labelary_text_default_images/h50_gt.png new file mode 100644 index 00000000..6cc1816b Binary files /dev/null and b/tests/fixtures/labelary_text_default_images/h50_gt.png differ diff --git a/tests/fixtures/labelary_text_default_images/h50_hash.png b/tests/fixtures/labelary_text_default_images/h50_hash.png new file mode 100644 index 00000000..3a8e3056 Binary files /dev/null and b/tests/fixtures/labelary_text_default_images/h50_hash.png differ diff --git a/tests/fixtures/labelary_text_default_images/h50_lcB.png b/tests/fixtures/labelary_text_default_images/h50_lcB.png new file mode 100644 index 00000000..6d0c3de2 Binary files /dev/null and b/tests/fixtures/labelary_text_default_images/h50_lcB.png differ diff --git a/tests/fixtures/labelary_text_default_images/h50_lcC.png b/tests/fixtures/labelary_text_default_images/h50_lcC.png new file mode 100644 index 00000000..26199e7a Binary files /dev/null and b/tests/fixtures/labelary_text_default_images/h50_lcC.png differ diff --git a/tests/fixtures/labelary_text_default_images/h50_lcD.png b/tests/fixtures/labelary_text_default_images/h50_lcD.png new file mode 100644 index 00000000..89e00e3b Binary files /dev/null and b/tests/fixtures/labelary_text_default_images/h50_lcD.png differ diff --git a/tests/fixtures/labelary_text_default_images/h50_lcE.png b/tests/fixtures/labelary_text_default_images/h50_lcE.png new file mode 100644 index 00000000..74acb100 Binary files /dev/null and b/tests/fixtures/labelary_text_default_images/h50_lcE.png differ diff --git a/tests/fixtures/labelary_text_default_images/h50_lcF.png b/tests/fixtures/labelary_text_default_images/h50_lcF.png new file mode 100644 index 00000000..2df35b41 Binary files /dev/null and b/tests/fixtures/labelary_text_default_images/h50_lcF.png differ diff --git a/tests/fixtures/labelary_text_default_images/h50_lcG.png b/tests/fixtures/labelary_text_default_images/h50_lcG.png new file mode 100644 index 00000000..355afca5 Binary files /dev/null and b/tests/fixtures/labelary_text_default_images/h50_lcG.png differ diff --git a/tests/fixtures/labelary_text_default_images/h50_lcH.png b/tests/fixtures/labelary_text_default_images/h50_lcH.png new file mode 100644 index 00000000..37777e4b Binary files /dev/null and b/tests/fixtures/labelary_text_default_images/h50_lcH.png differ diff --git a/tests/fixtures/labelary_text_default_images/h50_lcJ.png b/tests/fixtures/labelary_text_default_images/h50_lcJ.png new file mode 100644 index 00000000..56948450 Binary files /dev/null and b/tests/fixtures/labelary_text_default_images/h50_lcJ.png differ diff --git a/tests/fixtures/labelary_text_default_images/h50_lcK.png b/tests/fixtures/labelary_text_default_images/h50_lcK.png new file mode 100644 index 00000000..8ee6692d Binary files /dev/null and b/tests/fixtures/labelary_text_default_images/h50_lcK.png differ diff --git a/tests/fixtures/labelary_text_default_images/h50_lcL.png b/tests/fixtures/labelary_text_default_images/h50_lcL.png new file mode 100644 index 00000000..fc8912a9 Binary files /dev/null and b/tests/fixtures/labelary_text_default_images/h50_lcL.png differ diff --git a/tests/fixtures/labelary_text_default_images/h50_lcM.png b/tests/fixtures/labelary_text_default_images/h50_lcM.png new file mode 100644 index 00000000..0f96f33d Binary files /dev/null and b/tests/fixtures/labelary_text_default_images/h50_lcM.png differ diff --git a/tests/fixtures/labelary_text_default_images/h50_lcN.png b/tests/fixtures/labelary_text_default_images/h50_lcN.png new file mode 100644 index 00000000..04504191 Binary files /dev/null and b/tests/fixtures/labelary_text_default_images/h50_lcN.png differ diff --git a/tests/fixtures/labelary_text_default_images/h50_lcO.png b/tests/fixtures/labelary_text_default_images/h50_lcO.png new file mode 100644 index 00000000..4251d7d1 Binary files /dev/null and b/tests/fixtures/labelary_text_default_images/h50_lcO.png differ diff --git a/tests/fixtures/labelary_text_default_images/h50_lcQ.png b/tests/fixtures/labelary_text_default_images/h50_lcQ.png new file mode 100644 index 00000000..7cea6d3c Binary files /dev/null and b/tests/fixtures/labelary_text_default_images/h50_lcQ.png differ diff --git a/tests/fixtures/labelary_text_default_images/h50_lcR.png b/tests/fixtures/labelary_text_default_images/h50_lcR.png new file mode 100644 index 00000000..17885a19 Binary files /dev/null and b/tests/fixtures/labelary_text_default_images/h50_lcR.png differ diff --git a/tests/fixtures/labelary_text_default_images/h50_lcS.png b/tests/fixtures/labelary_text_default_images/h50_lcS.png new file mode 100644 index 00000000..b1c0d70a Binary files /dev/null and b/tests/fixtures/labelary_text_default_images/h50_lcS.png differ diff --git a/tests/fixtures/labelary_text_default_images/h50_lcT.png b/tests/fixtures/labelary_text_default_images/h50_lcT.png new file mode 100644 index 00000000..a7d20d22 Binary files /dev/null and b/tests/fixtures/labelary_text_default_images/h50_lcT.png differ diff --git a/tests/fixtures/labelary_text_default_images/h50_lcU.png b/tests/fixtures/labelary_text_default_images/h50_lcU.png new file mode 100644 index 00000000..bfc48939 Binary files /dev/null and b/tests/fixtures/labelary_text_default_images/h50_lcU.png differ diff --git a/tests/fixtures/labelary_text_default_images/h50_lcV.png b/tests/fixtures/labelary_text_default_images/h50_lcV.png new file mode 100644 index 00000000..2b3f4a40 Binary files /dev/null and b/tests/fixtures/labelary_text_default_images/h50_lcV.png differ diff --git a/tests/fixtures/labelary_text_default_images/h50_lcW.png b/tests/fixtures/labelary_text_default_images/h50_lcW.png new file mode 100644 index 00000000..12033469 Binary files /dev/null and b/tests/fixtures/labelary_text_default_images/h50_lcW.png differ diff --git a/tests/fixtures/labelary_text_default_images/h50_lcX.png b/tests/fixtures/labelary_text_default_images/h50_lcX.png new file mode 100644 index 00000000..a63a1aa6 Binary files /dev/null and b/tests/fixtures/labelary_text_default_images/h50_lcX.png differ diff --git a/tests/fixtures/labelary_text_default_images/h50_lcY.png b/tests/fixtures/labelary_text_default_images/h50_lcY.png new file mode 100644 index 00000000..22e2acde Binary files /dev/null and b/tests/fixtures/labelary_text_default_images/h50_lcY.png differ diff --git a/tests/fixtures/labelary_text_default_images/h50_lcZ.png b/tests/fixtures/labelary_text_default_images/h50_lcZ.png new file mode 100644 index 00000000..db798be3 Binary files /dev/null and b/tests/fixtures/labelary_text_default_images/h50_lcZ.png differ diff --git a/tests/fixtures/labelary_text_default_images/h50_lparen.png b/tests/fixtures/labelary_text_default_images/h50_lparen.png new file mode 100644 index 00000000..4e5748b1 Binary files /dev/null and b/tests/fixtures/labelary_text_default_images/h50_lparen.png differ diff --git a/tests/fixtures/labelary_text_default_images/h50_lt.png b/tests/fixtures/labelary_text_default_images/h50_lt.png new file mode 100644 index 00000000..c47da2b0 Binary files /dev/null and b/tests/fixtures/labelary_text_default_images/h50_lt.png differ diff --git a/tests/fixtures/labelary_text_default_images/h50_pct.png b/tests/fixtures/labelary_text_default_images/h50_pct.png new file mode 100644 index 00000000..e6f2c653 Binary files /dev/null and b/tests/fixtures/labelary_text_default_images/h50_pct.png differ diff --git a/tests/fixtures/labelary_text_default_images/h50_plus.png b/tests/fixtures/labelary_text_default_images/h50_plus.png new file mode 100644 index 00000000..60d4ca5e Binary files /dev/null and b/tests/fixtures/labelary_text_default_images/h50_plus.png differ diff --git a/tests/fixtures/labelary_text_default_images/h50_qmark.png b/tests/fixtures/labelary_text_default_images/h50_qmark.png new file mode 100644 index 00000000..4e9a5f37 Binary files /dev/null and b/tests/fixtures/labelary_text_default_images/h50_qmark.png differ diff --git a/tests/fixtures/labelary_text_default_images/h50_rparen.png b/tests/fixtures/labelary_text_default_images/h50_rparen.png new file mode 100644 index 00000000..38069774 Binary files /dev/null and b/tests/fixtures/labelary_text_default_images/h50_rparen.png differ diff --git a/tests/fixtures/labelary_text_default_images/h50_semi.png b/tests/fixtures/labelary_text_default_images/h50_semi.png new file mode 100644 index 00000000..049442f9 Binary files /dev/null and b/tests/fixtures/labelary_text_default_images/h50_semi.png differ diff --git a/tests/fixtures/labelary_text_default_images/h50_shipto.png b/tests/fixtures/labelary_text_default_images/h50_shipto.png new file mode 100644 index 00000000..5fab9e9a Binary files /dev/null and b/tests/fixtures/labelary_text_default_images/h50_shipto.png differ diff --git a/tests/fixtures/labelary_text_default_images/h50_slash.png b/tests/fixtures/labelary_text_default_images/h50_slash.png new file mode 100644 index 00000000..f261cb3a Binary files /dev/null and b/tests/fixtures/labelary_text_default_images/h50_slash.png differ diff --git a/tests/fixtures/labelary_text_default_images/h50_squote.png b/tests/fixtures/labelary_text_default_images/h50_squote.png new file mode 100644 index 00000000..bedb2c14 Binary files /dev/null and b/tests/fixtures/labelary_text_default_images/h50_squote.png differ diff --git a/tests/fixtures/labelary_text_default_images/h50_star.png b/tests/fixtures/labelary_text_default_images/h50_star.png new file mode 100644 index 00000000..a98d4935 Binary files /dev/null and b/tests/fixtures/labelary_text_default_images/h50_star.png differ diff --git a/tests/fixtures/labelary_text_default_images/h50_under.png b/tests/fixtures/labelary_text_default_images/h50_under.png new file mode 100644 index 00000000..c617db68 Binary files /dev/null and b/tests/fixtures/labelary_text_default_images/h50_under.png differ diff --git a/tests/fixtures/labelary_text_images/h20_n_num.png b/tests/fixtures/labelary_text_images/h20_n_num.png index 2aef4d3a..62f04335 100644 Binary files a/tests/fixtures/labelary_text_images/h20_n_num.png and b/tests/fixtures/labelary_text_images/h20_n_num.png differ diff --git a/tests/fixtures/labelary_text_images/h30_n_alpha.png b/tests/fixtures/labelary_text_images/h30_n_alpha.png index a4f43a9e..b81665cf 100644 Binary files a/tests/fixtures/labelary_text_images/h30_n_alpha.png and b/tests/fixtures/labelary_text_images/h30_n_alpha.png differ diff --git a/tests/fixtures/labelary_text_images/h30_n_date.png b/tests/fixtures/labelary_text_images/h30_n_date.png index 444234e8..1ecefcfd 100644 Binary files a/tests/fixtures/labelary_text_images/h30_n_date.png and b/tests/fixtures/labelary_text_images/h30_n_date.png differ diff --git a/tests/fixtures/labelary_text_images/h30_n_decimal.png b/tests/fixtures/labelary_text_images/h30_n_decimal.png index c0104bdd..d2a64f6e 100644 Binary files a/tests/fixtures/labelary_text_images/h30_n_decimal.png and b/tests/fixtures/labelary_text_images/h30_n_decimal.png differ diff --git a/tests/fixtures/labelary_text_images/h30_n_num.png b/tests/fixtures/labelary_text_images/h30_n_num.png index f9385069..e2cd1f8b 100644 Binary files a/tests/fixtures/labelary_text_images/h30_n_num.png and b/tests/fixtures/labelary_text_images/h30_n_num.png differ diff --git a/tests/fixtures/labelary_text_images/h39_n_long.png b/tests/fixtures/labelary_text_images/h39_n_long.png index e05cc267..13cbc802 100644 Binary files a/tests/fixtures/labelary_text_images/h39_n_long.png and b/tests/fixtures/labelary_text_images/h39_n_long.png differ diff --git a/tests/fixtures/labelary_text_images/h50_b_mix.png b/tests/fixtures/labelary_text_images/h50_b_mix.png index 2844a045..48f19425 100644 Binary files a/tests/fixtures/labelary_text_images/h50_b_mix.png and b/tests/fixtures/labelary_text_images/h50_b_mix.png differ diff --git a/tests/fixtures/labelary_text_images/h50_i_mix.png b/tests/fixtures/labelary_text_images/h50_i_mix.png index a20a04db..6d603ef8 100644 Binary files a/tests/fixtures/labelary_text_images/h50_i_mix.png and b/tests/fixtures/labelary_text_images/h50_i_mix.png differ diff --git a/tests/fixtures/labelary_text_images/h50_n_num.png b/tests/fixtures/labelary_text_images/h50_n_num.png index ebd43ed4..ff4726fd 100644 Binary files a/tests/fixtures/labelary_text_images/h50_n_num.png and b/tests/fixtures/labelary_text_images/h50_n_num.png differ diff --git a/tests/fixtures/labelary_text_images/h50_r_mix.png b/tests/fixtures/labelary_text_images/h50_r_mix.png index 07f027f6..cf67c655 100644 Binary files a/tests/fixtures/labelary_text_images/h50_r_mix.png and b/tests/fixtures/labelary_text_images/h50_r_mix.png differ diff --git a/tests/fixtures/labelary_text_images/h80_n_alpha.png b/tests/fixtures/labelary_text_images/h80_n_alpha.png index 75a4663e..494041af 100644 Binary files a/tests/fixtures/labelary_text_images/h80_n_alpha.png and b/tests/fixtures/labelary_text_images/h80_n_alpha.png differ diff --git a/tests/fixtures/labelary_text_images/h80_n_num.png b/tests/fixtures/labelary_text_images/h80_n_num.png index 29cbddb5..71bd0ffd 100644 Binary files a/tests/fixtures/labelary_text_images/h80_n_num.png and b/tests/fixtures/labelary_text_images/h80_n_num.png differ diff --git a/tests/fixtures/textBoxMatchCases.ts b/tests/fixtures/textBoxMatchCases.ts index fe2b1539..9fbd842d 100644 --- a/tests/fixtures/textBoxMatchCases.ts +++ b/tests/fixtures/textBoxMatchCases.ts @@ -13,6 +13,8 @@ * Sizes cover the common label range: 20 (small fineprint) through 80 * (header text). Skipping intermediate sizes here keeps the fixture * set focused. */ +import { charSlug } from './charSlug'; + export interface TextBoxMatchCase { id: string; fontHeight: number; @@ -41,17 +43,7 @@ const CHARS = [ '-', '.', '=', ]; -/** Build a filesystem-safe id segment for `char`. NTFS / macOS HFS+ - * default to case-insensitive filenames, so `A` and `a` would collide - * as fixture PNGs. Prefix lowercase ASCII letters with `lc` to keep - * the test ids one-to-one with their files. */ -function charSlug(c: string): string { - if (c === '-') return 'minus'; - if (c === '.') return 'dot'; - if (c === '=') return 'eq'; - if (c >= 'a' && c <= 'z') return `lc${c.toUpperCase()}`; - return c; -} + const uniformCases: TextBoxMatchCase[] = SIZES.flatMap((h) => CHARS.map((c) => ({ @@ -76,7 +68,16 @@ const stretchedCases: TextBoxMatchCase[] = [ { id: 'h30_w60_alpha', fontHeight: 30, fontWidth: 60, text: 'ABC', rotation: 'N', x: 200, y: 200 }, ]; +/** Long multi-word lines: char x3 cases contain no spaces, so space + * advance drift and per-char accumulation over realistic label text + * only show up here. */ +const longTextCases: TextBoxMatchCase[] = [ + { id: 'h50_shipto', fontHeight: 50, fontWidth: 0, text: 'SHIP TO: 123 MAIN STREET', rotation: 'N', x: 20, y: 200 }, + { id: 'h30_mixed', fontHeight: 30, fontWidth: 0, text: 'Artikel: 4711-XL Gewicht 12,5kg', rotation: 'N', x: 20, y: 200 }, +]; + export const textBoxMatchCases: TextBoxMatchCase[] = [ ...uniformCases, ...stretchedCases, + ...longTextCases, ]; diff --git a/tests/lib/boxMatch.ts b/tests/lib/boxMatch.ts new file mode 100644 index 00000000..d8be046a --- /dev/null +++ b/tests/lib/boxMatch.ts @@ -0,0 +1,39 @@ +import { expect } from 'vitest'; +import type { InkBBox } from './darkBBox'; + +/** Proportional slope (5% of the fixture's own bbox) so + * magnification-proportional drift, e.g. a wrong capInkPerMag, trips + * on tall cases a flat tolerance would swallow; the noise floor stays + * visible at the call site. */ +export function toleranceDots(labelaryDim: number, floorDots: number): number { + return Math.max(floorDots, Math.round(labelaryDim * 0.05)); +} + +/** Shared 4-axis gate so "systematic drift" cannot diverge between + * suites; textBoxMatch opts out on purpose (size-only sweep, no + * position gates). */ +export function expectBoxMatch( + local: InkBBox, + labelary: InkBBox, + floorDots: number, + id: string, +): void { + const wTol = toleranceDots(labelary.width, floorDots); + const hTol = toleranceDots(labelary.height, floorDots); + expect( + Math.abs(local.width - labelary.width), + `width drift for ${id}: local ${local.width} vs labelary ${labelary.width}`, + ).toBeLessThanOrEqual(wTol); + expect( + Math.abs(local.height - labelary.height), + `height drift for ${id}: local ${local.height} vs labelary ${labelary.height}`, + ).toBeLessThanOrEqual(hTol); + expect( + Math.abs(local.x - labelary.x), + `x drift for ${id}: local ${local.x} vs labelary ${labelary.x}`, + ).toBeLessThanOrEqual(wTol); + expect( + Math.abs(local.y - labelary.y), + `y drift for ${id}: local ${local.y} vs labelary ${labelary.y}`, + ).toBeLessThanOrEqual(hTol); +} diff --git a/tests/lib/darkBBox.ts b/tests/lib/darkBBox.ts new file mode 100644 index 00000000..a60eb635 --- /dev/null +++ b/tests/lib/darkBBox.ts @@ -0,0 +1,36 @@ +import type { PNG } from 'pngjs'; + +export interface InkBBox { + x: number; + y: number; + width: number; + height: number; +} + +/** The luminance cutoff is the shared definition of "ink"; forking it + * makes the gates disagree on glyph size. Extent is in pixels (= dots + * on the 8 dpmm fixture canvases). */ +export function darkBBox(png: PNG): InkBBox { + const { width, height, data } = png; + let minX = width; + let minY = height; + let maxX = -1; + let maxY = -1; + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + const idx = (y * width + x) * 4; + const lum = + 0.299 * (data[idx] ?? 0) + + 0.587 * (data[idx + 1] ?? 0) + + 0.114 * (data[idx + 2] ?? 0); + if (lum < 200) { + if (x < minX) minX = x; + if (y < minY) minY = y; + if (x > maxX) maxX = x; + if (y > maxY) maxY = y; + } + } + } + if (maxX < 0) return { x: 0, y: 0, width: 0, height: 0 }; + return { x: minX, y: minY, width: maxX - minX + 1, height: maxY - minY + 1 }; +} diff --git a/tests/lib/drawKonvaText.ts b/tests/lib/drawKonvaText.ts new file mode 100644 index 00000000..646d59a9 --- /dev/null +++ b/tests/lib/drawKonvaText.ts @@ -0,0 +1,50 @@ +import { konvaBaselineY } from './konvaBaseline'; + +interface Canvas2dLike { + font: string; + textBaseline: string; + measureText(text: string): TextMetrics; + fillText(text: string, x: number, y: number): void; + save(): void; + restore(): void; + translate(x: number, y: number): void; + scale(x: number, y: number): void; +} + +export interface KonvaTextDraw { + text: string; + /** Model-space top-left, device nudges already applied. */ + x: number; + y: number; + fontSizePx: number; + fontFamily: string; + fontStyle?: string; + scaleX?: number; + letterSpacingPx?: number; +} + +/** Single copy of the Konva-equivalent placement chain; a + * half-replicated variant is how a placement regression slips past + * one gate but not another. */ +export function drawKonvaText(ctx: Canvas2dLike, d: KonvaTextDraw): void { + const style = d.fontStyle ? `${d.fontStyle} ` : ''; + ctx.font = `${style}${d.fontSizePx}px "${d.fontFamily}"`; + ctx.textBaseline = 'alphabetic'; + const baselineY = konvaBaselineY(ctx, d.fontSizePx); + ctx.save(); + ctx.translate(d.x, d.y); + ctx.scale(d.scaleX ?? 1, 1); + const spacing = d.letterSpacingPx ?? 0; + if (spacing === 0) { + ctx.fillText(d.text, 0, baselineY); + } else { + // Konva applies letterSpacing between glyphs; canvas2d fillText has + // no portable equivalent, so draw per char. + let x = 0; + for (const ch of d.text) { + ctx.fillText(ch, x, baselineY); + x += ctx.measureText(ch).width + spacing; + } + } + ctx.restore(); +} diff --git a/tests/lib/inkCanvas.ts b/tests/lib/inkCanvas.ts new file mode 100644 index 00000000..0fcb13b5 --- /dev/null +++ b/tests/lib/inkCanvas.ts @@ -0,0 +1,14 @@ +import { createCanvas, type Canvas, type SKRSContext2D } from '@napi-rs/canvas'; + +/** 4x4 inch label at 8 dpmm, matching the Labelary fixtures. */ +export const INK_CANVAS_PX = 812; + +/** White canvas with black fill, the ink convention darkBBox scans for. */ +export function inkCanvas(px = INK_CANVAS_PX): { canvas: Canvas; ctx: SKRSContext2D } { + const canvas = createCanvas(px, px); + const ctx = canvas.getContext('2d'); + ctx.fillStyle = 'white'; + ctx.fillRect(0, 0, px, px); + ctx.fillStyle = 'black'; + return { canvas, ctx }; +} diff --git a/tests/lib/konvaBaseline.ts b/tests/lib/konvaBaseline.ts new file mode 100644 index 00000000..5e07389b --- /dev/null +++ b/tests/lib/konvaBaseline.ts @@ -0,0 +1,15 @@ +/** Konva's (non-legacy) line placement: alphabetic baseline at + * (fontBoundingBoxAscent - Descent)/2 + lineHeight/2, lineHeight = 1. */ +export function konvaBaselineY( + ctx: { measureText(text: string): TextMetrics }, + fontSizePx: number, +): number { + const m = ctx.measureText('M'); + const half = (m.fontBoundingBoxAscent - m.fontBoundingBoxDescent) / 2; + // Ink-box metrics are no substitute (a different quantity, ~2.5px off), + // so a backend without font-box metrics must fail loudly. + if (!Number.isFinite(half)) { + throw new Error('fontBoundingBox metrics unavailable; cannot mirror Konva placement'); + } + return half + fontSizePx / 2; +} diff --git a/tests/lib/printLabFont.ts b/tests/lib/printLabFont.ts new file mode 100644 index 00000000..fd678d26 --- /dev/null +++ b/tests/lib/printLabFont.ts @@ -0,0 +1,18 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import { GlobalFonts } from '@napi-rs/canvas'; + +export const PRINTLAB_FONT_FAMILY = 'PrintLab ZPL'; +export const PRINTLAB_FONT_PATH = path.resolve( + process.cwd(), + 'src/assets/fonts/PrintLabZPL-Bold.ttf', +); + +/** Single home for path and family so a font swap cannot leave a gate + * testing the old face. */ +export function registerPrintLabZpl(): void { + if (!fs.existsSync(PRINTLAB_FONT_PATH)) { + throw new Error(`Font file missing at ${PRINTLAB_FONT_PATH}`); + } + GlobalFonts.registerFromPath(PRINTLAB_FONT_PATH, PRINTLAB_FONT_FAMILY); +} diff --git a/tests/scripts/fetch_labelary_default_text_fixtures.ts b/tests/scripts/fetch_labelary_default_text_fixtures.ts index dce40cb1..42b5bf2e 100644 --- a/tests/scripts/fetch_labelary_default_text_fixtures.ts +++ b/tests/scripts/fetch_labelary_default_text_fixtures.ts @@ -12,8 +12,13 @@ import * as fs from 'fs'; import * as path from 'path'; import { textBoxMatchCases } from '../fixtures/textBoxMatchCases'; +import { deviceFontBoxMatchCases } from '../fixtures/deviceFontBoxMatchCases'; +import { font0GlyphCoverageCases } from '../fixtures/font0GlyphCoverageCases'; const FIXTURES_DIR = path.resolve('tests/fixtures/labelary_text_default_images'); +const DEVICE_FIXTURES_DIR = path.resolve( + 'tests/fixtures/labelary_devicefont_images', +); const RENDER_URL = 'http://api.labelary.com/v1/printers/8dpmm/labels/4x4/0/'; const RATE_LIMIT_MS = 1000; @@ -32,30 +37,60 @@ async function fetchLabel(zpl: string): Promise { return Buffer.from(await res.arrayBuffer()); } +interface FetchJob { + dir: string; + id: string; + zpl: string; +} + async function main(): Promise { fs.mkdirSync(FIXTURES_DIR, { recursive: true }); + fs.mkdirSync(DEVICE_FIXTURES_DIR, { recursive: true }); - const missing = textBoxMatchCases.filter( - (tc) => !fs.existsSync(path.join(FIXTURES_DIR, `${tc.id}.png`)), - ); + const jobs: FetchJob[] = [ + ...[...textBoxMatchCases, ...font0GlyphCoverageCases].map((tc) => ({ + dir: FIXTURES_DIR, + id: tc.id, + zpl: + `^XA^FO${tc.x},${tc.y}` + + `^A0${tc.rotation},${tc.fontHeight},${tc.fontWidth || tc.fontHeight}` + + `^FD${tc.text}^FS^XZ`, + })), + // Device fonts: width stays 0 so the firmware derives it from the + // cell matrix, matching what deviceFontMetrics does locally. + ...deviceFontBoxMatchCases.map((tc) => ({ + dir: DEVICE_FIXTURES_DIR, + id: tc.id, + zpl: + `^XA^FO${tc.x},${tc.y}` + + `^A${tc.fontId}${tc.rotation},${tc.fontHeight},${tc.fontWidth || ''}` + + `^FD${tc.text}^FS^XZ`, + })), + ]; + + // The case lists share ids on purpose (see fixtureIdContract). + const byFile = new Map(); + for (const job of jobs) { + const file = path.join(job.dir, `${job.id}.png`); + if (!byFile.has(file)) byFile.set(file, job); + } + const missing = [...byFile.entries()] + .filter(([file]) => !fs.existsSync(file)) + .map(([, job]) => job); if (missing.length === 0) { console.log('All fixtures already present.'); return; } - console.log(`Fetching ${missing.length} fixture(s) with default Zebra font...`); + console.log(`Fetching ${missing.length} fixture(s) from Labelary...`); - for (const tc of missing) { - const zpl = - `^XA^FO${tc.x},${tc.y}` + - `^A0${tc.rotation},${tc.fontHeight},${tc.fontWidth || tc.fontHeight}` + - `^FD${tc.text}^FS^XZ`; - const png = await fetchLabel(zpl); - fs.writeFileSync(path.join(FIXTURES_DIR, `${tc.id}.png`), png); - console.log(` ${tc.id}.png`); + for (const job of missing) { + const png = await fetchLabel(job.zpl); + fs.writeFileSync(path.join(job.dir, `${job.id}.png`), png); + console.log(` ${job.id}.png`); await new Promise((r) => setTimeout(r, RATE_LIMIT_MS)); } - console.log(`\nWrote ${missing.length} fixture(s) to ${FIXTURES_DIR}`); + console.log(`\nWrote ${missing.length} fixture(s).`); } main().catch((e: unknown) => { diff --git a/tests/scripts/fetch_labelary_text_fixtures.ts b/tests/scripts/fetch_labelary_text_fixtures.ts index 5254fdb9..206ca199 100644 --- a/tests/scripts/fetch_labelary_text_fixtures.ts +++ b/tests/scripts/fetch_labelary_text_fixtures.ts @@ -25,19 +25,18 @@ import * as fs from 'fs'; import * as path from 'path'; import { textTestCases } from '../fixtures/textTestCases'; - -const FONT_PATH = path.resolve('src/assets/fonts/PrintLabZPL-Bold.ttf'); +import { PRINTLAB_FONT_PATH } from '../lib/printLabFont'; const FIXTURES_DIR = path.resolve('tests/fixtures/labelary_text_images'); const FONT_UPLOAD_URL = 'http://api.labelary.com/v1/fonts'; const RENDER_URL = 'http://api.labelary.com/v1/printers/8dpmm/labels/4x4/0/'; const RATE_LIMIT_MS = 1000; async function uploadFont(): Promise { - const file = fs.readFileSync(FONT_PATH); + const file = fs.readFileSync(PRINTLAB_FONT_PATH); const form = new FormData(); form.append('file', new Blob([file]), 'PrintLabZPL.ttf'); form.append('path', 'R:PRINTLAB.TTF'); - console.log(`Uploading ${FONT_PATH} (${file.length} bytes) to Labelary...`); + console.log(`Uploading ${PRINTLAB_FONT_PATH} (${file.length} bytes) to Labelary...`); const res = await fetch(FONT_UPLOAD_URL, { method: 'POST', body: form }); if (!res.ok) { throw new Error(`Font upload ${res.status}: ${await res.text()}`); diff --git a/tests/scripts/report_font0_glyph_drift.ts b/tests/scripts/report_font0_glyph_drift.ts new file mode 100644 index 00000000..9f2a8072 --- /dev/null +++ b/tests/scripts/report_font0_glyph_drift.ts @@ -0,0 +1,125 @@ +/* + * Per-glyph ink drift report: PrintLab ZPL (our Font-0 substitute) vs + * Labelary's default CG Triumvirate render, over the full coverage set + * (tests/fixtures/font0GlyphCoverageCases.ts). + * + * Decision tool for font-pipeline work: advance widths are already baked + * into the TTF, so what this surfaces is glyph SHAPE drift. + * + * Run: pnpm dlx tsx tests/scripts/report_font0_glyph_drift.ts + * (fetch fixtures first via fetch_labelary_default_text_fixtures.ts) + */ +import * as fs from 'fs'; +import * as path from 'path'; +import { PNG } from 'pngjs'; +import { zplAnchorToModel } from '@zplab/core/lib/labelGeometry/textPositionTransforms'; +import { font0GlyphCoverageCases } from '../fixtures/font0GlyphCoverageCases'; +import { darkBBox } from '../lib/darkBBox'; +import { drawKonvaText } from '../lib/drawKonvaText'; +import { inkCanvas } from '../lib/inkCanvas'; +import { + PRINTLAB_FONT_FAMILY, + registerPrintLabZpl, +} from '../lib/printLabFont'; + +const FIXTURES_DIR = path.resolve('tests/fixtures/labelary_text_default_images'); + +// Production placement: ^FO anchor -> zplAnchorToModel -> Konva line +// placement, so dY reads as real on-canvas position drift, not harness +// offset. +function renderLocal(tc: (typeof font0GlyphCoverageCases)[number]) { + const { canvas, ctx } = inkCanvas(); + const model = zplAnchorToModel( + tc.x, + tc.y, + { fontHeight: tc.fontHeight, rotation: tc.rotation }, + 'FO', + ); + drawKonvaText(ctx, { + text: tc.text, + x: model.x, + y: model.y, + fontSizePx: tc.fontHeight, + fontFamily: PRINTLAB_FONT_FAMILY, + fontStyle: 'bold', + }); + return darkBBox(PNG.sync.read(canvas.toBuffer('image/png'))); +} + +function main(): void { + registerPrintLabZpl(); + + interface Row { + id: string; + char: string; + dW: number; + dH: number; + dY: number; + relW: number; + relH: number; + local: ReturnType; + labelary: ReturnType; + } + const rows: Row[] = []; + const missing: string[] = []; + + for (const tc of font0GlyphCoverageCases) { + const fixture = path.join(FIXTURES_DIR, `${tc.id}.png`); + if (!fs.existsSync(fixture)) { + missing.push(tc.id); + continue; + } + const labelary = darkBBox(PNG.sync.read(fs.readFileSync(fixture))); + const local = renderLocal(tc); + rows.push({ + id: tc.id, + char: tc.text[0] ?? '', + dW: local.width - labelary.width, + dH: local.height - labelary.height, + dY: local.y - labelary.y, + relW: labelary.width > 0 ? (local.width - labelary.width) / labelary.width : 0, + relH: labelary.height > 0 ? (local.height - labelary.height) / labelary.height : 0, + local, + labelary, + }); + } + + rows.sort((a, b) => Math.abs(b.relW) - Math.abs(a.relW)); + + console.log('char local WxH labelary WxH dW dH dY relW% relH%'); + for (const r of rows) { + const flag = Math.abs(r.dW) > 6 || Math.abs(r.dH) > 6 || Math.abs(r.dY) > 6 ? ' <<<' : ''; + console.log( + `${r.char.padEnd(5)} ${`${r.local.width}x${r.local.height}`.padEnd(11)} ` + + `${`${r.labelary.width}x${r.labelary.height}`.padEnd(14)} ` + + `${String(r.dW).padStart(3)} ${String(r.dH).padStart(3)} ` + + `${String(r.dY).padStart(3)} ` + + `${(r.relW * 100).toFixed(1).padStart(6)} ${(r.relH * 100).toFixed(1).padStart(6)}${flag}`, + ); + } + + const outliers = rows.filter( + (r) => Math.abs(r.dW) > 6 || Math.abs(r.dH) > 6 || Math.abs(r.dY) > 6, + ); + const moderate = rows.filter( + (r) => + !outliers.includes(r) && + (Math.abs(r.dW) > 3 || Math.abs(r.dH) > 3 || Math.abs(r.dY) > 3), + ); + console.log(`\n${rows.length} glyphs measured at h=50 (char x3).`); + console.log( + `Outliers >6 dots (box-match tolerance): ${outliers.length}` + + (outliers.length ? ` -> ${outliers.map((r) => r.char).join(' ')}` : ''), + ); + console.log( + `Moderate 4-6 dots: ${moderate.length}` + + (moderate.length ? ` -> ${moderate.map((r) => r.char).join(' ')}` : ''), + ); + if (missing.length) { + console.log( + `\nWARNING: ${missing.length} fixture(s) missing, run the fetch script: ${missing.join(', ')}`, + ); + } +} + +main();