Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions THIRD-PARTY-LICENSES.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,13 @@ 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 CG Triumvirate's rendered
vertical placement.
- Ink spans, advances and accent anchors of individual glyphs across the
printable Latin-1 range remapped to CG Triumvirate's measured metrics.
- Family and PostScript names renamed from "Roboto Condensed" to
"PrintLab ZPL".

Expand Down
9 changes: 4 additions & 5 deletions src/assets/fonts/NOTICE.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,10 @@ Relative to upstream Roboto Condensed:
- 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.
- All outlines shifted down ~0.04 em to match CG Triumvirate's rendered
vertical placement.
- Ink spans, advances and accent anchors of individual glyphs across the
printable Latin-1 range remapped to CG Triumvirate's measured metrics.
- Family and PostScript names renamed from "Roboto Condensed" to
"PrintLab ZPL".

Expand Down
Binary file modified src/assets/fonts/PrintLabZPL-Bold.ttf
Binary file not shown.
Binary file modified src/assets/fonts/PrintLabZPL-Bold.woff2
Binary file not shown.
11 changes: 11 additions & 0 deletions src/test/fixtureIdContract.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { describe, it, expect } from 'vitest';
import * as fs from 'fs';
import * as path from 'path';
import { textBoxMatchCases } from '../../tests/fixtures/textBoxMatchCases';
import { font0GlyphCoverageCases } from '../../tests/fixtures/font0GlyphCoverageCases';

Expand Down Expand Up @@ -29,4 +31,13 @@ describe('fixture id contract', () => {
}
}
});

it('has no orphaned fixture PNGs', () => {
const dir = path.resolve(process.cwd(), 'tests/fixtures/labelary_text_default_images');
const expected = new Set(
[...textBoxMatchCases, ...font0GlyphCoverageCases].map((tc) => `${tc.id}.png`),
);
const orphans = fs.readdirSync(dir).filter((f) => f.endsWith('.png') && !expected.has(f));
expect(orphans, 'unreferenced fixtures').toEqual([]);
});
});
2 changes: 1 addition & 1 deletion src/test/font0PlacementMatch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const H = 50;
// 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'];
const GLYPHS = ['_', '<', '>', '@', '*', '$', '(', ')', '/', 'f', 'j', 'Q', 'p', 'b', '~', '^', '[', 'ß', '©', '°', '§', '×', '–', 'Æ', 'Ç', 'ñ', '5', 'M'];

