Skip to content

Commit e76b148

Browse files
committed
feat(setup): printer-native font downloads replay as sent from the profile
1 parent 3b751d8 commit e76b148

52 files changed

Lines changed: 555 additions & 252 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎README.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ On desktop, **File → Settings… → App → MCP** starts a local MCP server,
186186
## Coverage
187187

188188
<!-- coverage:start (generated from the command catalog by scripts/gen-coverage.mjs; run `pnpm coverage:gen`) -->
189-
120 of the 225 ZPL II commands are modelled in the browser; desktop covers 2 more with a connected printer. 2 more are planned for both builds. 79 need a connected printer and are planned for desktop. The source editor checks parameters for 1 command. See per-command coverage: [docs/zpl-coverage.md](docs/zpl-coverage.md).
189+
125 of the 225 ZPL II commands are modelled in the browser; desktop covers 2 more with a connected printer. 2 more are planned for both builds. 74 need a connected printer and are planned for desktop. The source editor checks parameters for 1 command. See per-command coverage: [docs/zpl-coverage.md](docs/zpl-coverage.md).
190190

191191
| Area | Modelled |
192192
|---|---|
@@ -200,7 +200,7 @@ On desktop, **File → Settings… → App → MCP** starts a local MCP server,
200200
| Identity & access | 2 / 2 |
201201
| Graphics | 10 / 14 |
202202
| Media & feed | 9 / 9 |
203-
| Text & fonts | 9 / 15 |
203+
| Text & fonts | 14 / 15 |
204204
| Print quality | 10 / 18 |
205205
| Configuration & persistence | 4 / 5 |
206206
| Hardware / Host comm / RFID / Network | 4 / 89 |

