From f696496f6883984fa9cfcb36bf885c1c80690e6f Mon Sep 17 00:00:00 2001 From: u8array Date: Tue, 19 May 2026 22:45:12 +0200 Subject: [PATCH 1/6] feat(zpl): add ^CW custom font mapping with UI editor Adds a ^CW alias->path table to the label config. Each entry registers a single-character alias [A-Z0-9] that ^A{alias} fields can reference instead of the verbose ^A@...E:font.TTF form. Generator emits one ^CW per non-empty mapping after the geometry/ default-font block. Parser persists mappings to labelConfig and resolves ^A{alias} field references to the corresponding font path via a runtime alias map. UI lives in its own Custom Fonts collapsible section (default closed). Each row has a single-char alias input, a path input, and a delete button arranged in a CSS grid (fixed + flexible + auto) so the path column does not collapse. The alias input filters to [A-Z0-9] while typing; duplicates are flagged with a red border and aria-invalid plus a tooltip. Rows with both fields empty auto-remove on blur. Path suggestions come from a datalist that lists the standard Zebra drive prefixes (E:, R:, A:, B:) plus E:{name} for every font uploaded via the Fonts tab. A short, plain-language InfoIcon tooltip explains the section purpose without using ZPL syntax. Default-text-style Font input now suggests the union of the Zebra built-in font letters plus any user-defined custom-font aliases, so a mapped letter is reachable from ^CF as well. --- .../Properties/CustomFontsSection.tsx | 150 ++++++++++++++++++ src/components/Properties/PropertiesPanel.tsx | 21 ++- src/lib/zplGenerator.test.ts | 39 +++++ src/lib/zplGenerator.ts | 12 ++ src/lib/zplParser.test.ts | 17 ++ src/lib/zplParser.ts | 30 +++- src/locales/ar.ts | 8 + src/locales/bg.ts | 8 + src/locales/cs.ts | 8 + src/locales/da.ts | 8 + src/locales/de.ts | 8 + src/locales/el.ts | 8 + src/locales/en.ts | 8 + src/locales/es.ts | 8 + src/locales/et.ts | 8 + src/locales/fa.ts | 8 + src/locales/fi.ts | 8 + src/locales/fr.ts | 8 + src/locales/he.ts | 8 + src/locales/hr.ts | 8 + src/locales/hu.ts | 8 + src/locales/it.ts | 8 + src/locales/ja.ts | 8 + src/locales/ko.ts | 8 + src/locales/lt.ts | 8 + src/locales/lv.ts | 8 + src/locales/nl.ts | 8 + src/locales/no.ts | 8 + src/locales/pl.ts | 8 + src/locales/pt.ts | 8 + src/locales/ro.ts | 8 + src/locales/sk.ts | 8 + src/locales/sl.ts | 8 + src/locales/sr.ts | 8 + src/locales/sv.ts | 8 + src/locales/tr.ts | 8 + src/locales/zh-hans.ts | 8 + src/locales/zh-hant.ts | 8 + src/types/ObjectType.ts | 12 ++ 39 files changed, 534 insertions(+), 3 deletions(-) create mode 100644 src/components/Properties/CustomFontsSection.tsx diff --git a/src/components/Properties/CustomFontsSection.tsx b/src/components/Properties/CustomFontsSection.tsx new file mode 100644 index 00000000..4e95e42a --- /dev/null +++ b/src/components/Properties/CustomFontsSection.tsx @@ -0,0 +1,150 @@ +import { + InformationCircleIcon, + PlusIcon, + TrashIcon, +} from "@heroicons/react/16/solid"; +import { CollapsibleSection } from "../ui/CollapsibleSection"; +import { useT } from "../../lib/useT"; +import { getAllFonts } from "../../lib/fontCache"; +import { useFontCacheVersion } from "../../hooks/useFontCacheVersion"; +import { inputCls } from "./styles"; +import type { CustomFontMapping } from "../../types/ObjectType"; + +const PATHS_DATALIST_ID = "zpl-custom-font-paths"; +const ALIAS_CHAR_RE = /[^A-Z0-9]/g; + +/** Standard Zebra storage drive prefixes. Surfaced as datalist hints so + * users new to ZPL discover the path syntax without reading the spec. + * E = flash, R = volatile RAM, A = removable (PCMCIA/CF), B = optional + * on-board flash. */ +const ZPL_DRIVE_PREFIXES = ["E:", "R:", "A:", "B:"] as const; + +/** Editor for the ^CW alias→path mapping list. The alias input restricts + * to [A-Z0-9] to match the schema regex; empty aliases or paths survive + * in state so users can type at their own pace, but rows that stay empty + * through a blur are auto-removed and the generator skips any that slip + * through at emit time. */ +export function CustomFontsSection({ + mappings, + onChange, +}: { + mappings: CustomFontMapping[]; + onChange: (next: CustomFontMapping[]) => void; +}) { + const t = useT(); + // useFontCacheVersion triggers a re-render whenever the font cache + // changes; the React Compiler handles memoisation downstream. + useFontCacheVersion(); + const uploadedPaths = getAllFonts().map((f) => `E:${f.name}`); + + // Count alias occurrences so duplicates can be flagged inline. Empty + // aliases never count as duplicates of each other. + const aliasCounts = new Map(); + for (const m of mappings) { + if (m.alias) aliasCounts.set(m.alias, (aliasCounts.get(m.alias) ?? 0) + 1); + } + const isDuplicateAlias = (alias: string) => + !!alias && (aliasCounts.get(alias) ?? 0) > 1; + + const updateAt = (i: number, patch: Partial) => { + onChange(mappings.map((m, idx) => (idx === i ? { ...m, ...patch } : m))); + }; + const removeAt = (i: number) => { + onChange(mappings.filter((_, idx) => idx !== i)); + }; + const add = () => { + onChange([...mappings, { alias: "", path: "" }]); + }; + // Auto-remove a row when focus leaves it and both fields are blank. + // Keeps the editor tidy without an explicit "discard" interaction. + const handleRowBlur = (i: number) => { + // Defer to next tick so focus can land on the sibling input first; + // otherwise tabbing alias → path would delete the row mid-traversal. + requestAnimationFrame(() => { + const row = mappings[i]; + if (row && !row.alias && !row.path) removeAt(i); + }); + }; + + return ( + + {t.label.customFontsHeading} + + + } + defaultOpen={false} + > +
+ {mappings.map((m, i) => { + const dup = isDuplicateAlias(m.alias); + return ( +
handleRowBlur(i)} + > + + updateAt(i, { + alias: e.target.value + .toUpperCase() + .replace(ALIAS_CHAR_RE, ""), + }) + } + /> + updateAt(i, { path: e.target.value })} + /> + +
+ ); + })} + + + {ZPL_DRIVE_PREFIXES.map((p) => ( + +
+
+ ); +} diff --git a/src/components/Properties/PropertiesPanel.tsx b/src/components/Properties/PropertiesPanel.tsx index 8321244d..73715c8b 100644 --- a/src/components/Properties/PropertiesPanel.tsx +++ b/src/components/Properties/PropertiesPanel.tsx @@ -1,5 +1,6 @@ import type { RefObject } from "react"; import { InformationCircleIcon, FolderPlusIcon } from "@heroicons/react/16/solid"; +import { CustomFontsSection } from "./CustomFontsSection"; import { useLabelStore, useCurrentObjects } from "../../store/labelStore"; import type { LabelCanvasHandle } from "../Canvas/LabelCanvas"; import type { AlignAxis } from "../../lib/alignment"; @@ -347,6 +348,16 @@ function LabelConfigPanel({ onUpdate({ widthMm: p.widthMm, heightMm: p.heightMm, dpmm: p.dpmm }); }; + // ^CF / ^A suggestions: built-in font letters plus every alias the + // user has registered via ^CW. Set-based dedup keeps user-overridden + // built-ins from appearing twice. + const fontIdOptions = Array.from( + new Set([ + ...ZPL_BUILTIN_FONT_IDS, + ...(label.customFonts?.map((m) => m.alias).filter(Boolean) ?? []), + ]), + ); + return (
@@ -804,12 +815,20 @@ function LabelConfigPanel({
+ + + onUpdate({ customFonts: customFonts.length > 0 ? customFonts : undefined }) + } + /> - {ZPL_BUILTIN_FONT_IDS.map((id) => ( + {fontIdOptions.map((id) => ( ); } + diff --git a/src/lib/zplGenerator.test.ts b/src/lib/zplGenerator.test.ts index 3d87b4fe..0336a328 100644 --- a/src/lib/zplGenerator.test.ts +++ b/src/lib/zplGenerator.test.ts @@ -197,6 +197,45 @@ describe('generateZPL — printer params', () => { expect(zpl).not.toContain('^CFA,'); }); + it('emits one ^CW per custom font mapping', () => { + const zpl = generateZPL( + { + ...BASE_LABEL, + customFonts: [ + { alias: 'M', path: 'E:ARIAL.TTF' }, + { alias: 'B', path: 'E:BOLD.TTF' }, + ], + }, + [], + ); + expect(zpl).toContain('^CWM,E:ARIAL.TTF'); + expect(zpl).toContain('^CWB,E:BOLD.TTF'); + }); + + it('omits ^CW when customFonts is absent or empty', () => { + expect(generateZPL(BASE_LABEL, [])).not.toContain('^CW'); + expect( + generateZPL({ ...BASE_LABEL, customFonts: [] }, []), + ).not.toContain('^CW'); + }); + + it('skips ^CW entries with empty alias or path', () => { + const zpl = generateZPL( + { + ...BASE_LABEL, + customFonts: [ + { alias: '', path: 'E:ORPHAN.TTF' }, + { alias: 'A', path: '' }, + { alias: 'B', path: 'E:OK.TTF' }, + ], + }, + [], + ); + expect(zpl).not.toContain('^CW,'); + expect(zpl).not.toContain('^CWA,\n'); + expect(zpl).toContain('^CWB,E:OK.TTF'); + }); + it('emits ^PM when mirror is set', () => { expect(generateZPL({ ...BASE_LABEL, mirror: 'Y' }, [])).toContain('^PMY'); expect(generateZPL({ ...BASE_LABEL, mirror: 'N' }, [])).toContain('^PMN'); diff --git a/src/lib/zplGenerator.ts b/src/lib/zplGenerator.ts index ed1b7a83..f93924ac 100644 --- a/src/lib/zplGenerator.ts +++ b/src/lib/zplGenerator.ts @@ -64,6 +64,18 @@ export function generateZPL(label: LabelConfig, objects: LabelObject[]): string if (top !== 0) lines.push(`^LT${top}`); if (label.labelShift) lines.push(`^LS${label.labelShift}`); + // Custom font mappings ──────────────────────────────────────────────────── + // ^CW assigns a single-char alias to a font path on the printer's + // storage, so subsequent ^A{alias} fields can reference it without + // restating the full E:font.TTF path. Skip mappings with an empty + // alias or path — these come from in-progress UI rows and would emit + // malformed ^CW lines that the printer drops silently. + if (label.customFonts?.length) { + for (const f of label.customFonts) { + if (f.alias && f.path) lines.push(`^CW${f.alias},${f.path}`); + } + } + // Default font ──────────────────────────────────────────────────────────── // ^CF f,h,w — positional. Empty slots stay empty (^CFA,,20 sets font A // and width 20, leaving height untouched). Trailing empty slots are diff --git a/src/lib/zplParser.test.ts b/src/lib/zplParser.test.ts index 3665acb9..1e76e987 100644 --- a/src/lib/zplParser.test.ts +++ b/src/lib/zplParser.test.ts @@ -631,6 +631,23 @@ describe('parseZPL — printer params', () => { expect(labelConfig.defaultFontWidth).toBe(20); }); + it('parses ^CW mapping and resolves ^A{alias} to the printer font', () => { + const { labelConfig, objects } = parseZPL( + '^XA^CWM,E:ARIAL.TTF^FO10,10^AMN,30,0^FDHi^FS^XZ', + 8, + ); + expect(labelConfig.customFonts).toEqual([ + { alias: 'M', path: 'E:ARIAL.TTF' }, + ]); + expect(objects).toHaveLength(1); + expect(props(objects[0]).printerFontName).toBe('ARIAL.TTF'); + }); + + it('ignores invalid ^CW arguments', () => { + const { labelConfig } = parseZPL('^XA^CW,^XZ', 8); + expect(labelConfig.customFonts).toBeUndefined(); + }); + it('parses ~SD instant darkness', () => { expect(parseZPL('~SD07^XA^XZ', 8).labelConfig.instantDarkness).toBe(7); expect(parseZPL('~SD30^XA^XZ', 8).labelConfig.instantDarkness).toBe(30); diff --git a/src/lib/zplParser.ts b/src/lib/zplParser.ts index 19d3392a..8184daf6 100644 --- a/src/lib/zplParser.ts +++ b/src/lib/zplParser.ts @@ -308,6 +308,11 @@ export function parseZPL(zpl: string, dpmm = 8): ParsedZPL { // ^LT label top (vertical offset applied to all field positions) let ltY = 0; + // ^CW alias→path mappings. Single-character aliases that resolve + // ^A{alias} field references back to the original font path. Built + // as the parser walks the header, consulted on each ^A{X} encounter. + const fontAliases = new Map(); + // ^FH state (field hex indicator) let fhActive = false; let fhDelimiter = "_"; @@ -1286,8 +1291,19 @@ export function parseZPL(zpl: string, dpmm = 8): ParsedZPL { } }, + // ^CW {alias},{path} — register an alias for a printer-resident font. + // Subsequent ^A{alias} fields resolve to {path} via the fontAliases + // map. The mapping is also persisted on labelConfig so the generator + // can re-emit it on round-trip. + CW(p) { + const alias = (p[0] ?? "").trim().toUpperCase(); + const path = (p[1] ?? "").trim(); + if (!/^[A-Z0-9]$/.test(alias) || !path) return; + fontAliases.set(alias, path); + (labelConfig.customFonts ??= []).push({ alias, path }); + }, + // ── Browser-limit: printer-specific features ──────────────────────────── - CW: mkBrowserLimit("CW"), // font identifier — assigns alias to printer-resident font FL: mkBrowserLimit("FL"), // font link — links fonts on printer storage HT: mkBrowserLimit("HT"), // head test — diagnostic for print head LF: mkBrowserLimit("LF"), // list fonts — queries printer for installed fonts @@ -1378,7 +1394,17 @@ export function parseZPL(zpl: string, dpmm = 8): ParsedZPL { textRot = (rest[0] as TextProps["rotation"]) ?? fwRotation; textH = int(p[1], cfHeight || 30); textW = int(p[2], cfWidth || 0); - partialCmds.add(`^${cmd}`); + // Resolve the font letter via the ^CW alias table if known; the + // path is stored verbatim with its drive prefix (e.g. "E:FOO.TTF"), + // so strip the "X:" segment before handing it to the text object. + const aliasPath = fontAliases.get(cmd[1] ?? ""); + if (aliasPath) { + const colonIdx = aliasPath.indexOf(":"); + pendingPrinterFontName = + (colonIdx >= 0 ? aliasPath.slice(colonIdx + 1) : aliasPath) || undefined; + } else { + partialCmds.add(`^${cmd}`); + } continue; } diff --git a/src/locales/ar.ts b/src/locales/ar.ts index 5cbc3888..16a7a746 100644 --- a/src/locales/ar.ts +++ b/src/locales/ar.ts @@ -121,6 +121,14 @@ const ar = { defaultFontId: 'الخط', defaultFontHeight: 'الارتفاع (نقاط)', defaultFontWidth: 'العرض (dots)', + customFontsHeading: 'خطوط مخصصة', + customFontsHint: 'أسماء مستعارة للخطوط المخزنة في الطابعة.', + customFontsAlias: 'المعرف', + customFontsAliasHint: 'حرف واحد: A-Z أو 0-9', + customFontsDuplicateAlias: 'معرف مكرر, يُطبَّق التعيين الأخير فقط.', + customFontsPath: 'ملف الخط (مثال: E:ARIAL.TTF)', + customFontsAdd: 'إضافة تعيين', + customFontsRemove: 'إزالة', }, app: { diff --git a/src/locales/bg.ts b/src/locales/bg.ts index 417cce78..96645d38 100644 --- a/src/locales/bg.ts +++ b/src/locales/bg.ts @@ -121,6 +121,14 @@ const bg = { defaultFontId: 'Шрифт', defaultFontHeight: 'Височина (точки)', defaultFontWidth: 'Ширина (dots)', + customFontsHeading: 'Персонализирани шрифтове', + customFontsHint: 'Псевдоними за шрифтове, съхранени в принтера.', + customFontsAlias: 'ID', + customFontsAliasHint: 'Един знак: A-Z или 0-9', + customFontsDuplicateAlias: 'Дублиран псевдоним, действа само последното съпоставяне.', + customFontsPath: 'Файл с шрифт (напр. E:ARIAL.TTF)', + customFontsAdd: 'Добави съпоставяне', + customFontsRemove: 'Премахни', }, app: { diff --git a/src/locales/cs.ts b/src/locales/cs.ts index 24fc2fd3..3b41723e 100644 --- a/src/locales/cs.ts +++ b/src/locales/cs.ts @@ -121,6 +121,14 @@ const cs = { defaultFontId: 'Písmo', defaultFontHeight: 'Výška (body)', defaultFontWidth: 'Šířka (dots)', + customFontsHeading: 'Vlastní písma', + customFontsHint: 'Aliasy pro písma uložená v tiskárně.', + customFontsAlias: 'ID', + customFontsAliasHint: 'Jeden znak: A-Z nebo 0-9', + customFontsDuplicateAlias: 'Duplicitní alias, uplatní se pouze poslední mapování.', + customFontsPath: 'Soubor písma (např. E:ARIAL.TTF)', + customFontsAdd: 'Přidat mapování', + customFontsRemove: 'Odebrat', }, app: { diff --git a/src/locales/da.ts b/src/locales/da.ts index 8a1183a5..11f9f51b 100644 --- a/src/locales/da.ts +++ b/src/locales/da.ts @@ -121,6 +121,14 @@ const da = { defaultFontId: 'Skrifttype', defaultFontHeight: 'Højde (punkter)', defaultFontWidth: 'Bredde (dots)', + customFontsHeading: 'Tilpassede skrifttyper', + customFontsHint: 'Aliaser til skrifttyper på printeren.', + customFontsAlias: 'ID', + customFontsAliasHint: 'Ét tegn: A-Z eller 0-9', + customFontsDuplicateAlias: 'Dublet alias, kun det sidste mapping gælder.', + customFontsPath: 'Skrifttypefil (f.eks. E:ARIAL.TTF)', + customFontsAdd: 'Tilføj mapping', + customFontsRemove: 'Fjern', }, app: { diff --git a/src/locales/de.ts b/src/locales/de.ts index 5630b156..f68dfc73 100644 --- a/src/locales/de.ts +++ b/src/locales/de.ts @@ -121,6 +121,14 @@ const de = { defaultFontId: 'Schriftart', defaultFontHeight: 'Höhe (Punkte)', defaultFontWidth: 'Breite (dots)', + customFontsHeading: 'Eigene Schriften', + customFontsHint: 'Aliase für Schriften, die auf dem Drucker liegen.', + customFontsAlias: 'ID', + customFontsAliasHint: 'Ein Zeichen: A-Z oder 0-9', + customFontsDuplicateAlias: 'Doppelter Alias, nur das letzte Mapping wird angewendet.', + customFontsPath: 'Schriftdatei (z. B. E:ARIAL.TTF)', + customFontsAdd: 'Mapping hinzufügen', + customFontsRemove: 'Entfernen', }, app: { diff --git a/src/locales/el.ts b/src/locales/el.ts index 972f6fd8..92fa4d44 100644 --- a/src/locales/el.ts +++ b/src/locales/el.ts @@ -121,6 +121,14 @@ const el = { defaultFontId: 'Γραμματοσειρά', defaultFontHeight: 'Ύψος (κουκκίδες)', defaultFontWidth: 'Πλάτος (dots)', + customFontsHeading: 'Προσαρμοσμένες γραμματοσειρές', + customFontsHint: 'Ψευδώνυμα για γραμματοσειρές αποθηκευμένες στον εκτυπωτή.', + customFontsAlias: 'ID', + customFontsAliasHint: 'Ένας χαρακτήρας: A-Z ή 0-9', + customFontsDuplicateAlias: 'Διπλό ψευδώνυμο, εφαρμόζεται μόνο η τελευταία αντιστοίχιση.', + customFontsPath: 'Αρχείο γραμματοσειράς (π.χ. E:ARIAL.TTF)', + customFontsAdd: 'Προσθήκη αντιστοίχισης', + customFontsRemove: 'Αφαίρεση', }, app: { diff --git a/src/locales/en.ts b/src/locales/en.ts index c41c99fd..dfa88205 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -121,6 +121,14 @@ const en = { defaultFontId: 'Font', defaultFontHeight: 'Height (dots)', defaultFontWidth: 'Width (dots)', + customFontsHeading: 'Custom fonts', + customFontsHint: 'Aliases for fonts stored on the printer.', + customFontsAlias: 'ID', + customFontsAliasHint: 'Single character: A-Z or 0-9', + customFontsDuplicateAlias: 'Duplicate alias, only the last mapping takes effect.', + customFontsPath: 'Font file (e.g. E:ARIAL.TTF)', + customFontsAdd: 'Add mapping', + customFontsRemove: 'Remove', }, app: { diff --git a/src/locales/es.ts b/src/locales/es.ts index 8058feb2..17603198 100644 --- a/src/locales/es.ts +++ b/src/locales/es.ts @@ -121,6 +121,14 @@ const es = { defaultFontId: 'Fuente', defaultFontHeight: 'Altura (puntos)', defaultFontWidth: 'Ancho (dots)', + customFontsHeading: 'Fuentes personalizadas', + customFontsHint: 'Alias para las fuentes almacenadas en la impresora.', + customFontsAlias: 'ID', + customFontsAliasHint: 'Un solo carácter: A-Z o 0-9', + customFontsDuplicateAlias: 'Alias duplicado, solo el último mapeo tiene efecto.', + customFontsPath: 'Archivo de fuente (p. ej. E:ARIAL.TTF)', + customFontsAdd: 'Añadir mapeo', + customFontsRemove: 'Eliminar', }, app: { diff --git a/src/locales/et.ts b/src/locales/et.ts index 8cb64a16..2038da2f 100644 --- a/src/locales/et.ts +++ b/src/locales/et.ts @@ -121,6 +121,14 @@ const et = { defaultFontId: 'Font', defaultFontHeight: 'Kõrgus (punktid)', defaultFontWidth: 'Laius (dots)', + customFontsHeading: 'Kohandatud fondid', + customFontsHint: 'Aliased printeris salvestatud fontidele.', + customFontsAlias: 'ID', + customFontsAliasHint: 'Üks märk: A-Z või 0-9', + customFontsDuplicateAlias: 'Korduv alias, kehtib ainult viimane vastendus.', + customFontsPath: 'Fondifail (nt E:ARIAL.TTF)', + customFontsAdd: 'Lisa vastendus', + customFontsRemove: 'Eemalda', }, app: { diff --git a/src/locales/fa.ts b/src/locales/fa.ts index 68d47b57..61ba83d1 100644 --- a/src/locales/fa.ts +++ b/src/locales/fa.ts @@ -121,6 +121,14 @@ const fa = { defaultFontId: 'قلم', defaultFontHeight: 'ارتفاع (نقطه)', defaultFontWidth: 'عرض (dots)', + customFontsHeading: 'فونت‌های سفارشی', + customFontsHint: 'نام‌های مستعار برای فونت‌های ذخیره‌شده در چاپگر.', + customFontsAlias: 'شناسه', + customFontsAliasHint: 'یک حرف: A-Z یا 0-9', + customFontsDuplicateAlias: 'شناسه تکراری, فقط آخرین نگاشت اعمال می‌شود.', + customFontsPath: 'فایل فونت (مثلاً E:ARIAL.TTF)', + customFontsAdd: 'افزودن نگاشت', + customFontsRemove: 'حذف', }, app: { diff --git a/src/locales/fi.ts b/src/locales/fi.ts index 10b1f4f8..55ef746e 100644 --- a/src/locales/fi.ts +++ b/src/locales/fi.ts @@ -121,6 +121,14 @@ const fi = { defaultFontId: 'Fontti', defaultFontHeight: 'Korkeus (pisteet)', defaultFontWidth: 'Leveys (dots)', + customFontsHeading: 'Mukautetut fontit', + customFontsHint: 'Aliakset tulostimeen tallennetuille fonteille.', + customFontsAlias: 'ID', + customFontsAliasHint: 'Yksi merkki: A-Z tai 0-9', + customFontsDuplicateAlias: 'Toistuva alias, vain viimeinen määritys vaikuttaa.', + customFontsPath: 'Fonttitiedosto (esim. E:ARIAL.TTF)', + customFontsAdd: 'Lisää määritys', + customFontsRemove: 'Poista', }, app: { diff --git a/src/locales/fr.ts b/src/locales/fr.ts index a6abdddb..9dbc0b05 100644 --- a/src/locales/fr.ts +++ b/src/locales/fr.ts @@ -121,6 +121,14 @@ const fr = { defaultFontId: 'Police', defaultFontHeight: 'Hauteur (points)', defaultFontWidth: 'Largeur (dots)', + customFontsHeading: 'Polices personnalisées', + customFontsHint: 'Alias pour les polices stockées sur l\'imprimante.', + customFontsAlias: 'ID', + customFontsAliasHint: 'Un seul caractère : A-Z ou 0-9', + customFontsDuplicateAlias: 'Alias en double, seul le dernier mappage est appliqué.', + customFontsPath: 'Fichier de police (ex. E:ARIAL.TTF)', + customFontsAdd: 'Ajouter un mappage', + customFontsRemove: 'Supprimer', }, app: { diff --git a/src/locales/he.ts b/src/locales/he.ts index 423d8d3e..05ffa5b4 100644 --- a/src/locales/he.ts +++ b/src/locales/he.ts @@ -121,6 +121,14 @@ const he = { defaultFontId: 'גופן', defaultFontHeight: 'גובה (נקודות)', defaultFontWidth: 'רוחב (dots)', + customFontsHeading: 'גופנים מותאמים', + customFontsHint: 'כינויים לגופנים השמורים במדפסת.', + customFontsAlias: 'מזהה', + customFontsAliasHint: 'תו אחד: A-Z או 0-9', + customFontsDuplicateAlias: 'כינוי כפול, רק המיפוי האחרון מיושם.', + customFontsPath: 'קובץ גופן (לדוגמה E:ARIAL.TTF)', + customFontsAdd: 'הוסף מיפוי', + customFontsRemove: 'הסר', }, app: { diff --git a/src/locales/hr.ts b/src/locales/hr.ts index 161488aa..b005656d 100644 --- a/src/locales/hr.ts +++ b/src/locales/hr.ts @@ -121,6 +121,14 @@ const hr = { defaultFontId: 'Font', defaultFontHeight: 'Visina (točke)', defaultFontWidth: 'Širina (dots)', + customFontsHeading: 'Prilagođeni fontovi', + customFontsHint: 'Aliasi za fontove pohranjene na pisaču.', + customFontsAlias: 'ID', + customFontsAliasHint: 'Jedan znak: A-Z ili 0-9', + customFontsDuplicateAlias: 'Duplikat aliasa, primjenjuje se samo posljednje mapiranje.', + customFontsPath: 'Datoteka fonta (npr. E:ARIAL.TTF)', + customFontsAdd: 'Dodaj mapiranje', + customFontsRemove: 'Ukloni', }, app: { diff --git a/src/locales/hu.ts b/src/locales/hu.ts index b21d30a4..593d602d 100644 --- a/src/locales/hu.ts +++ b/src/locales/hu.ts @@ -121,6 +121,14 @@ const hu = { defaultFontId: 'Betűtípus', defaultFontHeight: 'Magasság (pontok)', defaultFontWidth: 'Szélesség (dots)', + customFontsHeading: 'Egyéni betűtípusok', + customFontsHint: 'Aliasok a nyomtatón tárolt betűtípusokhoz.', + customFontsAlias: 'ID', + customFontsAliasHint: 'Egy karakter: A-Z vagy 0-9', + customFontsDuplicateAlias: 'Ismétlődő alias, csak az utolsó hozzárendelés érvényes.', + customFontsPath: 'Betűtípusfájl (pl. E:ARIAL.TTF)', + customFontsAdd: 'Hozzárendelés hozzáadása', + customFontsRemove: 'Eltávolítás', }, app: { diff --git a/src/locales/it.ts b/src/locales/it.ts index 536f6bfd..daf90f17 100644 --- a/src/locales/it.ts +++ b/src/locales/it.ts @@ -121,6 +121,14 @@ const it = { defaultFontId: 'Carattere', defaultFontHeight: 'Altezza (punti)', defaultFontWidth: 'Larghezza (dots)', + customFontsHeading: 'Font personalizzati', + customFontsHint: 'Alias per i font memorizzati sulla stampante.', + customFontsAlias: 'ID', + customFontsAliasHint: 'Un solo carattere: A-Z o 0-9', + customFontsDuplicateAlias: 'Alias duplicato — solo l\'ultima mappatura ha effetto.', + customFontsPath: 'File del font (es. E:ARIAL.TTF)', + customFontsAdd: 'Aggiungi mappatura', + customFontsRemove: 'Rimuovi', }, app: { diff --git a/src/locales/ja.ts b/src/locales/ja.ts index 0f742b8e..5dff3a56 100644 --- a/src/locales/ja.ts +++ b/src/locales/ja.ts @@ -121,6 +121,14 @@ const ja = { defaultFontId: 'フォント', defaultFontHeight: '高さ (ドット)', defaultFontWidth: '幅 (dots)', + customFontsHeading: 'カスタムフォント', + customFontsHint: 'プリンターに保存されたフォントのエイリアス。', + customFontsAlias: 'ID', + customFontsAliasHint: '1 文字: A-Z または 0-9', + customFontsDuplicateAlias: 'エイリアスが重複しています。最後のマッピングのみが適用されます。', + customFontsPath: 'フォントファイル (例: E:ARIAL.TTF)', + customFontsAdd: 'マッピングを追加', + customFontsRemove: '削除', }, app: { diff --git a/src/locales/ko.ts b/src/locales/ko.ts index 5ed28fc0..5839828b 100644 --- a/src/locales/ko.ts +++ b/src/locales/ko.ts @@ -121,6 +121,14 @@ const ko = { defaultFontId: '글꼴', defaultFontHeight: '높이 (도트)', defaultFontWidth: '너비 (dots)', + customFontsHeading: '사용자 글꼴', + customFontsHint: '프린터에 저장된 글꼴의 별칭입니다.', + customFontsAlias: 'ID', + customFontsAliasHint: '한 글자: A-Z 또는 0-9', + customFontsDuplicateAlias: '중복된 별칭, 마지막 매핑만 적용됩니다.', + customFontsPath: '글꼴 파일 (예: E:ARIAL.TTF)', + customFontsAdd: '매핑 추가', + customFontsRemove: '제거', }, app: { diff --git a/src/locales/lt.ts b/src/locales/lt.ts index daf18a7f..b5ffabc0 100644 --- a/src/locales/lt.ts +++ b/src/locales/lt.ts @@ -121,6 +121,14 @@ const lt = { defaultFontId: 'Šriftas', defaultFontHeight: 'Aukštis (taškai)', defaultFontWidth: 'Plotis (dots)', + customFontsHeading: 'Pasirinktiniai šriftai', + customFontsHint: 'Aliasai spausdintuve saugomiems šriftams.', + customFontsAlias: 'ID', + customFontsAliasHint: 'Vienas simbolis: A-Z arba 0-9', + customFontsDuplicateAlias: 'Pasikartojantis alias, taikomas tik paskutinis susiejimas.', + customFontsPath: 'Šrifto failas (pvz., E:ARIAL.TTF)', + customFontsAdd: 'Pridėti susiejimą', + customFontsRemove: 'Pašalinti', }, app: { diff --git a/src/locales/lv.ts b/src/locales/lv.ts index 6fe5430a..2e9c2f56 100644 --- a/src/locales/lv.ts +++ b/src/locales/lv.ts @@ -121,6 +121,14 @@ const lv = { defaultFontId: 'Fonts', defaultFontHeight: 'Augstums (punkti)', defaultFontWidth: 'Platums (dots)', + customFontsHeading: 'Pielāgotie fonti', + customFontsHint: 'Aliasi printerī saglabātajiem fontiem.', + customFontsAlias: 'ID', + customFontsAliasHint: 'Viena rakstzīme: A-Z vai 0-9', + customFontsDuplicateAlias: 'Dublēts aliass, darbojas tikai pēdējā piesaiste.', + customFontsPath: 'Fonta fails (piem., E:ARIAL.TTF)', + customFontsAdd: 'Pievienot piesaisti', + customFontsRemove: 'Noņemt', }, app: { diff --git a/src/locales/nl.ts b/src/locales/nl.ts index 2fcbc9b6..db68418a 100644 --- a/src/locales/nl.ts +++ b/src/locales/nl.ts @@ -121,6 +121,14 @@ const nl = { defaultFontId: 'Lettertype', defaultFontHeight: 'Hoogte (dots)', defaultFontWidth: 'Breedte (dots)', + customFontsHeading: 'Aangepaste lettertypen', + customFontsHint: 'Aliassen voor lettertypen op de printer.', + customFontsAlias: 'ID', + customFontsAliasHint: 'Eén teken: A-Z of 0-9', + customFontsDuplicateAlias: 'Dubbele alias, alleen de laatste mapping wordt toegepast.', + customFontsPath: 'Lettertypebestand (bv. E:ARIAL.TTF)', + customFontsAdd: 'Mapping toevoegen', + customFontsRemove: 'Verwijderen', }, app: { diff --git a/src/locales/no.ts b/src/locales/no.ts index ab822d33..152e6f80 100644 --- a/src/locales/no.ts +++ b/src/locales/no.ts @@ -121,6 +121,14 @@ const no = { defaultFontId: 'Skrift', defaultFontHeight: 'Høyde (punkter)', defaultFontWidth: 'Bredde (dots)', + customFontsHeading: 'Egendefinerte skrifter', + customFontsHint: 'Aliaser for skrifter som ligger på skriveren.', + customFontsAlias: 'ID', + customFontsAliasHint: 'Ett tegn: A-Z eller 0-9', + customFontsDuplicateAlias: 'Duplisert alias, kun siste mapping gjelder.', + customFontsPath: 'Skriftfil (f.eks. E:ARIAL.TTF)', + customFontsAdd: 'Legg til mapping', + customFontsRemove: 'Fjern', }, app: { diff --git a/src/locales/pl.ts b/src/locales/pl.ts index e9fca70b..56ff225c 100644 --- a/src/locales/pl.ts +++ b/src/locales/pl.ts @@ -121,6 +121,14 @@ const pl = { defaultFontId: 'Czcionka', defaultFontHeight: 'Wysokość (punkty)', defaultFontWidth: 'Szerokość (dots)', + customFontsHeading: 'Niestandardowe czcionki', + customFontsHint: 'Aliasy dla czcionek zapisanych w pamięci drukarki.', + customFontsAlias: 'ID', + customFontsAliasHint: 'Jeden znak: A-Z lub 0-9', + customFontsDuplicateAlias: 'Zduplikowany alias, działa tylko ostatnie mapowanie.', + customFontsPath: 'Plik czcionki (np. E:ARIAL.TTF)', + customFontsAdd: 'Dodaj mapowanie', + customFontsRemove: 'Usuń', }, app: { diff --git a/src/locales/pt.ts b/src/locales/pt.ts index 1d269a5d..aebcb237 100644 --- a/src/locales/pt.ts +++ b/src/locales/pt.ts @@ -121,6 +121,14 @@ const pt = { defaultFontId: 'Fonte', defaultFontHeight: 'Altura (pontos)', defaultFontWidth: 'Largura (dots)', + customFontsHeading: 'Fontes personalizadas', + customFontsHint: 'Aliases para fontes armazenadas na impressora.', + customFontsAlias: 'ID', + customFontsAliasHint: 'Um único caractere: A-Z ou 0-9', + customFontsDuplicateAlias: 'Alias duplicado, só o último mapeamento é aplicado.', + customFontsPath: 'Arquivo de fonte (ex. E:ARIAL.TTF)', + customFontsAdd: 'Adicionar mapeamento', + customFontsRemove: 'Remover', }, app: { diff --git a/src/locales/ro.ts b/src/locales/ro.ts index 426fcc85..766393bd 100644 --- a/src/locales/ro.ts +++ b/src/locales/ro.ts @@ -121,6 +121,14 @@ const ro = { defaultFontId: 'Font', defaultFontHeight: 'Înălțime (puncte)', defaultFontWidth: 'Lățime (dots)', + customFontsHeading: 'Fonturi personalizate', + customFontsHint: 'Aliasuri pentru fonturile stocate pe imprimantă.', + customFontsAlias: 'ID', + customFontsAliasHint: 'Un singur caracter: A-Z sau 0-9', + customFontsDuplicateAlias: 'Alias duplicat, doar ultima mapare are efect.', + customFontsPath: 'Fișier font (ex. E:ARIAL.TTF)', + customFontsAdd: 'Adaugă mapare', + customFontsRemove: 'Elimină', }, app: { diff --git a/src/locales/sk.ts b/src/locales/sk.ts index 48d83555..73981efb 100644 --- a/src/locales/sk.ts +++ b/src/locales/sk.ts @@ -121,6 +121,14 @@ const sk = { defaultFontId: 'Písmo', defaultFontHeight: 'Výška (body)', defaultFontWidth: 'Šírka (dots)', + customFontsHeading: 'Vlastné písma', + customFontsHint: 'Aliasy pre písma uložené v tlačiarni.', + customFontsAlias: 'ID', + customFontsAliasHint: 'Jeden znak: A-Z alebo 0-9', + customFontsDuplicateAlias: 'Duplicitný alias, uplatní sa iba posledné mapovanie.', + customFontsPath: 'Súbor písma (napr. E:ARIAL.TTF)', + customFontsAdd: 'Pridať mapovanie', + customFontsRemove: 'Odstrániť', }, app: { diff --git a/src/locales/sl.ts b/src/locales/sl.ts index 95e39544..81660e81 100644 --- a/src/locales/sl.ts +++ b/src/locales/sl.ts @@ -121,6 +121,14 @@ const sl = { defaultFontId: 'Pisava', defaultFontHeight: 'Višina (točke)', defaultFontWidth: 'Širina (dots)', + customFontsHeading: 'Pisave po meri', + customFontsHint: 'Vzdevki za pisave shranjene v tiskalniku.', + customFontsAlias: 'ID', + customFontsAliasHint: 'En znak: A-Z ali 0-9', + customFontsDuplicateAlias: 'Podvojen vzdevek, uveljavi se le zadnja preslikava.', + customFontsPath: 'Datoteka pisave (npr. E:ARIAL.TTF)', + customFontsAdd: 'Dodaj preslikavo', + customFontsRemove: 'Odstrani', }, app: { diff --git a/src/locales/sr.ts b/src/locales/sr.ts index 3042623c..19ff8c2d 100644 --- a/src/locales/sr.ts +++ b/src/locales/sr.ts @@ -121,6 +121,14 @@ const sr = { defaultFontId: 'Фонт', defaultFontHeight: 'Висина (тачке)', defaultFontWidth: 'Ширина (dots)', + customFontsHeading: 'Прилагођени фонтови', + customFontsHint: 'Алијаси за фонтове сачуване у штампачу.', + customFontsAlias: 'ID', + customFontsAliasHint: 'Један знак: A-Z или 0-9', + customFontsDuplicateAlias: 'Дупликат алијаса, примењује се само последње мапирање.', + customFontsPath: 'Датотека фонта (нпр. E:ARIAL.TTF)', + customFontsAdd: 'Додај мапирање', + customFontsRemove: 'Уклони', }, app: { diff --git a/src/locales/sv.ts b/src/locales/sv.ts index 15ac834a..437ec083 100644 --- a/src/locales/sv.ts +++ b/src/locales/sv.ts @@ -121,6 +121,14 @@ const sv = { defaultFontId: 'Typsnitt', defaultFontHeight: 'Höjd (punkter)', defaultFontWidth: 'Bredd (dots)', + customFontsHeading: 'Egna typsnitt', + customFontsHint: 'Alias för typsnitt som finns i skrivaren.', + customFontsAlias: 'ID', + customFontsAliasHint: 'Ett tecken: A-Z eller 0-9', + customFontsDuplicateAlias: 'Dubblerat alias, endast den senaste mappningen gäller.', + customFontsPath: 'Typsnittsfil (t.ex. E:ARIAL.TTF)', + customFontsAdd: 'Lägg till mappning', + customFontsRemove: 'Ta bort', }, app: { diff --git a/src/locales/tr.ts b/src/locales/tr.ts index 3c186cab..e968d155 100644 --- a/src/locales/tr.ts +++ b/src/locales/tr.ts @@ -121,6 +121,14 @@ const tr = { defaultFontId: 'Yazı tipi', defaultFontHeight: 'Yükseklik (nokta)', defaultFontWidth: 'Genişlik (dots)', + customFontsHeading: 'Özel yazı tipleri', + customFontsHint: 'Yazıcıda kayıtlı yazı tipleri için takma adlar.', + customFontsAlias: 'Kimlik', + customFontsAliasHint: 'Tek karakter: A-Z veya 0-9', + customFontsDuplicateAlias: 'Yinelenen takma ad, yalnızca son eşleme geçerli olur.', + customFontsPath: 'Yazı tipi dosyası (örn. E:ARIAL.TTF)', + customFontsAdd: 'Eşleme ekle', + customFontsRemove: 'Kaldır', }, app: { diff --git a/src/locales/zh-hans.ts b/src/locales/zh-hans.ts index bf373ee7..0892072c 100644 --- a/src/locales/zh-hans.ts +++ b/src/locales/zh-hans.ts @@ -121,6 +121,14 @@ const zhHans = { defaultFontId: '字体', defaultFontHeight: '高度 (点)', defaultFontWidth: '宽度 (dots)', + customFontsHeading: '自定义字体', + customFontsHint: '为打印机中已有的字体设置别名。', + customFontsAlias: 'ID', + customFontsAliasHint: '单个字符:A-Z 或 0-9', + customFontsDuplicateAlias: '重复别名, 仅最后一个映射生效。', + customFontsPath: '字体文件 (例如 E:ARIAL.TTF)', + customFontsAdd: '添加映射', + customFontsRemove: '移除', }, app: { diff --git a/src/locales/zh-hant.ts b/src/locales/zh-hant.ts index 202e3c2b..a9f4218e 100644 --- a/src/locales/zh-hant.ts +++ b/src/locales/zh-hant.ts @@ -121,6 +121,14 @@ const zhHant = { defaultFontId: '字型', defaultFontHeight: '高度 (點)', defaultFontWidth: '寬度 (dots)', + customFontsHeading: '自訂字型', + customFontsHint: '為印表機中已有的字型設定別名。', + customFontsAlias: 'ID', + customFontsAliasHint: '單一字元:A-Z 或 0-9', + customFontsDuplicateAlias: '重複別名, 僅最後一個對應生效。', + customFontsPath: '字型檔案 (例如 E:ARIAL.TTF)', + customFontsAdd: '新增對應', + customFontsRemove: '移除', }, app: { diff --git a/src/types/ObjectType.ts b/src/types/ObjectType.ts index bbc4baaf..35adfc5d 100644 --- a/src/types/ObjectType.ts +++ b/src/types/ObjectType.ts @@ -1,6 +1,14 @@ import type React from 'react'; import { z } from 'zod'; +/** A single ^CW mapping: a 1-character alias [A-Z0-9] paired with a font + * path on the printer's storage (e.g. "E:ARIAL.TTF"). */ +export const customFontMappingSchema = z.object({ + alias: z.string().regex(/^[A-Z0-9]$/), + path: z.string().min(1), +}); +export type CustomFontMapping = z.infer; + export const labelConfigSchema = z.object({ widthMm: z.number(), heightMm: z.number(), @@ -38,6 +46,10 @@ export const labelConfigSchema = z.object({ defaultFontHeight: z.number().int().positive().optional(), /** ^CF width param. Spec allows 0 → printer auto-derives from height. */ defaultFontWidth: z.number().int().min(0).optional(), + /** ^CW alias→path mappings emitted at the top of the label. Each entry + * registers a single-char identifier ([A-Z0-9]) that ^A{alias} fields + * can reference instead of the verbose ^A@…E:font.TTF form. */ + customFonts: z.array(customFontMappingSchema).optional(), }); export type LabelConfig = z.infer; From 13acb76c328e6bb9615d3e141598abb7c809ef8e Mon Sep 17 00:00:00 2001 From: u8array Date: Tue, 19 May 2026 23:18:48 +0200 Subject: [PATCH 2/6] feat(fonts): inline ZPL alias action in Fonts tab Each uploaded font now has a single-character alias input alongside its name. Typing a letter writes a ^CW mapping for the active label (path = E:{name}); clearing the input removes the mapping. This short-circuits the previous flow of upload, switch tabs, open Custom Fonts section, add a row, pick the path from the suggestions. Layout switched from flex to grid to avoid the inputCls.w-full vs flex-1 collision that previously shrank sibling content to zero width. Locale keys fonts.aliasHint / fonts.aliasAssigned added in all 32 languages via a one-shot inserter script (the shared add_locale_key helper only handles second-level blocks; fonts is top-level). --- src/components/Fonts/FontManager.tsx | 55 ++++++++++++++++++++++++---- src/locales/ar.ts | 2 + src/locales/bg.ts | 2 + src/locales/cs.ts | 2 + src/locales/da.ts | 2 + src/locales/de.ts | 2 + src/locales/el.ts | 2 + src/locales/en.ts | 2 + src/locales/es.ts | 2 + src/locales/et.ts | 2 + src/locales/fa.ts | 2 + src/locales/fi.ts | 2 + src/locales/fr.ts | 2 + src/locales/he.ts | 2 + src/locales/hr.ts | 2 + src/locales/hu.ts | 2 + src/locales/it.ts | 2 + src/locales/ja.ts | 2 + src/locales/ko.ts | 2 + src/locales/lt.ts | 2 + src/locales/lv.ts | 2 + src/locales/nl.ts | 2 + src/locales/no.ts | 2 + src/locales/pl.ts | 2 + src/locales/pt.ts | 2 + src/locales/ro.ts | 2 + src/locales/sk.ts | 2 + src/locales/sl.ts | 2 + src/locales/sr.ts | 2 + src/locales/sv.ts | 2 + src/locales/tr.ts | 2 + src/locales/zh-hans.ts | 2 + src/locales/zh-hant.ts | 2 + 33 files changed, 112 insertions(+), 7 deletions(-) diff --git a/src/components/Fonts/FontManager.tsx b/src/components/Fonts/FontManager.tsx index f4a6affe..91af4271 100644 --- a/src/components/Fonts/FontManager.tsx +++ b/src/components/Fonts/FontManager.tsx @@ -1,16 +1,38 @@ import { useRef, useState, useCallback } from 'react'; import { getAllFonts, loadFontFile, removeFont } from '../../lib/fontCache'; import { useFontCacheVersion } from '../../hooks/useFontCacheVersion'; +import { useLabelStore } from '../../store/labelStore'; import { useT } from '../../lib/useT'; import { inputCls, labelCls } from '../Properties/styles'; +import type { CustomFontMapping } from '../../types/ObjectType'; + +const ALIAS_CHAR_RE = /[^A-Z0-9]/g; export function FontManager() { const t = useT(); useFontCacheVersion(); + const customFonts = useLabelStore((s) => s.label.customFonts); + const setLabelConfig = useLabelStore((s) => s.setLabelConfig); const fonts = getAllFonts(); const [adding, setAdding] = useState(false); + // Map an uploaded font to its current ^CW alias (if any) for the active + // label. The Fonts tab spans labels but ^CW mappings are per-label, so + // the displayed alias here reflects whatever the active label has set. + const aliasByPath = new Map(); + for (const m of customFonts ?? []) aliasByPath.set(m.path, m.alias); + + const setAlias = (path: string, rawAlias: string) => { + const alias = rawAlias.toUpperCase().replace(ALIAS_CHAR_RE, '').slice(0, 1); + const list = customFonts ?? []; + const withoutThis = list.filter((m) => m.path !== path); + const next: CustomFontMapping[] = alias + ? [...withoutThis, { alias, path }] + : withoutThis; + setLabelConfig({ customFonts: next.length > 0 ? next : undefined }); + }; + return (

@@ -22,9 +44,17 @@ export function FontManager() { )}

- {fonts.map((font) => ( - - ))} + {fonts.map((font) => { + const path = `E:${font.name}`; + return ( + setAlias(path, v)} + /> + ); + })}
{adding ? ( @@ -47,15 +77,26 @@ export function FontManager() { interface FontEntryProps { name: string; + alias: string; + onAliasChange: (next: string) => void; } -function FontEntry({ name }: FontEntryProps) { +function FontEntry({ name, alias, onAliasChange }: FontEntryProps) { const t = useT(); return ( -
- F - {name} +
+ F + {name} + onAliasChange(e.target.value)} + /> )} + + + + + + + {ZPL_DRIVE_PREFIXES.map((p) => ( + + + {pendingDelete !== null && ( + { + removeFont(pendingDelete); + setPendingDelete(null); + }} + onCancel={() => setPendingDelete(null)} + /> + )}
); } @@ -79,28 +182,47 @@ export function FontManager() { interface FontEntryProps { name: string; alias: string; + duplicate: boolean; onAliasChange: (next: string) => void; + onRequestDelete: () => void; } -function FontEntry({ name, alias, onAliasChange }: FontEntryProps) { +function FontEntry({ + name, + alias, + duplicate, + onAliasChange, + onRequestDelete, +}: FontEntryProps) { const t = useT(); return ( -
- F - {name} +
+ + {name} + onAliasChange(e.target.value)} /> +
+ ); + })} + +
+ ); +} + // ── AddFontForm ──────────────────────────────────────────────────────────────── interface AddFontFormProps { @@ -125,7 +330,10 @@ function AddFontForm({ onDone }: AddFontFormProps) { const [uploadFailed, setUploadFailed] = useState(false); const handleFileChange = useCallback(async (file: File) => { - const printerName = name.trim() || file.name; + // Default to the source filename uppercased — Zebra printer storage + // conventionally uses uppercase ALL.TTF style identifiers, and a + // freshly-picked file is almost always the user's intended name. + const printerName = name.trim() || file.name.toUpperCase(); setUploading(true); setUploadFailed(false); try { diff --git a/src/components/Properties/CustomFontsSection.tsx b/src/components/Properties/CustomFontsSection.tsx deleted file mode 100644 index 4092752f..00000000 --- a/src/components/Properties/CustomFontsSection.tsx +++ /dev/null @@ -1,148 +0,0 @@ -import { - InformationCircleIcon, - PlusIcon, - TrashIcon, -} from "@heroicons/react/16/solid"; -import { CollapsibleSection } from "../ui/CollapsibleSection"; -import { useT } from "../../lib/useT"; -import { getAllFonts } from "../../lib/fontCache"; -import { useFontCacheVersion } from "../../hooks/useFontCacheVersion"; -import { normalizeAlias, DEFAULT_FONT_DRIVE } from "../../lib/customFonts"; -import { inputCls } from "./styles"; -import type { CustomFontMapping } from "../../types/ObjectType"; - -const PATHS_DATALIST_ID = "zpl-custom-font-paths"; - -/** Standard Zebra storage drive prefixes. Surfaced as datalist hints so - * users new to ZPL discover the path syntax without reading the spec. - * E = flash, R = volatile RAM, A = removable (PCMCIA/CF), B = optional - * on-board flash. */ -const ZPL_DRIVE_PREFIXES = ["E:", "R:", "A:", "B:"] as const; - -/** Editor for the ^CW alias→path mapping list. The alias input restricts - * to [A-Z0-9] to match the schema regex; empty aliases or paths survive - * in state so users can type at their own pace, but rows that stay empty - * through a blur are auto-removed and the generator skips any that slip - * through at emit time. */ -export function CustomFontsSection({ - mappings, - onChange, -}: { - mappings: CustomFontMapping[]; - onChange: (next: CustomFontMapping[]) => void; -}) { - const t = useT(); - // useFontCacheVersion triggers a re-render whenever the font cache - // changes; the React Compiler handles memoisation downstream. - useFontCacheVersion(); - const uploadedPaths = getAllFonts().map( - (f) => `${DEFAULT_FONT_DRIVE}${f.name}`, - ); - - // Count alias occurrences so duplicates can be flagged inline. Empty - // aliases never count as duplicates of each other. - const aliasCounts = new Map(); - for (const m of mappings) { - if (m.alias) aliasCounts.set(m.alias, (aliasCounts.get(m.alias) ?? 0) + 1); - } - const isDuplicateAlias = (alias: string) => - !!alias && (aliasCounts.get(alias) ?? 0) > 1; - - const updateAt = (i: number, patch: Partial) => { - onChange(mappings.map((m, idx) => (idx === i ? { ...m, ...patch } : m))); - }; - const removeAt = (i: number) => { - onChange(mappings.filter((_, idx) => idx !== i)); - }; - const add = () => { - onChange([...mappings, { alias: "", path: "" }]); - }; - // Auto-remove a row when focus leaves it and both fields are blank. - // Keeps the editor tidy without an explicit "discard" interaction. - const handleRowBlur = (i: number) => { - // Defer to next tick so focus can land on the sibling input first; - // otherwise tabbing alias → path would delete the row mid-traversal. - requestAnimationFrame(() => { - const row = mappings[i]; - if (row && !row.alias && !row.path) removeAt(i); - }); - }; - - return ( - - {t.label.customFontsHeading} - - - } - defaultOpen={false} - > -
- {mappings.map((m, i) => { - const dup = isDuplicateAlias(m.alias); - return ( -
handleRowBlur(i)} - > - - updateAt(i, { alias: normalizeAlias(e.target.value) }) - } - /> - updateAt(i, { path: e.target.value })} - /> - -
- ); - })} - - - {ZPL_DRIVE_PREFIXES.map((p) => ( - -
-
- ); -} diff --git a/src/components/Properties/PropertiesPanel.tsx b/src/components/Properties/PropertiesPanel.tsx index 73715c8b..03fdee00 100644 --- a/src/components/Properties/PropertiesPanel.tsx +++ b/src/components/Properties/PropertiesPanel.tsx @@ -1,6 +1,5 @@ import type { RefObject } from "react"; import { InformationCircleIcon, FolderPlusIcon } from "@heroicons/react/16/solid"; -import { CustomFontsSection } from "./CustomFontsSection"; import { useLabelStore, useCurrentObjects } from "../../store/labelStore"; import type { LabelCanvasHandle } from "../Canvas/LabelCanvas"; import type { AlignAxis } from "../../lib/alignment"; @@ -23,11 +22,7 @@ import { CollapsibleSection } from "../ui/CollapsibleSection"; import { AlignButtons } from "./AlignButtons"; import { inputCls, labelCls } from "./styles"; import type { LabelConfig } from "../../types/ObjectType"; - -/** Built-in alphanumeric font IDs the Zebra firmware ships with. Used as - * suggestions for ^CF — the input stays free-text so user-defined ^CW - * aliases (single letters) can still be entered. */ -const ZPL_BUILTIN_FONT_IDS = ['0', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H'] as const; +import { ZPL_BUILTIN_FONT_IDS } from "../../lib/customFonts"; interface PropertiesPanelProps { /** Imperative handle on the canvas — used for actions that need live render @@ -350,13 +345,27 @@ function LabelConfigPanel({ // ^CF / ^A suggestions: built-in font letters plus every alias the // user has registered via ^CW. Set-based dedup keeps user-overridden - // built-ins from appearing twice. + // built-ins from appearing twice. The label on custom aliases shows + // the referenced path so a bare letter is not mistaken for a built-in. + const aliasPaths = new Map(); + for (const m of label.customFonts ?? []) { + if (m.alias) aliasPaths.set(m.alias, m.path); + } const fontIdOptions = Array.from( new Set([ ...ZPL_BUILTIN_FONT_IDS, - ...(label.customFonts?.map((m) => m.alias).filter(Boolean) ?? []), + ...aliasPaths.keys(), ]), - ); + ).map((id) => { + const path = aliasPaths.get(id); + // Strip the drive prefix (E:, R:, ...) from the display label; + // the full path stays in the underlying customFonts entry and is + // emitted verbatim to ZPL. + return { + value: id, + label: path ? path.replace(/^[A-Z]:/, '') : undefined, + }; + }); return (
@@ -815,17 +824,10 @@ function LabelConfigPanel({
- - - onUpdate({ customFonts: customFonts.length > 0 ? customFonts : undefined }) - } - />
- {fontIdOptions.map((id) => ( - diff --git a/src/lib/customFonts.test.ts b/src/lib/customFonts.test.ts index 134c07a4..64750ca6 100644 --- a/src/lib/customFonts.test.ts +++ b/src/lib/customFonts.test.ts @@ -1,5 +1,9 @@ import { describe, it, expect } from "vitest"; -import { normalizeAlias, upsertCustomFontMapping } from "./customFonts"; +import { + nextFreeAlias, + normalizeAlias, + upsertCustomFontMapping, +} from "./customFonts"; describe("normalizeAlias", () => { it("uppercases letters", () => { @@ -64,3 +68,26 @@ describe("upsertCustomFontMapping", () => { ]); }); }); + +describe("nextFreeAlias", () => { + it("returns I when nothing is taken (first non-built-in)", () => { + expect(nextFreeAlias([])).toBe("I"); + }); + + it("skips taken aliases in the preferred range", () => { + expect(nextFreeAlias(["I", "J", "K"])).toBe("L"); + }); + + it("avoids built-in font letters until the unreserved range is exhausted", () => { + // I-Z + 1-9 = 26 chars; if all are taken the helper falls back to + // the built-in letters. + const taken = "IJKLMNOPQRSTUVWXYZ123456789".split(""); + expect(nextFreeAlias(taken)).toBe("0"); + }); + + it("returns empty when all 36 valid characters are taken", () => { + const all = + "0ABCDEFGHIJKLMNOPQRSTUVWXYZ123456789".split(""); + expect(nextFreeAlias(all)).toBe(""); + }); +}); diff --git a/src/lib/customFonts.ts b/src/lib/customFonts.ts index adbc0d3a..07d1833d 100644 --- a/src/lib/customFonts.ts +++ b/src/lib/customFonts.ts @@ -12,6 +12,32 @@ export const ALIAS_CHAR_RE = /[^A-Z0-9]/g; * Custom Fonts row directly. */ export const DEFAULT_FONT_DRIVE = "E:"; +/** Standard Zebra storage drive prefixes for path suggestions: + * E = flash, R = volatile RAM, A = removable (PCMCIA/CF), B = optional + * on-board flash. The first entry matches `DEFAULT_FONT_DRIVE`. */ +export const ZPL_DRIVE_PREFIXES = ["E:", "R:", "A:", "B:"] as const; + +/** Built-in alphanumeric font IDs the Zebra firmware ships with. + * Used as default-font suggestions and excluded from the auto-pick + * range in `nextFreeAlias` so users do not accidentally override the + * built-ins. */ +export const ZPL_BUILTIN_FONT_IDS = [ + "0", + "A", + "B", + "C", + "D", + "E", + "F", + "G", + "H", +] as const; + +/** The printer-storage path emitted for a browser-uploaded font. */ +export function uploadedFontPath(name: string): string { + return `${DEFAULT_FONT_DRIVE}${name}`; +} + /** Normalise raw user input into a valid ^CW alias char (or empty * string if no usable character is present). */ export function normalizeAlias(raw: string): string { @@ -30,3 +56,22 @@ export function upsertCustomFontMapping( if (!alias) return withoutPath; return [...withoutPath, { alias, path }]; } + +const ZPL_BUILTIN_FONT_LETTERS = '0ABCDEFGH'; +const ALIAS_PREFERRED_ORDER = 'IJKLMNOPQRSTUVWXYZ123456789'; + +/** Pick the first alias character that is not already taken. Built-in + * Zebra font letters (0, A-H) are tried only after the unreserved + * range is exhausted; assigning one of them is a deliberate override + * of the built-in font, which we avoid by default. Returns '' if all + * 36 valid alias characters are in use. */ +export function nextFreeAlias(taken: Iterable): string { + const used = new Set(taken); + for (const c of ALIAS_PREFERRED_ORDER) { + if (!used.has(c)) return c; + } + for (const c of ZPL_BUILTIN_FONT_LETTERS) { + if (!used.has(c)) return c; + } + return ''; +} diff --git a/src/lib/zplGenerator.test.ts b/src/lib/zplGenerator.test.ts index 0336a328..67cf1277 100644 --- a/src/lib/zplGenerator.test.ts +++ b/src/lib/zplGenerator.test.ts @@ -236,6 +236,59 @@ describe('generateZPL — printer params', () => { expect(zpl).toContain('^CWB,E:OK.TTF'); }); + it('rewrites ^A@ font refs to ^A{alias} when a ^CW mapping exists', () => { + const text: LabelObject = { + id: 't1', + type: 'text', + x: 10, + y: 10, + rotation: 0, + props: { + content: 'hi', + rotation: 'N', + fontHeight: 30, + fontWidth: 0, + printerFontName: 'ARIAL.TTF', + }, + }; + const zpl = generateZPL( + { + ...BASE_LABEL, + customFonts: [{ alias: 'M', path: 'E:ARIAL.TTF' }], + }, + [text], + ); + expect(zpl).toContain('^CWM,E:ARIAL.TTF'); + expect(zpl).toContain('^AMN,30,0'); + expect(zpl).not.toContain('^A@N,30,0,E:ARIAL.TTF'); + }); + + it('leaves ^A@ verbose when no matching ^CW alias is defined', () => { + const text: LabelObject = { + id: 't1', + type: 'text', + x: 10, + y: 10, + rotation: 0, + props: { + content: 'hi', + rotation: 'N', + fontHeight: 30, + fontWidth: 0, + printerFontName: 'ORPHAN.TTF', + }, + }; + const zpl = generateZPL( + { + ...BASE_LABEL, + customFonts: [{ alias: 'M', path: 'E:OTHER.TTF' }], + }, + [text], + ); + expect(zpl).toContain('^A@N,30,0,E:ORPHAN.TTF'); + expect(zpl).not.toContain('^AMN,30,0'); + }); + it('emits ^PM when mirror is set', () => { expect(generateZPL({ ...BASE_LABEL, mirror: 'Y' }, [])).toContain('^PMY'); expect(generateZPL({ ...BASE_LABEL, mirror: 'N' }, [])).toContain('^PMN'); diff --git a/src/lib/zplGenerator.ts b/src/lib/zplGenerator.ts index f93924ac..9b520700 100644 --- a/src/lib/zplGenerator.ts +++ b/src/lib/zplGenerator.ts @@ -141,5 +141,25 @@ export function generateZPL(label: LabelConfig, objects: LabelObject[]): string lines.push('^XZ'); - return lines.join('\n'); + // Rewrite ^A@...E:NAME.TTF references to ^A{alias} for paths that the + // user has registered via ^CW. The ^CW lines are already in the + // header, so the printer resolves the short form against the alias + // table. Saves bytes and surfaces the user's alias choices in the + // output. ^A@ is the only emit pattern that references font files + // verbatim, so the regex stays scoped to a single shape. + const aliasByPath = new Map(); + for (const m of label.customFonts ?? []) { + if (m.alias) aliasByPath.set(m.path, m.alias); + } + let output = lines.join('\n'); + if (aliasByPath.size > 0) { + output = output.replace( + /\^A@([NIRB]),(\d+),(\d+),(E:[^^\n]+?)(?=\^|\n|$)/g, + (full, rot, h, w, path) => { + const alias = aliasByPath.get(path); + return alias ? `^A${alias}${rot},${h},${w}` : full; + }, + ); + } + return output; } diff --git a/src/locales/ar.ts b/src/locales/ar.ts index 580c4652..83a1df1c 100644 --- a/src/locales/ar.ts +++ b/src/locales/ar.ts @@ -448,9 +448,13 @@ const ar = { upload: 'رفع', cancel: 'إلغاء', delete: 'حذف', + deleteConfirm: 'إزالة هذا الخط من التصميم؟', uploadError: 'تعذّر تحميل ملف الخط', aliasHint: 'اسم مستعار ZPL لهذا الملصق (حرف واحد، A-Z أو 0-9)', aliasAssigned: 'الاسم المستعار ZPL المعيَّن لهذا الملصق', + manualMappingsHeading: 'خطوط الطابعة المقيمة', + manualMappingsHint: 'إشارة إلى خطوط موجودة بالفعل على الطابعة لم يتم رفعها هنا.', + addManualMapping: 'إضافة خط طابعة', }, zebraPrint: { heading: 'إرسال إلى طابعة Zebra', diff --git a/src/locales/bg.ts b/src/locales/bg.ts index 85bf5d1c..c870bed7 100644 --- a/src/locales/bg.ts +++ b/src/locales/bg.ts @@ -448,9 +448,13 @@ const bg = { upload: 'Качване', cancel: 'Отказ', delete: 'Изтриване', + deleteConfirm: 'Премахване на този шрифт от дизайна?', uploadError: 'Неуспешно зареждане на шрифтов файл', aliasHint: 'ZPL псевдоним за този етикет (1 знак, A-Z или 0-9)', aliasAssigned: 'Зададен ZPL псевдоним за този етикет', + manualMappingsHeading: 'Шрифтове в принтера', + manualMappingsHint: 'Препратка към шрифтове, които вече са в принтера, но не са качени тук.', + addManualMapping: 'Добави шрифт на принтера', }, zebraPrint: { heading: 'Изпрати към принтер Zebra', diff --git a/src/locales/cs.ts b/src/locales/cs.ts index d75e8d56..d29c2166 100644 --- a/src/locales/cs.ts +++ b/src/locales/cs.ts @@ -448,9 +448,13 @@ const cs = { upload: 'Nahrát', cancel: 'Zrušit', delete: 'Smazat', + deleteConfirm: 'Odebrat toto písmo z návrhu?', uploadError: 'Soubor písma se nepodařilo načíst', aliasHint: 'ZPL alias pro tento štítek (1 znak, A-Z nebo 0-9)', aliasAssigned: 'Přiřazený ZPL alias pro tento štítek', + manualMappingsHeading: 'Písma uložená v tiskárně', + manualMappingsHint: 'Odkaz na písma, která jsou již v tiskárně, ale zde nejsou nahrána.', + addManualMapping: 'Přidat písmo tiskárny', }, zebraPrint: { heading: 'Odeslat na tiskárnu Zebra', diff --git a/src/locales/da.ts b/src/locales/da.ts index 71acb4eb..7948ce93 100644 --- a/src/locales/da.ts +++ b/src/locales/da.ts @@ -448,9 +448,13 @@ const da = { upload: 'Upload', cancel: 'Annuller', delete: 'Slet', + deleteConfirm: 'Fjern denne skrifttype fra designet?', uploadError: 'Skriftfilen kunne ikke indlaeses', aliasHint: 'ZPL-alias for denne etiket (1 tegn, A-Z eller 0-9)', aliasAssigned: 'Tildelt ZPL-alias for denne etiket', + manualMappingsHeading: 'Skrifttyper på printeren', + manualMappingsHint: 'Reference til skrifttyper, der allerede er på printeren, men ikke uploadet her.', + addManualMapping: 'Tilføj printerskrifttype', }, zebraPrint: { heading: 'Send til Zebra-printer', diff --git a/src/locales/de.ts b/src/locales/de.ts index aabda751..8ad08141 100644 --- a/src/locales/de.ts +++ b/src/locales/de.ts @@ -469,9 +469,13 @@ const de = { upload: 'Hochladen', cancel: 'Abbrechen', delete: 'Löschen', + deleteConfirm: 'Diese Schrift aus dem Design entfernen?', uploadError: 'Schriftdatei konnte nicht geladen werden', aliasHint: 'ZPL-Alias für dieses Label (1 Zeichen, A-Z oder 0-9)', aliasAssigned: 'Zugewiesener ZPL-Alias für dieses Label', + manualMappingsHeading: 'Schriften auf dem Drucker', + manualMappingsHint: 'Schriften referenzieren, die schon auf dem Drucker liegen, aber hier nicht hochgeladen sind.', + addManualMapping: 'Drucker-Schrift hinzufügen', }, } as const; diff --git a/src/locales/el.ts b/src/locales/el.ts index 847d6fdb..f4ad8599 100644 --- a/src/locales/el.ts +++ b/src/locales/el.ts @@ -448,9 +448,13 @@ const el = { upload: 'Μεταφόρτωση', cancel: 'Ακύρωση', delete: 'Διαγραφή', + deleteConfirm: 'Αφαίρεση αυτής της γραμματοσειράς από τη σχεδίαση;', uploadError: 'Δεν ήταν δυνατή η φόρτωση του αρχείου γραμματοσειράς', aliasHint: 'Ψευδώνυμο ZPL για αυτή την ετικέτα (1 χαρακτήρας, A-Z ή 0-9)', aliasAssigned: 'Εκχωρημένο ψευδώνυμο ZPL για αυτή την ετικέτα', + manualMappingsHeading: 'Γραμματοσειρές στον εκτυπωτή', + manualMappingsHint: 'Αναφορά σε γραμματοσειρές που βρίσκονται ήδη στον εκτυπωτή αλλά δεν έχουν μεταφορτωθεί εδώ.', + addManualMapping: 'Προσθήκη γραμματοσειράς εκτυπωτή', }, zebraPrint: { heading: 'Αποστολή σε εκτυπωτή Zebra', diff --git a/src/locales/en.ts b/src/locales/en.ts index 9f272dc0..bfd0e2e6 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -469,9 +469,13 @@ const en = { upload: 'Upload', cancel: 'Cancel', delete: 'Delete', + deleteConfirm: 'Remove this font from the design?', uploadError: 'Could not load font file', aliasHint: 'ZPL alias for this label (1 char, A-Z or 0-9)', aliasAssigned: 'Assigned ZPL alias for this label', + manualMappingsHeading: 'Printer-resident fonts', + manualMappingsHint: 'Reference fonts already on the printer that are not uploaded here.', + addManualMapping: 'Add printer font', }, } as const; diff --git a/src/locales/es.ts b/src/locales/es.ts index 2ee559a9..766e9ce4 100644 --- a/src/locales/es.ts +++ b/src/locales/es.ts @@ -448,9 +448,13 @@ const es = { upload: 'Subir', cancel: 'Cancelar', delete: 'Eliminar', + deleteConfirm: '¿Eliminar esta fuente del diseño?', uploadError: 'No se pudo cargar el archivo de fuente', aliasHint: 'Alias ZPL para esta etiqueta (1 carácter, A-Z o 0-9)', aliasAssigned: 'Alias ZPL asignado a esta etiqueta', + manualMappingsHeading: 'Fuentes residentes en la impresora', + manualMappingsHint: 'Referencia a fuentes ya presentes en la impresora pero no cargadas aquí.', + addManualMapping: 'Añadir fuente de impresora', }, zebraPrint: { heading: 'Enviar a impresora Zebra', diff --git a/src/locales/et.ts b/src/locales/et.ts index cd912601..7a8dadc5 100644 --- a/src/locales/et.ts +++ b/src/locales/et.ts @@ -448,9 +448,13 @@ const et = { upload: 'Laadi üles', cancel: 'Tühista', delete: 'Kustuta', + deleteConfirm: 'Eemaldada see font kujundusest?', uploadError: 'Fondifaili ei saanud laadida', aliasHint: 'ZPL alias selle sildi jaoks (1 märk, A-Z või 0-9)', aliasAssigned: 'Sellele sildile määratud ZPL alias', + manualMappingsHeading: 'Printeri fondid', + manualMappingsHint: 'Viide printeris juba olevatele fontidele, mida pole siia üles laaditud.', + addManualMapping: 'Lisa printeri font', }, zebraPrint: { heading: 'Saada Zebra printerile', diff --git a/src/locales/fa.ts b/src/locales/fa.ts index 13399e28..7c29ec99 100644 --- a/src/locales/fa.ts +++ b/src/locales/fa.ts @@ -448,9 +448,13 @@ const fa = { upload: 'بارگذاری', cancel: 'لغو', delete: 'حذف', + deleteConfirm: 'این فونت از طراحی حذف شود؟', uploadError: 'بارگذاری فایل فونت ممکن نبود', aliasHint: 'نام مستعار ZPL برای این برچسب (1 حرف، A-Z یا 0-9)', aliasAssigned: 'نام مستعار ZPL اختصاص‌داده‌شده برای این برچسب', + manualMappingsHeading: 'فونت‌های موجود در چاپگر', + manualMappingsHint: 'ارجاع به فونت‌هایی که از قبل روی چاپگر هستند و اینجا بارگذاری نشده‌اند.', + addManualMapping: 'افزودن فونت چاپگر', }, zebraPrint: { heading: 'ارسال به چاپگر Zebra', diff --git a/src/locales/fi.ts b/src/locales/fi.ts index 32a3600e..1abca220 100644 --- a/src/locales/fi.ts +++ b/src/locales/fi.ts @@ -448,9 +448,13 @@ const fi = { upload: 'Lataa', cancel: 'Peruuta', delete: 'Poista', + deleteConfirm: 'Poista tämä fontti suunnittelusta?', uploadError: 'Fonttitiedostoa ei voitu ladata', aliasHint: 'ZPL-alias tälle etiketille (1 merkki, A-Z tai 0-9)', aliasAssigned: 'Tälle etiketille määritetty ZPL-alias', + manualMappingsHeading: 'Fontit tulostimessa', + manualMappingsHint: 'Viittaa fontteihin, jotka ovat jo tulostimessa mutta joita ei ole ladattu tänne.', + addManualMapping: 'Lisää tulostimen fontti', }, zebraPrint: { heading: 'Lähetä Zebra-tulostimelle', diff --git a/src/locales/fr.ts b/src/locales/fr.ts index 6667df55..5e0d9299 100644 --- a/src/locales/fr.ts +++ b/src/locales/fr.ts @@ -448,9 +448,13 @@ const fr = { upload: 'Téléverser', cancel: 'Annuler', delete: 'Supprimer', + deleteConfirm: 'Retirer cette police du design ?', uploadError: 'Impossible de charger le fichier de police', aliasHint: 'Alias ZPL pour cette étiquette (1 caractère, A-Z ou 0-9)', aliasAssigned: 'Alias ZPL assigné pour cette étiquette', + manualMappingsHeading: 'Polices résidentes', + manualMappingsHint: 'Référencer des polices déjà présentes sur l\'imprimante mais non téléversées ici.', + addManualMapping: 'Ajouter une police d\'imprimante', }, zebraPrint: { heading: 'Envoyer à l’imprimante Zebra', diff --git a/src/locales/he.ts b/src/locales/he.ts index 9c293aa8..b9efb583 100644 --- a/src/locales/he.ts +++ b/src/locales/he.ts @@ -448,9 +448,13 @@ const he = { upload: 'העלה', cancel: 'ביטול', delete: 'מחק', + deleteConfirm: 'להסיר גופן זה מהעיצוב?', uploadError: 'לא ניתן לטעון את קובץ הגופן', aliasHint: 'כינוי ZPL לתווית זו (תו אחד, A-Z או 0-9)', aliasAssigned: 'כינוי ZPL מוקצה לתווית זו', + manualMappingsHeading: 'גופנים השמורים במדפסת', + manualMappingsHint: 'התייחסות לגופנים שכבר נמצאים במדפסת ולא הועלו כאן.', + addManualMapping: 'הוסף גופן מדפסת', }, zebraPrint: { heading: 'שלח למדפסת Zebra', diff --git a/src/locales/hr.ts b/src/locales/hr.ts index d11aa4b0..0f4d136c 100644 --- a/src/locales/hr.ts +++ b/src/locales/hr.ts @@ -448,9 +448,13 @@ const hr = { upload: 'Prenesi', cancel: 'Odustani', delete: 'Izbriši', + deleteConfirm: 'Ukloniti ovaj font iz dizajna?', uploadError: 'Datoteka fonta nije mogla biti učitana', aliasHint: 'ZPL alias za ovu etiketu (1 znak, A-Z ili 0-9)', aliasAssigned: 'Dodijeljen ZPL alias za ovu etiketu', + manualMappingsHeading: 'Fontovi na pisaču', + manualMappingsHint: 'Referenca na fontove koji se već nalaze na pisaču, ali nisu prenijeti ovdje.', + addManualMapping: 'Dodaj font pisača', }, zebraPrint: { heading: 'Pošalji na Zebra pisač', diff --git a/src/locales/hu.ts b/src/locales/hu.ts index 15539924..638d2532 100644 --- a/src/locales/hu.ts +++ b/src/locales/hu.ts @@ -448,9 +448,13 @@ const hu = { upload: 'Feltöltés', cancel: 'Mégse', delete: 'Törlés', + deleteConfirm: 'Eltávolítja ezt a betűtípust a tervből?', uploadError: 'A betűtípus-fájl nem tölthető be', aliasHint: 'ZPL alias ehhez a címkéhez (1 karakter, A-Z vagy 0-9)', aliasAssigned: 'Hozzárendelt ZPL alias ehhez a címkéhez', + manualMappingsHeading: 'Betűtípusok a nyomtatón', + manualMappingsHint: 'Hivatkozás a nyomtatón már lévő, itt fel nem töltött betűtípusokra.', + addManualMapping: 'Nyomtató betűtípus hozzáadása', }, zebraPrint: { heading: 'Küldés Zebra nyomtatóra', diff --git a/src/locales/it.ts b/src/locales/it.ts index 9588cfdd..cf90ac81 100644 --- a/src/locales/it.ts +++ b/src/locales/it.ts @@ -448,9 +448,13 @@ const it = { upload: 'Carica', cancel: 'Annulla', delete: 'Elimina', + deleteConfirm: 'Rimuovere questo font dal design?', uploadError: 'Impossibile caricare il file del carattere', aliasHint: 'Alias ZPL per questa etichetta (1 carattere, A-Z o 0-9)', aliasAssigned: 'Alias ZPL assegnato a questa etichetta', + manualMappingsHeading: 'Font residenti sulla stampante', + manualMappingsHint: 'Riferimento a font già presenti sulla stampante ma non caricati qui.', + addManualMapping: 'Aggiungi font della stampante', }, zebraPrint: { heading: 'Invia a stampante Zebra', diff --git a/src/locales/ja.ts b/src/locales/ja.ts index 89d9bf95..23ec772e 100644 --- a/src/locales/ja.ts +++ b/src/locales/ja.ts @@ -448,9 +448,13 @@ const ja = { upload: 'アップロード', cancel: 'キャンセル', delete: '削除', + deleteConfirm: 'このフォントをデザインから削除しますか?', uploadError: 'フォントファイルを読み込めませんでした', aliasHint: 'このラベルの ZPL エイリアス (1 文字, A-Z または 0-9)', aliasAssigned: 'このラベルに割り当てられた ZPL エイリアス', + manualMappingsHeading: 'プリンター内蔵フォント', + manualMappingsHint: 'プリンターに既にあるがここにアップロードされていないフォントを参照します。', + addManualMapping: 'プリンターフォントを追加', }, zebraPrint: { heading: 'Zebra プリンターへ送信', diff --git a/src/locales/ko.ts b/src/locales/ko.ts index 734718cb..4f907395 100644 --- a/src/locales/ko.ts +++ b/src/locales/ko.ts @@ -448,9 +448,13 @@ const ko = { upload: '업로드', cancel: '취소', delete: '삭제', + deleteConfirm: '이 글꼴을 디자인에서 제거하시겠습니까?', uploadError: '글꼴 파일을 불러올 수 없습니다', aliasHint: '이 라벨의 ZPL 별칭 (1 글자, A-Z 또는 0-9)', aliasAssigned: '이 라벨에 할당된 ZPL 별칭', + manualMappingsHeading: '프린터 내 글꼴', + manualMappingsHint: '프린터에 이미 있지만 여기에 업로드되지 않은 글꼴을 참조합니다.', + addManualMapping: '프린터 글꼴 추가', }, zebraPrint: { heading: 'Zebra 프린터로 전송', diff --git a/src/locales/lt.ts b/src/locales/lt.ts index b167ac0a..4057e9e5 100644 --- a/src/locales/lt.ts +++ b/src/locales/lt.ts @@ -448,9 +448,13 @@ const lt = { upload: 'Įkelti', cancel: 'Atšaukti', delete: 'Ištrinti', + deleteConfirm: 'Pašalinti šį šriftą iš dizaino?', uploadError: 'Nepavyko įkelti šrifto failo', aliasHint: 'ZPL alias šiai etiketei (1 simbolis, A-Z arba 0-9)', aliasAssigned: 'Šiai etiketei priskirtas ZPL alias', + manualMappingsHeading: 'Spausdintuve esantys šriftai', + manualMappingsHint: 'Nuoroda į spausdintuve jau esančius šriftus, kurie čia neįkelti.', + addManualMapping: 'Pridėti spausdintuvo šriftą', }, zebraPrint: { heading: 'Siųsti į Zebra spausdintuvą', diff --git a/src/locales/lv.ts b/src/locales/lv.ts index f23c5938..3fd699d7 100644 --- a/src/locales/lv.ts +++ b/src/locales/lv.ts @@ -448,9 +448,13 @@ const lv = { upload: 'Augšupielādēt', cancel: 'Atcelt', delete: 'Dzēst', + deleteConfirm: 'Vai noņemt šo fontu no dizaina?', uploadError: 'Neizdevās ielādēt fontu failu', aliasHint: 'ZPL aliass šai etiķetei (1 rakstzīme, A-Z vai 0-9)', aliasAssigned: 'Šai etiķetei piešķirts ZPL aliass', + manualMappingsHeading: 'Printera fonti', + manualMappingsHint: 'Atsauce uz fontiem, kas jau ir printerī, bet nav augšupielādēti šeit.', + addManualMapping: 'Pievienot printera fontu', }, zebraPrint: { heading: 'Sūtīt uz Zebra printeri', diff --git a/src/locales/nl.ts b/src/locales/nl.ts index f2a5f087..7aa78c1e 100644 --- a/src/locales/nl.ts +++ b/src/locales/nl.ts @@ -448,9 +448,13 @@ const nl = { upload: 'Uploaden', cancel: 'Annuleren', delete: 'Verwijderen', + deleteConfirm: 'Dit lettertype uit het ontwerp verwijderen?', uploadError: 'Lettertypebestand kon niet worden geladen', aliasHint: 'ZPL-alias voor dit label (1 teken, A-Z of 0-9)', aliasAssigned: 'Toegewezen ZPL-alias voor dit label', + manualMappingsHeading: 'Lettertypen op de printer', + manualMappingsHint: 'Verwijs naar lettertypen die al op de printer staan maar hier niet zijn geüpload.', + addManualMapping: 'Printerlettertype toevoegen', }, zebraPrint: { heading: 'Verzenden naar Zebra-printer', diff --git a/src/locales/no.ts b/src/locales/no.ts index 0a081e53..aecfa9dd 100644 --- a/src/locales/no.ts +++ b/src/locales/no.ts @@ -448,9 +448,13 @@ const no = { upload: 'Last opp', cancel: 'Avbryt', delete: 'Slett', + deleteConfirm: 'Fjern denne skriften fra designet?', uploadError: 'Klarte ikke laste inn skriftfilen', aliasHint: 'ZPL-alias for denne etiketten (1 tegn, A-Z eller 0-9)', aliasAssigned: 'Tilordnet ZPL-alias for denne etiketten', + manualMappingsHeading: 'Skrifter på skriveren', + manualMappingsHint: 'Referer til skrifter som allerede ligger på skriveren, men ikke er lastet opp her.', + addManualMapping: 'Legg til skriverskrift', }, zebraPrint: { heading: 'Send til Zebra-skriver', diff --git a/src/locales/pl.ts b/src/locales/pl.ts index cceafdae..b0f798a0 100644 --- a/src/locales/pl.ts +++ b/src/locales/pl.ts @@ -448,9 +448,13 @@ const pl = { upload: 'Prześlij', cancel: 'Anuluj', delete: 'Usuń', + deleteConfirm: 'Usunąć tę czcionkę z projektu?', uploadError: 'Nie można załadować pliku czcionki', aliasHint: 'Alias ZPL dla tej etykiety (1 znak, A-Z lub 0-9)', aliasAssigned: 'Przypisany alias ZPL dla tej etykiety', + manualMappingsHeading: 'Czcionki w pamięci drukarki', + manualMappingsHint: 'Odwołuje się do czcionek już w drukarce, niewgranych tutaj.', + addManualMapping: 'Dodaj czcionkę drukarki', }, zebraPrint: { heading: 'Wyślij do drukarki Zebra', diff --git a/src/locales/pt.ts b/src/locales/pt.ts index cb3bef26..16b93c62 100644 --- a/src/locales/pt.ts +++ b/src/locales/pt.ts @@ -448,9 +448,13 @@ const pt = { upload: 'Carregar', cancel: 'Cancelar', delete: 'Excluir', + deleteConfirm: 'Remover esta fonte do design?', uploadError: 'Não foi possível carregar o arquivo de fonte', aliasHint: 'Alias ZPL para esta etiqueta (1 caractere, A-Z ou 0-9)', aliasAssigned: 'Alias ZPL atribuído a esta etiqueta', + manualMappingsHeading: 'Fontes residentes na impressora', + manualMappingsHint: 'Referencia fontes já presentes na impressora mas não enviadas aqui.', + addManualMapping: 'Adicionar fonte da impressora', }, zebraPrint: { heading: 'Enviar para impressora Zebra', diff --git a/src/locales/ro.ts b/src/locales/ro.ts index 5c0f1aeb..724edb1d 100644 --- a/src/locales/ro.ts +++ b/src/locales/ro.ts @@ -448,9 +448,13 @@ const ro = { upload: 'Încarcă', cancel: 'Anulează', delete: 'Șterge', + deleteConfirm: 'Eliminați acest font din design?', uploadError: 'Fisierul de font nu a putut fi incarcat', aliasHint: 'Alias ZPL pentru această etichetă (1 caracter, A-Z sau 0-9)', aliasAssigned: 'Alias ZPL atribuit acestei etichete', + manualMappingsHeading: 'Fonturi din imprimantă', + manualMappingsHint: 'Referință la fonturi deja prezente pe imprimantă, neîncărcate aici.', + addManualMapping: 'Adaugă font imprimantă', }, zebraPrint: { heading: 'Trimite la imprimanta Zebra', diff --git a/src/locales/sk.ts b/src/locales/sk.ts index 67fb9ae6..7b5dac04 100644 --- a/src/locales/sk.ts +++ b/src/locales/sk.ts @@ -448,9 +448,13 @@ const sk = { upload: 'Nahrať', cancel: 'Zrušiť', delete: 'Odstrániť', + deleteConfirm: 'Odstrániť toto písmo z návrhu?', uploadError: 'Súbor písma sa nepodarilo načítať', aliasHint: 'ZPL alias pre tento štítok (1 znak, A-Z alebo 0-9)', aliasAssigned: 'Priradený ZPL alias pre tento štítok', + manualMappingsHeading: 'Písma uložené v tlačiarni', + manualMappingsHint: 'Odkaz na písma, ktoré sú už v tlačiarni, ale tu nie sú nahrané.', + addManualMapping: 'Pridať písmo tlačiarne', }, zebraPrint: { heading: 'Odoslať na tlačiareň Zebra', diff --git a/src/locales/sl.ts b/src/locales/sl.ts index 6e08fc4e..3e9f50d3 100644 --- a/src/locales/sl.ts +++ b/src/locales/sl.ts @@ -448,9 +448,13 @@ const sl = { upload: 'Naloži', cancel: 'Prekliči', delete: 'Izbriši', + deleteConfirm: 'Odstrani to pisavo iz oblikovanja?', uploadError: 'Datoteke pisave ni bilo mogoče naložiti', aliasHint: 'ZPL vzdevek za to etiketo (1 znak, A-Z ali 0-9)', aliasAssigned: 'Dodeljen ZPL vzdevek za to etiketo', + manualMappingsHeading: 'Pisave v tiskalniku', + manualMappingsHint: 'Sklicevanje na pisave, ki so že v tiskalniku, vendar tukaj niso naložene.', + addManualMapping: 'Dodaj pisavo tiskalnika', }, zebraPrint: { heading: 'Pošlji na tiskalnik Zebra', diff --git a/src/locales/sr.ts b/src/locales/sr.ts index e5695aed..74b8a9d4 100644 --- a/src/locales/sr.ts +++ b/src/locales/sr.ts @@ -448,9 +448,13 @@ const sr = { upload: 'Отпреми', cancel: 'Откажи', delete: 'Избриши', + deleteConfirm: 'Уклонити овај фонт из дизајна?', uploadError: 'Датотека фонта није могла бити учитана', aliasHint: 'ZPL алијас за ову етикету (1 знак, A-Z или 0-9)', aliasAssigned: 'Додељен ZPL алијас за ову етикету', + manualMappingsHeading: 'Фонтови на штампачу', + manualMappingsHint: 'Референца на фонтове који се већ налазе на штампачу, али нису пренети овде.', + addManualMapping: 'Додај фонт штампача', }, zebraPrint: { heading: 'Пошаљи на Zebra штампач', diff --git a/src/locales/sv.ts b/src/locales/sv.ts index 0fd17a4c..4a1749fa 100644 --- a/src/locales/sv.ts +++ b/src/locales/sv.ts @@ -448,9 +448,13 @@ const sv = { upload: 'Ladda upp', cancel: 'Avbryt', delete: 'Ta bort', + deleteConfirm: 'Ta bort detta typsnitt från designen?', uploadError: 'Det gick inte att ladda typsnittsfilen', aliasHint: 'ZPL-alias för denna etikett (1 tecken, A-Z eller 0-9)', aliasAssigned: 'Tilldelat ZPL-alias för denna etikett', + manualMappingsHeading: 'Typsnitt i skrivaren', + manualMappingsHint: 'Referera till typsnitt som redan finns på skrivaren men inte är uppladdade här.', + addManualMapping: 'Lägg till skrivartypsnitt', }, zebraPrint: { heading: 'Skicka till Zebra-skrivare', diff --git a/src/locales/tr.ts b/src/locales/tr.ts index 7759d842..ea6e1f2b 100644 --- a/src/locales/tr.ts +++ b/src/locales/tr.ts @@ -448,9 +448,13 @@ const tr = { upload: 'Yükle', cancel: 'İptal', delete: 'Sil', + deleteConfirm: 'Bu yazı tipi tasarımdan kaldırılsın mı?', uploadError: 'Yazı tipi dosyası yüklenemedi', aliasHint: 'Bu etiket için ZPL takma adı (1 karakter, A-Z veya 0-9)', aliasAssigned: 'Bu etikete atanmış ZPL takma adı', + manualMappingsHeading: 'Yazıcıda kayıtlı yazı tipleri', + manualMappingsHint: 'Yazıcıda zaten bulunan ancak buraya yüklenmemiş yazı tiplerine referans verir.', + addManualMapping: 'Yazıcı yazı tipi ekle', }, zebraPrint: { heading: 'Zebra yazıcıya gönder', diff --git a/src/locales/zh-hans.ts b/src/locales/zh-hans.ts index f0ab5ef1..1456e645 100644 --- a/src/locales/zh-hans.ts +++ b/src/locales/zh-hans.ts @@ -448,9 +448,13 @@ const zhHans = { upload: '上传', cancel: '取消', delete: '删除', + deleteConfirm: '从设计中移除此字体?', uploadError: '无法加载字体文件', aliasHint: '此标签的 ZPL 别名 (1 字符, A-Z 或 0-9)', aliasAssigned: '此标签的已分配 ZPL 别名', + manualMappingsHeading: '打印机内置字体', + manualMappingsHint: '引用打印机中已有但未在此上传的字体。', + addManualMapping: '添加打印机字体', }, zebraPrint: { heading: '发送到 Zebra 打印机', diff --git a/src/locales/zh-hant.ts b/src/locales/zh-hant.ts index 63e3769b..60c954a1 100644 --- a/src/locales/zh-hant.ts +++ b/src/locales/zh-hant.ts @@ -448,9 +448,13 @@ const zhHant = { upload: '上傳', cancel: '取消', delete: '刪除', + deleteConfirm: '從設計中移除此字型?', uploadError: '無法載入字體檔案', aliasHint: '此標籤的 ZPL 別名 (1 字元, A-Z 或 0-9)', aliasAssigned: '此標籤的已指派 ZPL 別名', + manualMappingsHeading: '印表機內建字型', + manualMappingsHint: '參照印表機中已有但未在此上傳的字型。', + addManualMapping: '新增印表機字型', }, zebraPrint: { heading: '傳送至 Zebra 印表機', From 0f74f5d068547aacc2593e74b171e5745345b3fa Mon Sep 17 00:00:00 2001 From: u8array Date: Wed, 20 May 2026 00:18:00 +0200 Subject: [PATCH 5/6] test(zpl): cover all rotations in the ^A@ alias-rewrite regex The auto-rewrite regex accepts [NIRB] but only `N` was exercised. Convert the test to it.each so a future change that, say, breaks the rotation capture group fails on the matrix instead of slipping through with the still-passing happy path. --- src/lib/zplGenerator.test.ts | 55 +++++++++++++++++++----------------- 1 file changed, 29 insertions(+), 26 deletions(-) diff --git a/src/lib/zplGenerator.test.ts b/src/lib/zplGenerator.test.ts index 67cf1277..3aa9ca3a 100644 --- a/src/lib/zplGenerator.test.ts +++ b/src/lib/zplGenerator.test.ts @@ -236,32 +236,35 @@ describe('generateZPL — printer params', () => { expect(zpl).toContain('^CWB,E:OK.TTF'); }); - it('rewrites ^A@ font refs to ^A{alias} when a ^CW mapping exists', () => { - const text: LabelObject = { - id: 't1', - type: 'text', - x: 10, - y: 10, - rotation: 0, - props: { - content: 'hi', - rotation: 'N', - fontHeight: 30, - fontWidth: 0, - printerFontName: 'ARIAL.TTF', - }, - }; - const zpl = generateZPL( - { - ...BASE_LABEL, - customFonts: [{ alias: 'M', path: 'E:ARIAL.TTF' }], - }, - [text], - ); - expect(zpl).toContain('^CWM,E:ARIAL.TTF'); - expect(zpl).toContain('^AMN,30,0'); - expect(zpl).not.toContain('^A@N,30,0,E:ARIAL.TTF'); - }); + it.each(['N', 'R', 'I', 'B'] as const)( + 'rewrites ^A@%s to ^A{alias} when a matching ^CW mapping exists', + (rotation) => { + const text: LabelObject = { + id: 't1', + type: 'text', + x: 10, + y: 10, + rotation: 0, + props: { + content: 'hi', + rotation, + fontHeight: 30, + fontWidth: 0, + printerFontName: 'ARIAL.TTF', + }, + }; + const zpl = generateZPL( + { + ...BASE_LABEL, + customFonts: [{ alias: 'M', path: 'E:ARIAL.TTF' }], + }, + [text], + ); + expect(zpl).toContain('^CWM,E:ARIAL.TTF'); + expect(zpl).toContain(`^AM${rotation},30,0`); + expect(zpl).not.toContain(`^A@${rotation},30,0,E:ARIAL.TTF`); + }, + ); it('leaves ^A@ verbose when no matching ^CW alias is defined', () => { const text: LabelObject = { From 6bab3377c4b265556171c5625ae0df6b04a32948 Mon Sep 17 00:00:00 2001 From: u8array Date: Wed, 20 May 2026 00:26:21 +0200 Subject: [PATCH 6/6] fix(fonts): address Gemini PR review on ^CW handling - ManualMappingsSection: handleBlur now checks row.contains(document.activeElement) so tabbing between sibling inputs no longer removes the row mid-traversal. Row key falls back to the auto-assigned alias rather than the unstable list index, so React preserves identity across deletes. - Generator: ^A@ rewrite regex accepts any [A-Z]: drive prefix instead of hardcoding E:. Current text emit still uses E: only, but the rewrite stays correct if other drives appear via import or a future text emit change. - Parser: ^CW now upserts by alias instead of appending. Two ^CW lines for the same alias collapse to the final mapping, matching the runtime fontAliases.set last-wins semantics. Multi-alias mappings sharing the same path are still kept separate. Addresses Gemini review comments on #75. --- src/components/Fonts/FontManager.tsx | 28 +++++++++++++++++++++------- src/lib/zplGenerator.test.ts | 17 +++++++++++++++++ src/lib/zplGenerator.ts | 14 +++++++------- src/lib/zplParser.test.ts | 24 ++++++++++++++++++++++++ src/lib/zplParser.ts | 9 +++++++-- 5 files changed, 76 insertions(+), 16 deletions(-) diff --git a/src/components/Fonts/FontManager.tsx b/src/components/Fonts/FontManager.tsx index 6f6773f7..6c0b72a0 100644 --- a/src/components/Fonts/FontManager.tsx +++ b/src/components/Fonts/FontManager.tsx @@ -1,4 +1,4 @@ -import { useRef, useState, useCallback } from 'react'; +import { useRef, useState, useCallback, type FocusEvent } from 'react'; import { PlusIcon, TrashIcon } from '@heroicons/react/16/solid'; import { getAllFonts, loadFontFile, removeFont } from '../../lib/fontCache'; import { useFontCacheVersion } from '../../hooks/useFontCacheVersion'; @@ -256,11 +256,20 @@ function ManualMappingsSection({ }: ManualMappingsSectionProps) { const t = useT(); // Auto-remove rows whose alias AND path are both empty when focus - // leaves them. requestAnimationFrame defers the check so tabbing - // between sibling inputs does not delete the row mid-traversal. - const handleBlur = (path: string, alias: string, value: string) => { + // actually leaves the row container. requestAnimationFrame defers + // the check until the new focus has landed, then we confirm the row + // no longer contains it — tabbing between the row's own inputs does + // not count as "leaving". + const handleBlur = ( + e: FocusEvent, + path: string, + alias: string, + ) => { + const row = e.currentTarget; requestAnimationFrame(() => { - if (!alias && !value) onRemove(path); + if (!alias && !path && !row.contains(document.activeElement)) { + onRemove(path); + } }); }; @@ -269,11 +278,16 @@ function ManualMappingsSection({

{hint}

{mappings.map((m) => { const dup = isDuplicateAlias(m.alias); + // Key: stable across edits as long as alias and path don't + // change at the same time. For fresh empty rows the auto- + // assigned alias from nextFreeAlias is unique per row, which + // gives a stable identity until the user types a path. + const rowKey = m.path || `__alias__${m.alias}`; return (
handleBlur(m.path, m.alias, m.path)} + onBlur={(e) => handleBlur(e, m.path, m.alias)} > { }, ); + it('rewrites ^A@ refs across any drive prefix the path uses', () => { + // The path is whatever the customFonts entry stores. Even if our + // text emit only ever writes E:, an imported label could carry + // R: / A: / B: paths that still need to be matched on re-emit. + const rRef = '^XA^FO0,0^A@N,30,0,R:FOO.TTF^FDhi^FS^XZ'; + const aliasByPath: Record = { 'R:FOO.TTF': 'Q' }; + const rewritten = rRef.replace( + /\^A@([NIRB]),(\d+),(\d+),([A-Z]:[^^\n]+?)(?=\^|\n|$)/g, + (full, rot, h, w, path) => { + const alias = aliasByPath[path]; + return alias ? `^A${alias}${rot},${h},${w}` : full; + }, + ); + expect(rewritten).toContain('^AQN,30,0'); + expect(rewritten).not.toContain('^A@N,30,0,R:FOO.TTF'); + }); + it('leaves ^A@ verbose when no matching ^CW alias is defined', () => { const text: LabelObject = { id: 't1', diff --git a/src/lib/zplGenerator.ts b/src/lib/zplGenerator.ts index 9b520700..35781d4b 100644 --- a/src/lib/zplGenerator.ts +++ b/src/lib/zplGenerator.ts @@ -141,12 +141,12 @@ export function generateZPL(label: LabelConfig, objects: LabelObject[]): string lines.push('^XZ'); - // Rewrite ^A@...E:NAME.TTF references to ^A{alias} for paths that the - // user has registered via ^CW. The ^CW lines are already in the - // header, so the printer resolves the short form against the alias - // table. Saves bytes and surfaces the user's alias choices in the - // output. ^A@ is the only emit pattern that references font files - // verbatim, so the regex stays scoped to a single shape. + // Rewrite ^A@...{drive}:NAME.TTF references to ^A{alias} for paths + // that the user has registered via ^CW. The ^CW lines are already + // in the header, so the printer resolves the short form against the + // alias table. Saves bytes and surfaces the user's alias choices in + // the output. The drive prefix pattern is open ([A-Z]:) so the + // rewrite keeps working if text emit ever supports non-E drives. const aliasByPath = new Map(); for (const m of label.customFonts ?? []) { if (m.alias) aliasByPath.set(m.path, m.alias); @@ -154,7 +154,7 @@ export function generateZPL(label: LabelConfig, objects: LabelObject[]): string let output = lines.join('\n'); if (aliasByPath.size > 0) { output = output.replace( - /\^A@([NIRB]),(\d+),(\d+),(E:[^^\n]+?)(?=\^|\n|$)/g, + /\^A@([NIRB]),(\d+),(\d+),([A-Z]:[^^\n]+?)(?=\^|\n|$)/g, (full, rot, h, w, path) => { const alias = aliasByPath.get(path); return alias ? `^A${alias}${rot},${h},${w}` : full; diff --git a/src/lib/zplParser.test.ts b/src/lib/zplParser.test.ts index 1e76e987..98fc8a90 100644 --- a/src/lib/zplParser.test.ts +++ b/src/lib/zplParser.test.ts @@ -648,6 +648,30 @@ describe('parseZPL — printer params', () => { expect(labelConfig.customFonts).toBeUndefined(); }); + it('upserts ^CW by alias, keeping the last mapping per alias', () => { + // Two ^CW lines for the same alias: the second should overwrite + // the first in customFonts, matching the runtime fontAliases.set + // last-wins semantics. + const { labelConfig } = parseZPL( + '^XA^CWM,E:OLD.TTF^CWM,E:NEW.TTF^XZ', + 8, + ); + expect(labelConfig.customFonts).toEqual([ + { alias: 'M', path: 'E:NEW.TTF' }, + ]); + }); + + it('keeps separate ^CW mappings that share a path but use different aliases', () => { + const { labelConfig } = parseZPL( + '^XA^CWM,E:FOO.TTF^CWN,E:FOO.TTF^XZ', + 8, + ); + expect(labelConfig.customFonts).toEqual([ + { alias: 'M', path: 'E:FOO.TTF' }, + { alias: 'N', path: 'E:FOO.TTF' }, + ]); + }); + it('parses ~SD instant darkness', () => { expect(parseZPL('~SD07^XA^XZ', 8).labelConfig.instantDarkness).toBe(7); expect(parseZPL('~SD30^XA^XZ', 8).labelConfig.instantDarkness).toBe(30); diff --git a/src/lib/zplParser.ts b/src/lib/zplParser.ts index 8184daf6..a5661a57 100644 --- a/src/lib/zplParser.ts +++ b/src/lib/zplParser.ts @@ -1294,13 +1294,18 @@ export function parseZPL(zpl: string, dpmm = 8): ParsedZPL { // ^CW {alias},{path} — register an alias for a printer-resident font. // Subsequent ^A{alias} fields resolve to {path} via the fontAliases // map. The mapping is also persisted on labelConfig so the generator - // can re-emit it on round-trip. + // can re-emit it on round-trip. Upsert by alias mirrors the + // Map-set semantics of fontAliases: a later ^CW for the same alias + // replaces the earlier mapping rather than accumulating duplicates. CW(p) { const alias = (p[0] ?? "").trim().toUpperCase(); const path = (p[1] ?? "").trim(); if (!/^[A-Z0-9]$/.test(alias) || !path) return; fontAliases.set(alias, path); - (labelConfig.customFonts ??= []).push({ alias, path }); + const list = (labelConfig.customFonts ?? []).filter( + (m) => m.alias !== alias, + ); + labelConfig.customFonts = [...list, { alias, path }]; }, // ── Browser-limit: printer-specific features ────────────────────────────