describe('Font 0 placement match: patched glyphs vs Labelary', () => {
beforeAll(() => {
Expand Down
122 changes: 119 additions & 3 deletions tests/fixtures/charSlug.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/** Case-insensitive filesystems would collide `A` and `a`, so
* lowercase gets an `lc` prefix. */
export const PUNCT_SLUGS: Record<string, string> = {
export const CHAR_SLUGS: Record<string, string> = {
'-': 'minus',
'.': 'dot',
'=': 'eq',
Expand All @@ -24,11 +24,127 @@ export const PUNCT_SLUGS: Record<string, string> = {
"'": 'squote',
'<': 'lt',
'>': 'gt',
'[': 'lbrack',
']': 'rbrack',
'{': 'lbrace',
'}': 'rbrace',
'|': 'pipe',
'~': 'tilde',
'^': 'caret',
'\\': 'bslash',
'`': 'backtick',
'\u00a8': 'Diaeresis',
'\u00af': 'Macron',
'\u00b4': 'AcuteAccent',
'\u00b8': 'CedillaAccent',
'ä': 'lcAE',
'ö': 'lcOE',
'ü': 'lcUE',
'Ä': 'AE',
'Ö': 'OE',
'Ü': 'UE',
'ß': 'ss',
'À': 'AWithGrave',
'Á': 'AWithAcute',
'Â': 'AWithCircumflex',
'Ã': 'AWithTilde',
'Å': 'AWithRingAbove',
'Æ': 'AeLig',
'Ç': 'CWithCedilla',
'È': 'EWithGrave',
'É': 'EWithAcute',
'Ê': 'EWithCircumflex',
'Ë': 'EWithDiaeresis',
'Ì': 'IWithGrave',
'Í': 'IWithAcute',
'Î': 'IWithCircumflex',
'Ï': 'IWithDiaeresis',
'Ð': 'Eth',
'Ñ': 'NWithTilde',
'Ò': 'OWithGrave',
'Ó': 'OWithAcute',
'Ô': 'OWithCircumflex',
'Õ': 'OWithTilde',
'Ø': 'Ostroke',
'Ù': 'UWithGrave',
'Ú': 'UWithAcute',
'Û': 'UWithCircumflex',
'Ý': 'YWithAcute',
'Þ': 'Thorn',
'à': 'lcAWithGrave',
'á': 'lcAWithAcute',
'â': 'lcAWithCircumflex',
'ã': 'lcAWithTilde',
'å': 'lcAWithRingAbove',
'æ': 'lcAeLig',
'ç': 'lcCWithCedilla',
'è': 'lcEWithGrave',
'é': 'lcEWithAcute',
'ê': 'lcEWithCircumflex',
'ë': 'lcEWithDiaeresis',
'ì': 'lcIWithGrave',
'í': 'lcIWithAcute',
'î': 'lcIWithCircumflex',
'ï': 'lcIWithDiaeresis',
'ð': 'lcEth',
'ñ': 'lcNWithTilde',
'ò': 'lcOWithGrave',
'ó': 'lcOWithAcute',
'ô': 'lcOWithCircumflex',
'õ': 'lcOWithTilde',
'ø': 'lcOstroke',
'ù': 'lcUWithGrave',
'ú': 'lcUWithAcute',
'û': 'lcUWithCircumflex',
'ý': 'lcYWithAcute',
'þ': 'lcThorn',
'ÿ': 'lcYWithDiaeresis',
'¡': 'InvertedExclamation',
'¢': 'Cent',
'£': 'Pound',
'¤': 'Currency',
'¥': 'Yen',
'¦': 'BrokenBar',
'§': 'Section',
'©': 'Copyright',
'ª': 'lcFeminineOrdinalIndicator',
'«': 'LeftPointingDoubleAngleQuotation',
'¬': 'Not',
'®': 'Registered',
'°': 'Degree',
'±': 'PlusMinus',
'²': 'SuperscriptTwo',
'³': 'SuperscriptThree',
'µ': 'lcMicro',
'¶': 'Pilcrow',
'·': 'MiddleDot',
'¹': 'SuperscriptOne',
'º': 'lcMasculineOrdinalIndicator',
'»': 'RightPointingDoubleAngleQuotation',
'¼': 'VulgarFractionOneQuarter',
'½': 'VulgarFractionOneHalf',
'¾': 'VulgarFractionThreeQuarters',
'¿': 'InvertedQuestion',
'×': 'Multiplication',
'÷': 'Division',
'€': 'Euro',
'™': 'Trade',
'•': 'Bullet',
'…': 'HorizontalEllipsis',
'–': 'EnDash',
'—': 'EmDash',
'\u201a': 'SingleLow9Quotation',
'\u2018': 'LeftSingleQuotation',
'\u2019': 'RightSingleQuotation',
'\u201e': 'DoubleLow9Quotation',
'\u201c': 'LeftDoubleQuotation',
'\u201d': 'RightDoubleQuotation',
'‰': 'PerMille',
};

export function charSlug(c: string): string {
const punct = PUNCT_SLUGS[c];
if (punct) return punct;
const slug = CHAR_SLUGS[c];
if (slug) return slug;
if (c >= 'a' && c <= 'z') return `lc${c.toUpperCase()}`;
return c;
}
31 changes: 24 additions & 7 deletions tests/fixtures/font0GlyphCoverageCases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,31 @@
* 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';
import { CHAR_SLUGS, charSlug } from './charSlug';

const ALL_CHARS = [
...'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
...'abcdefghijklmnopqrstuvwxyz',
...'0123456789',
...Object.keys(PUNCT_SLUGS),
];
export const UNSUPPORTED_CHARS = ['\u212e'];

/** Residuals the calibration deliberately leaves: <=3 dots of accent
* ink on the I family and fraction/ellipsis inner spacing, plus the
* spacing accents, whose shared outlines cannot move to Zebra's
* standalone positions without breaking the accented composites. */
export const KNOWN_DRIFT = [...'ÍÎÏÌìíîïýÿðøÙÚÛÜ«²³¹¼½¾…`\u00b4\u00b8\u2018\u2019\u201c\u201d'];

/** Ink-less space, NBSP and soft hyphen excluded; a missing slug
* throws instead of silently dropping coverage. */
const LATIN1_PRINTABLE: string[] = [];
for (let c = 0x21; c <= 0x7e; c++) LATIN1_PRINTABLE.push(String.fromCharCode(c));
for (let c = 0xa1; c <= 0xff; c++) {
if (c !== 0xad) LATIN1_PRINTABLE.push(String.fromCharCode(c));
}
const SPECIALS = [...'\u20ac\u2122\u2022\u2026\u2013\u2014\u201a\u2018\u2019\u201e\u201c\u201d\u2030'];

const ALL_CHARS = [...LATIN1_PRINTABLE, ...SPECIALS];
for (const c of ALL_CHARS) {
if (!/[0-9A-Za-z]/.test(c) && !(c in CHAR_SLUGS)) {
throw new Error(`no fixture slug for ${c} (U+${c.codePointAt(0)!.toString(16)})`);

Check warning on line 27 in tests/fixtures/font0GlyphCoverageCases.ts

View workflow job for this annotation

GitHub Actions / lint-test-build

Forbidden non-null assertion
}
}

const SIZE = 50;

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/fixtures/labelary_text_images/h20_n_num.png
Binary file modified tests/fixtures/labelary_text_images/h30_n_alpha.png
Binary file modified tests/fixtures/labelary_text_images/h30_n_date.png
Binary file modified tests/fixtures/labelary_text_images/h30_n_decimal.png
Binary file modified tests/fixtures/labelary_text_images/h30_n_num.png
Binary file modified tests/fixtures/labelary_text_images/h39_n_long.png
Binary file modified tests/fixtures/labelary_text_images/h50_b_mix.png
Binary file modified tests/fixtures/labelary_text_images/h50_i_mix.png
Binary file modified tests/fixtures/labelary_text_images/h50_n_num.png
Binary file modified tests/fixtures/labelary_text_images/h50_r_mix.png
Binary file modified tests/fixtures/labelary_text_images/h80_n_alpha.png
Binary file modified tests/fixtures/labelary_text_images/h80_n_num.png
35 changes: 27 additions & 8 deletions tests/scripts/fetch_labelary_default_text_fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,19 +43,38 @@ interface FetchJob {
zpl: string;
}

/** ^ and ~ terminate ^FD, so such payloads go through ^FH hex escapes;
* non-ASCII needs ^CI28 (UTF-8). */
function fdFor(text: string): { ci: string; fd: string } {
const ci = /[^\x20-\x7e]/.test(text) ? '^CI28' : '';
if (!/[\^~\\_]/.test(text)) return { ci, fd: `^FD${text}` };
const hex = [...text]
.map((c) => {
const b = Buffer.from(c, 'utf8');
return /[\^~\\_]/.test(c)
? [...b].map((x) => `_${x.toString(16).padStart(2, '0')}`).join('')
: c;
})
.join('');
return { ci, fd: `^FH^FD${hex}` };
}

async function main(): Promise<void> {
fs.mkdirSync(FIXTURES_DIR, { recursive: true });
fs.mkdirSync(DEVICE_FIXTURES_DIR, { recursive: true });

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`,
})),
...[...textBoxMatchCases, ...font0GlyphCoverageCases].map((tc) => {
const { ci, fd } = fdFor(tc.text);
return {
dir: FIXTURES_DIR,
id: tc.id,
zpl:
`^XA${ci}^FO${tc.x},${tc.y}` +
`^A0${tc.rotation},${tc.fontHeight},${tc.fontWidth || tc.fontHeight}` +
`${fd}^FS^XZ`,
};
}),
// Device fonts: width stays 0 so the firmware derives it from the
// cell matrix, matching what deviceFontMetrics does locally.
...deviceFontBoxMatchCases.map((tc) => ({
Expand Down
8 changes: 6 additions & 2 deletions tests/scripts/report_font0_glyph_drift.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ 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 { font0GlyphCoverageCases, KNOWN_DRIFT } from '../fixtures/font0GlyphCoverageCases';
import { darkBBox } from '../lib/darkBBox';
import { drawKonvaText } from '../lib/drawKonvaText';
import { inkCanvas } from '../lib/inkCanvas';
Expand Down Expand Up @@ -98,12 +98,16 @@ function main(): void {
);
}

const known = new Set(KNOWN_DRIFT);
const outliers = rows.filter(
(r) => Math.abs(r.dW) > 6 || Math.abs(r.dH) > 6 || Math.abs(r.dY) > 6,
(r) =>
!known.has(r.char) &&
(Math.abs(r.dW) > 6 || Math.abs(r.dH) > 6 || Math.abs(r.dY) > 6),
);
const moderate = rows.filter(
(r) =>
!outliers.includes(r) &&
!known.has(r.char) &&
(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).`);
Expand Down
Loading