‎docs/zpl-coverage.md‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -80,11 +80,11 @@ never share. The row total is pinned in `packages/core/src/catalog/catalog.test.
8080
| `^CO` | font cache size | `[x]` | `[x]` | `[ ]` |
8181
| `^FL` | font linking | `[x]` | `[x]` | `[ ]` |
8282
| `^LF` | list font links | `[ ]` | `[~]` | `[ ]` |
83-
| `~DB` | download bitmap font | `[ ]` | `[~]` | `[ ]` |
84-
| `~DS` | download scalable font | `[ ]` | `[~]` | `[ ]` |
85-
| `~DT` | download TrueType font | `[ ]` | `[~]` | `[ ]` |
86-
| `~DU` | download unbounded TrueType | `[ ]` | `[~]` | `[ ]` |
87-
| `~DE` | download encoding | `[ ]` | `[~]` | `[ ]` |
83+
| `~DB` | download bitmap font | `[x]` | `[x]` | `[ ]` |
84+
| `~DS` | download scalable font | `[x]` | `[x]` | `[ ]` |
85+
| `~DT` | download TrueType font | `[x]` | `[x]` | `[ ]` |
86+
| `~DU` | download unbounded TrueType | `[x]` | `[x]` | `[ ]` |
87+
| `~DE` | download encoding | `[x]` | `[x]` | `[ ]` |
8888

8989
## Barcodes
9090

‎packages/core/src/catalog/commands.json‎

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -663,8 +663,8 @@
663663
"title": "download bitmap font",
664664
"summary": "Downloads a bitmap font and its metrics into printer memory.",
665665
"support": {
666-
"web": "no",
667-
"desktop": "planned",
666+
"web": "yes",
667+
"desktop": "yes",
668668
"lint": "no"
669669
}
670670
},
@@ -677,8 +677,8 @@
677677
"title": "download scalable font",
678678
"summary": "Downloads an encoded scalable Intellifont into printer memory.",
679679
"support": {
680-
"web": "no",
681-
"desktop": "planned",
680+
"web": "yes",
681+
"desktop": "yes",
682682
"lint": "no"
683683
}
684684
},
@@ -691,8 +691,8 @@
691691
"title": "download TrueType font",
692692
"summary": "Downloads an encoded TrueType font with up to 256 characters into printer memory.",
693693
"support": {
694-
"web": "no",
695-
"desktop": "planned",
694+
"web": "yes",
695+
"desktop": "yes",
696696
"lint": "no"
697697
}
698698
},
@@ -705,8 +705,8 @@
705705
"title": "download unbounded TrueType",
706706
"summary": "Downloads an encoded TrueType font with more than 256 glyphs into printer memory.",
707707
"support": {
708-
"web": "no",
709-
"desktop": "planned",
708+
"web": "yes",
709+
"desktop": "yes",
710710
"lint": "no"
711711
}
712712
},
@@ -719,8 +719,8 @@
719719
"title": "download encoding",
720720
"summary": "Downloads an encoding table that maps field-data bytes to Unicode values for TrueType fonts.",
721721
"support": {
722-
"web": "no",
723-
"desktop": "planned",
722+
"web": "yes",
723+
"desktop": "yes",
724724
"lint": "no"
725725
}
726726
},

‎packages/core/src/catalog/schema.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ const SUPPORT_LEVELS = ["yes", "planned", "no"] as const;
66
export type SupportLevel = (typeof SUPPORT_LEVELS)[number];
77

88
/** What a partial import drops; the app maps each cause to its report wording. */
9-
const IMPORT_LOSS_CAUSES = ["fontFace", "printerComms", "gfRawBinary", "qrFdMode", "storedGraphic", "shortPayload", "recallMagnification", "checksumMismatch", "oversizeUpload", "unshippableUpload", "bitmapFont", "scalableFont", "unshippableFontName", "unshippableGraphicName", "fontNameNotTrueType", "fontVersionReplaced", "fnPartialInsert", "recallFormat", "recallSlot", "recallSettings"] as const;
9+
const IMPORT_LOSS_CAUSES = ["fontFace", "printerComms", "gfRawBinary", "qrFdMode", "storedGraphic", "shortPayload", "recallMagnification", "checksumMismatch", "oversizeUpload", "oversizeFontUpload", "unreplayableUpload", "cachedFaceDropped", "unshippableUpload", "unshippableFontName", "unshippableGraphicName", "fontVersionReplaced", "fnPartialInsert", "recallFormat", "recallSlot", "recallSettings"] as const;
1010
export type ImportLossCause = (typeof IMPORT_LOSS_CAUSES)[number];
1111

1212
/** Support per axis; docs/zpl-coverage.md defines the three. */

‎packages/core/src/lib/zplCanonicalPrefixes.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { applyPrefixRemap, PAYLOAD_CMDS, tokenize, type TokenizerChars } from "./zplParser/helpers";
22

33
/** Image and font payloads carry any byte, hex or binary, so their lines stay as typed. */
4-
const BYTE_PAYLOAD_CMDS = new Set(["GF", "DY", "DG", "DT", "DU", "DS", "DB"]);
4+
const BYTE_PAYLOAD_CMDS = new Set(["GF", "DY", "DG", "DT", "DU", "DS", "DB", "DE"]);
55

66
/** The same bytes with ^CC/^CT/^CD undone: every live prefix back to ^ or ~ and every
77
* parameter delimiter to a comma, data left alone, so texts compare across a remap.

‎packages/core/src/lib/zplImportService.ts‎

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { newId } from "./ids";
99
import type { ImportLossCause } from "../catalog";
1010
import { renameTemplateMarkers } from "./fnTemplate";
1111
import { PER_FORMAT_ZPL_FIELDS, effectiveDpmm, type CustomFontMapping, type JmDensity, type LabelConfig } from "../types/LabelConfig";
12-
import { isSetupPath, type PrinterProfile, type SetupGraphic } from "../types/PrinterProfile";
12+
import { isSetupDownload, isSetupPath, SETUP_UPLOAD_MAX_CHARS, type PrinterProfile, type SetupGraphic } from "../types/PrinterProfile";
1313
import { exportableLeaves, type LabelObject, type Page, isGroup } from "../types/Group";
1414
import { nextFreeFnNumber, uniqueVariableName, type ColumnMapping, type Variable } from "../types/Variable";
1515
import type { DatasetInput } from "../types/DataSource";
@@ -46,11 +46,16 @@ export interface ZplImportResult {
4646

4747
const FONT_LOSS = {
4848
versionReplaced: "fontVersionReplaced",
49-
bitmapFont: "bitmapFont",
50-
scalableFont: "scalableFont",
51-
notTrueTypeName: "fontNameNotTrueType",
49+
faceDropped: "cachedFaceDropped",
5250
} as const satisfies Record<FontLossReason, ImportLossCause>;
5351

52+
function fontRefusal(f: { path: string; download?: string }): ImportLossCause | undefined {
53+
if (!isSetupPath(f.path)) return "unshippableFontName";
54+
if (f.download === undefined) return undefined;
55+
if (f.download.length > SETUP_UPLOAD_MAX_CHARS) return "oversizeFontUpload";
56+
return isSetupDownload(f.download) ? undefined : "unreplayableUpload";
57+
}
58+
5459
/** Document findings have no page, so the parser's partials map cannot dedup them. */
5560
function noteDocumentLoss(findings: ImportFinding[], command: string, loss: ImportLossCause, span?: SourceSpan): void {
5661
if (findings.some((f) => f.pageIndex === DOCUMENT_FINDING && f.command === command && f.loss === loss)) return;
@@ -305,20 +310,18 @@ function assembleImport(r: ParsedZPL, dpmm: number): ZplImportResult {
305310
}
306311
: undefined;
307312

308-
const uploadedUnique = [...new Set(r.uploadedFontPaths)];
309313
const printerProfile: Partial<PrinterProfile> = { ...r.printerProfile };
310314
// A design font claims only the upload it ships. An upload it merely references stays provisioning.
311-
const setupFontPaths = uploadedUnique.filter((p) => !r.embeddedFontPaths.has(p));
315+
const setupFonts = r.uploadedFonts.filter((f) => !r.embeddedFontPaths.has(f.path));
312316
for (const f of r.fontLosses) noteDocumentLoss(findings, f.command, FONT_LOSS[f.reason], f.span);
313317
// Refused here with a finding, or the whole profile patch fails and every field it carried drops in silence.
314-
const carriedFontPaths = setupFontPaths.filter((path) => {
315-
if (isSetupPath(path)) return true;
316-
noteDocumentLoss(findings, "~DY", "unshippableFontName");
317-
return false;
318+
const carriedFonts = setupFonts.flatMap(({ via, ...f }) => {
319+
const refusal = fontRefusal(f);
320+
if (refusal === undefined) return [f];
321+
noteDocumentLoss(findings, via, refusal);
322+
return [];
318323
});
319-
if (carriedFontPaths.length > 0) {
320-
printerProfile.setupFonts = carriedFontPaths.map((path) => ({ path }));
321-
}
324+
if (carriedFonts.length > 0) printerProfile.setupFonts = carriedFonts;
322325
const shipped = new Set(
323326
r.pages.flatMap((page) => exportableLeaves(page.objects)).flatMap((leaf) => {
324327
const props = leaf.props as ImageProps;

‎packages/core/src/lib/zplParser.ts‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ export const DEVICE_ACTION_IDS: ReadonlySet<string> = new Set([
169169
* in-span they make single-field regen unsafe (the span replace drops the
170170
* definition). ^BY absent: consumers self-flag via sawBareBarcode; ^JM is
171171
* legal pre-^FS, so it can sit in a field span too. */
172-
const PERSISTENT_DEF_CODES = new Set(["CF", "FW", "CW", "SO", "LH", "LT", "JM", "DG", "DY", "DT", "DU", "DS", "DB"]);
172+
const PERSISTENT_DEF_CODES = new Set(["CF", "FW", "CW", "SO", "LH", "LT", "JM", "DG", "DY", "DT", "DU", "DS", "DB", "DE"]);
173173

174174
/** Why this page cannot be regenerated byte for byte, or undefined when it can.
175175
* The overlay flag and the user-facing wording read the same derivation, so a
@@ -918,8 +918,9 @@ export function parseZPL(
918918
labelConfig.heightMm = labelMeta.value.heightMm;
919919
}
920920

921-
const liveFontPaths = resolveLiveFonts(s);
922-
const bound = bindFontEmbeds(labelConfig.customFonts, liveFontPaths);
921+
const liveFonts = resolveLiveFonts(s);
922+
// A job ships a face from the cache only, so a replayed download stays provisioning.
923+
const bound = bindFontEmbeds(labelConfig.customFonts, liveFonts.flatMap((f) => (f.download === undefined ? [f.path] : [])));
923924
if (bound.fonts) labelConfig.customFonts = bound.fonts;
924925
const embeddedFontPaths = bound.embedded;
925926

@@ -930,7 +931,7 @@ export function parseZPL(
930931
labelConfig,
931932
printerProfile,
932933
decodedImages: s.result.decodedImages,
933-
uploadedFontPaths: liveFontPaths,
934+
uploadedFonts: liveFonts,
934935
embeddedFontPaths,
935936
fontLosses: [...s.fonts.fontLosses],
936937
uploadedGraphics: [...s.fonts.downloadedGraphics].map(([path, g]) => ({ path, gfa: g.gfaCache, via: g.via })),

‎packages/core/src/lib/zplParser/context.ts‎

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import type { ImportLossCause } from "../../catalog/schema";
55
import type { LabelConfig } from "../../types/LabelConfig";
66
import type { CachedImage } from "../imageCache";
77
import type { DecodedGraphic } from "./types";
8-
import type { PrinterProfile } from "../../types/PrinterProfile";
8+
import type { PrinterProfile, SetupFont } from "../../types/PrinterProfile";
9+
import { isTrueTypeFileName } from "../customFonts";
910
import type { Variable } from "../../types/Variable";
1011
import type { TextProps } from "../../registry/text";
1112
import type { SerialMode } from "../../registry/serialField";
@@ -206,9 +207,16 @@ export interface DownloadedFont {
206207
digest: string;
207208
status: DownloadedFontStatus;
208209
/** From the download command or the ~DY format code, the only places the stream states it. */
209-
kind: "truetype" | "bitmap" | "scalable";
210+
kind: "truetype" | "bitmap" | "scalable" | "encoding";
211+
/** The download command, for the findings the profile boundary raises. */
212+
via: string;
213+
/** The download rebuilt from its parsed header and payload, kept when the cache holds no face the emitter could rebuild it from. */
214+
download?: string;
210215
}
211216

217+
/** The cache serves a TrueType face only, and only under a name ^A@ accepts, spec p.63. */
218+
export const cacheServes = (kind: DownloadedFont["kind"], path: string): boolean => kind === "truetype" && isTrueTypeFileName(path);
219+
212220
/** ^CW aliases + ~DY uploads; span the whole parse across ^XA blocks. */
213221
export interface FontsState {
214222
aliases: Map<string, string>;
@@ -420,15 +428,14 @@ function finalFontRefs(s: ParserState): string[] {
420428

421429
/** The uploads the finished design can still ship.
422430
* A ^ID drops an upload unless the design named it before the delete and still names it now. */
423-
export function resolveLiveFonts(s: ParserState): string[] {
431+
export function resolveLiveFonts(s: ParserState): (SetupFont & { via: string })[] {
424432
const refs = finalFontRefs(s);
425-
const live: string[] = [];
433+
const live: (SetupFont & { via: string })[] = [];
426434
for (const [path, font] of s.fonts.downloadedFonts) {
427435
if (font.status === "deleted") continue;
428436
if (font.status === "deletedWhileNamed" && !refs.some((r) => storageRefMatchesPath(r, path))) continue;
429-
// A printer-native font has no web face and no ~DY format this emitter can send.
430-
if (font.kind !== "truetype") continue;
431-
live.push(path);
437+
if (font.download !== undefined) live.push({ path, via: font.via, download: font.download });
438+
else if (cacheServes(font.kind, path)) live.push({ path, via: font.via });
432439
}
433440
return live;
434441
}

0 commit comments

Comments
 (0)