From 61322559e6cdbcb09bc00c07b7e55c55c8de5708 Mon Sep 17 00:00:00 2001 From: u8array Date: Tue, 21 Apr 2026 18:05:22 +0200 Subject: [PATCH 1/3] feat: Add support for custom printer fonts Introduces a new font caching mechanism to handle custom printer fonts. This allows users to upload TTF/OTF font files that will be registered with the browser and used for text rendering. The font cache persists fonts in localStorage and provides a hook for components to subscribe to cache changes. --- src/components/Canvas/KonvaObject.tsx | 29 ++----- src/lib/fontCache.ts | 113 ++++++++++++++++++++++++++ src/lib/zplParser.ts | 9 ++ src/locales/ar.ts | 6 ++ src/locales/bg.ts | 6 ++ src/locales/cs.ts | 6 ++ src/locales/da.ts | 6 ++ src/locales/de.ts | 6 ++ src/locales/el.ts | 6 ++ src/locales/en.ts | 6 ++ src/locales/es.ts | 6 ++ src/locales/et.ts | 6 ++ src/locales/fa.ts | 6 ++ src/locales/fi.ts | 6 ++ src/locales/fr.ts | 6 ++ src/locales/he.ts | 6 ++ src/locales/hr.ts | 6 ++ src/locales/hu.ts | 6 ++ src/locales/it.ts | 6 ++ src/locales/ja.ts | 6 ++ src/locales/ko.ts | 6 ++ src/locales/lt.ts | 6 ++ src/locales/lv.ts | 6 ++ src/locales/nl.ts | 6 ++ src/locales/no.ts | 6 ++ src/locales/pl.ts | 6 ++ src/locales/pt.ts | 6 ++ src/locales/ro.ts | 6 ++ src/locales/sk.ts | 6 ++ src/locales/sl.ts | 6 ++ src/locales/sr.ts | 6 ++ src/locales/sv.ts | 6 ++ src/locales/tr.ts | 6 ++ src/locales/zh-hans.ts | 6 ++ src/locales/zh-hant.ts | 6 ++ src/registry/text.tsx | 62 +++++++++++++- 36 files changed, 381 insertions(+), 24 deletions(-) create mode 100644 src/lib/fontCache.ts diff --git a/src/components/Canvas/KonvaObject.tsx b/src/components/Canvas/KonvaObject.tsx index 40bc691..ab74fd0 100644 --- a/src/components/Canvas/KonvaObject.tsx +++ b/src/components/Canvas/KonvaObject.tsx @@ -1,4 +1,5 @@ import { useState, useEffect } from "react"; +import { getFontFamily } from "../../lib/fontCache"; import { Circle, Ellipse, @@ -397,27 +398,6 @@ function KonvaObjectInner({ } else if (p.rotation === "B") { displayX -= renderedH; } - } else if ( - obj.type === "code128" || - obj.type === "code39" || - obj.type === "ean13" || - obj.type === "upca" || - obj.type === "ean8" || - obj.type === "upce" || - obj.type === "interleaved2of5" || - obj.type === "code93" - ) { - const p = obj.props as { height: number }; - displayY -= p.height; - } else if (obj.type === "pdf417") { - const p = obj.props as { rowHeight: number }; - displayY -= p.rowHeight * 10; - } else if (obj.type === "qrcode") { - const p = obj.props as { magnification: number }; - displayY -= p.magnification * 25; - } else if (obj.type === "datamatrix") { - const p = obj.props as { dimension: number }; - displayY -= p.dimension * 20; } } @@ -462,6 +442,9 @@ function KonvaObjectInner({ if (obj.type === "text") { const p = obj.props; const fontSize = Math.max(dotsToPx(p.fontHeight, scale, dpmm) / 1.3, 6); + const fontFamily = p.printerFontName + ? (getFontFamily(p.printerFontName) ?? "'Roboto Condensed', sans-serif") + : "'Roboto Condensed', sans-serif"; const zplRotationDeg: Record = { N: 0, R: 90, @@ -496,7 +479,7 @@ function KonvaObjectInner({ (); +const listeners = new Set<() => void>(); + +function notify(): void { + listeners.forEach(fn => fn()); +} + +/** Subscribe to cache changes. Returns an unsubscribe function. */ +export function subscribe(fn: () => void): () => void { + listeners.add(fn); + return () => listeners.delete(fn); +} + +/** Hook: returns a version counter that increments whenever the font cache changes. */ +export function useFontCacheVersion(): number { + const [version, setVersion] = useState(0); + useEffect(() => subscribe(() => setVersion(v => v + 1)), []); + return version; +} + +function printerNameToFamily(name: string): string { + // Strip extension, prefix with "zpl-" to avoid collisions with system fonts + return 'zpl-' + name.replace(/\.[^.]+$/, '').toUpperCase(); +} + +async function registerFontFace(entry: CachedFont): Promise { + try { + const face = new FontFace(entry.fontFamily, `url(${entry.dataUrl})`); + await face.load(); + document.fonts.add(face); + } catch { + // Font invalid or API unavailable — canvas will fall back to default font + } +} + +// Hydrate from localStorage on module load +for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i); + if (!key?.startsWith(LS_PREFIX)) continue; + try { + const entry = JSON.parse(localStorage.getItem(key) ?? 'null') as CachedFont; + cache.set(entry.name, entry); + // Re-register fonts asynchronously — canvas renders after React mounts + void registerFontFace(entry); + } catch { + // ignore corrupt entries + } +} + +/** Look up a cached font by printer filename (case-insensitive). */ +export function getFont(printerName: string): CachedFont | undefined { + return cache.get(printerName.toUpperCase()); +} + +/** Return the CSS font-family for a printer font name, or undefined if not loaded. */ +export function getFontFamily(printerName: string): string | undefined { + return cache.get(printerName.toUpperCase())?.fontFamily; +} + +export function getAllFonts(): CachedFont[] { + return [...cache.values()]; +} + +/** Load a TTF/OTF File into the cache under the given printer font name. */ +export async function loadFontFile(file: File, printerName: string): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = async () => { + const dataUrl = reader.result as string; + const name = printerName.toUpperCase(); + const fontFamily = printerNameToFamily(name); + const entry: CachedFont = { id: crypto.randomUUID(), name, dataUrl, fontFamily }; + cache.set(name, entry); + try { + localStorage.setItem(LS_PREFIX + name, JSON.stringify(entry)); + } catch { + // localStorage full — font stays in memory only + } + await registerFontFace(entry); + notify(); + resolve(entry); + }; + reader.onerror = () => reject(new Error(`Failed to read font: ${file.name}`)); + reader.readAsDataURL(file); + }); +} + +export function removeFont(printerName: string): void { + const name = printerName.toUpperCase(); + cache.delete(name); + localStorage.removeItem(LS_PREFIX + name); + notify(); +} diff --git a/src/lib/zplParser.ts b/src/lib/zplParser.ts index f773c79..b5beb94 100644 --- a/src/lib/zplParser.ts +++ b/src/lib/zplParser.ts @@ -245,6 +245,9 @@ export function parseZPL(zpl: string, dpmm = 8): ParsedZPL { let cbRowHeight = 10; let cbSecurity: CodablockProps['securityLevel'] = 'Y'; + // ^A@ pending printer font name (e.g. "ARIAL.TTF") + let pendingPrinterFontName: string | undefined; + // ^SN / ^SF serialization state let snPending = false; let snIncrement = 1; @@ -286,7 +289,9 @@ export function parseZPL(zpl: string, dpmm = 8): ParsedZPL { fontWidth: textW, rotation: textRot, reverse: (lrActive || frActive) || undefined, + printerFontName: pendingPrinterFontName, }; + pendingPrinterFontName = undefined; if (fbWidth > 0) { textProps.blockWidth = fbWidth; textProps.blockLines = fbLines; @@ -1055,6 +1060,10 @@ 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; + // Extract font filename from "E:ARIAL.TTF" or "R:FONT.TTF" + const fontRef = p[3] ?? ''; + const colonIdx = fontRef.indexOf(':'); + pendingPrinterFontName = (colonIdx >= 0 ? fontRef.slice(colonIdx + 1) : fontRef) || undefined; partialCmds.add('^A@'); break; } diff --git a/src/locales/ar.ts b/src/locales/ar.ts index 0fd4295..5cbd6bd 100644 --- a/src/locales/ar.ts +++ b/src/locales/ar.ts @@ -109,6 +109,12 @@ const ar = { justifyC: 'C — وسط', justifyR: 'R — يمين', justifyJ: 'J — ضبط', + printerFont: 'Printer font (^A@)', + uploadFont: 'Upload font file', + uploadingFont: 'Uploading…', + replaceFont: 'Replace font', + fontLoaded: 'Font loaded', + fontMissing: 'Font not loaded', }, code128: { content: 'المحتوى', diff --git a/src/locales/bg.ts b/src/locales/bg.ts index a6628b5..db59620 100644 --- a/src/locales/bg.ts +++ b/src/locales/bg.ts @@ -109,6 +109,12 @@ const bg = { justifyC: 'C — Център', justifyR: 'R — Дясно', justifyJ: 'J — Двустранно', + printerFont: 'Printer font (^A@)', + uploadFont: 'Upload font file', + uploadingFont: 'Uploading…', + replaceFont: 'Replace font', + fontLoaded: 'Font loaded', + fontMissing: 'Font not loaded', }, code128: { content: 'Съдържание', diff --git a/src/locales/cs.ts b/src/locales/cs.ts index 84227a5..d7629e1 100644 --- a/src/locales/cs.ts +++ b/src/locales/cs.ts @@ -109,6 +109,12 @@ const cs = { justifyC: 'C — Na střed', justifyR: 'R — Vpravo', justifyJ: 'J — Do bloku', + printerFont: 'Printer font (^A@)', + uploadFont: 'Upload font file', + uploadingFont: 'Uploading…', + replaceFont: 'Replace font', + fontLoaded: 'Font loaded', + fontMissing: 'Font not loaded', }, code128: { content: 'Obsah', diff --git a/src/locales/da.ts b/src/locales/da.ts index dd23a2b..8734198 100644 --- a/src/locales/da.ts +++ b/src/locales/da.ts @@ -109,6 +109,12 @@ const da = { justifyC: 'C — Centreret', justifyR: 'R — Højre', justifyJ: 'J — Lige margener', + printerFont: 'Printer font (^A@)', + uploadFont: 'Upload font file', + uploadingFont: 'Uploading…', + replaceFont: 'Replace font', + fontLoaded: 'Font loaded', + fontMissing: 'Font not loaded', }, code128: { content: 'Indhold', diff --git a/src/locales/de.ts b/src/locales/de.ts index 1cd3f71..d14fc3f 100644 --- a/src/locales/de.ts +++ b/src/locales/de.ts @@ -109,6 +109,12 @@ const de = { justifyC: 'C — Zentriert', justifyR: 'R — Rechts', justifyJ: 'J — Blocksatz', + printerFont: 'Printer font (^A@)', + uploadFont: 'Upload font file', + uploadingFont: 'Uploading…', + replaceFont: 'Replace font', + fontLoaded: 'Font loaded', + fontMissing: 'Font not loaded', }, code128: { content: 'Inhalt', diff --git a/src/locales/el.ts b/src/locales/el.ts index c6e602b..969f1a7 100644 --- a/src/locales/el.ts +++ b/src/locales/el.ts @@ -109,6 +109,12 @@ const el = { justifyC: 'C — Κέντρο', justifyR: 'R — Δεξιά', justifyJ: 'J — Πλήρης', + printerFont: 'Printer font (^A@)', + uploadFont: 'Upload font file', + uploadingFont: 'Uploading…', + replaceFont: 'Replace font', + fontLoaded: 'Font loaded', + fontMissing: 'Font not loaded', }, code128: { content: 'Περιεχόμενο', diff --git a/src/locales/en.ts b/src/locales/en.ts index 2519e82..6c779c8 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -109,6 +109,12 @@ const en = { justifyC: 'C — Center', justifyR: 'R — Right', justifyJ: 'J — Justified', + printerFont: 'Printer font (^A@)', + uploadFont: 'Upload font file', + uploadingFont: 'Uploading…', + replaceFont: 'Replace font', + fontLoaded: 'Font loaded', + fontMissing: 'Font not loaded', }, code128: { content: 'Content', diff --git a/src/locales/es.ts b/src/locales/es.ts index cdcc727..deae3bd 100644 --- a/src/locales/es.ts +++ b/src/locales/es.ts @@ -109,6 +109,12 @@ const es = { justifyC: 'C — Centro', justifyR: 'R — Derecha', justifyJ: 'J — Justificado', + printerFont: 'Printer font (^A@)', + uploadFont: 'Upload font file', + uploadingFont: 'Uploading…', + replaceFont: 'Replace font', + fontLoaded: 'Font loaded', + fontMissing: 'Font not loaded', }, code128: { content: 'Contenido', diff --git a/src/locales/et.ts b/src/locales/et.ts index 83bcae6..8b0bf63 100644 --- a/src/locales/et.ts +++ b/src/locales/et.ts @@ -109,6 +109,12 @@ const et = { justifyC: 'C — Keskele', justifyR: 'R — Paremale', justifyJ: 'J — Rööpjoondus', + printerFont: 'Printer font (^A@)', + uploadFont: 'Upload font file', + uploadingFont: 'Uploading…', + replaceFont: 'Replace font', + fontLoaded: 'Font loaded', + fontMissing: 'Font not loaded', }, code128: { content: 'Sisu', diff --git a/src/locales/fa.ts b/src/locales/fa.ts index 6669c41..0b356db 100644 --- a/src/locales/fa.ts +++ b/src/locales/fa.ts @@ -109,6 +109,12 @@ const fa = { justifyC: 'C — وسط', justifyR: 'R — راست', justifyJ: 'J — تنظیم', + printerFont: 'Printer font (^A@)', + uploadFont: 'Upload font file', + uploadingFont: 'Uploading…', + replaceFont: 'Replace font', + fontLoaded: 'Font loaded', + fontMissing: 'Font not loaded', }, code128: { content: 'محتوا', diff --git a/src/locales/fi.ts b/src/locales/fi.ts index 0f678d0..cc5a9d9 100644 --- a/src/locales/fi.ts +++ b/src/locales/fi.ts @@ -109,6 +109,12 @@ const fi = { justifyC: 'C — Keskitetty', justifyR: 'R — Oikea', justifyJ: 'J — Tasattu', + printerFont: 'Printer font (^A@)', + uploadFont: 'Upload font file', + uploadingFont: 'Uploading…', + replaceFont: 'Replace font', + fontLoaded: 'Font loaded', + fontMissing: 'Font not loaded', }, code128: { content: 'Sisältö', diff --git a/src/locales/fr.ts b/src/locales/fr.ts index 51dcfc1..1e31225 100644 --- a/src/locales/fr.ts +++ b/src/locales/fr.ts @@ -109,6 +109,12 @@ const fr = { justifyC: 'C — Centré', justifyR: 'R — Droite', justifyJ: 'J — Justifié', + printerFont: 'Printer font (^A@)', + uploadFont: 'Upload font file', + uploadingFont: 'Uploading…', + replaceFont: 'Replace font', + fontLoaded: 'Font loaded', + fontMissing: 'Font not loaded', }, code128: { content: 'Contenu', diff --git a/src/locales/he.ts b/src/locales/he.ts index 3f5dfb4..a640e42 100644 --- a/src/locales/he.ts +++ b/src/locales/he.ts @@ -109,6 +109,12 @@ const he = { justifyC: 'C — מרכז', justifyR: 'R — ימין', justifyJ: 'J — מיושר', + printerFont: 'Printer font (^A@)', + uploadFont: 'Upload font file', + uploadingFont: 'Uploading…', + replaceFont: 'Replace font', + fontLoaded: 'Font loaded', + fontMissing: 'Font not loaded', }, code128: { content: 'תוכן', diff --git a/src/locales/hr.ts b/src/locales/hr.ts index f9f028c..8b9f0d3 100644 --- a/src/locales/hr.ts +++ b/src/locales/hr.ts @@ -109,6 +109,12 @@ const hr = { justifyC: 'C — Sredina', justifyR: 'R — Desno', justifyJ: 'J — Obostrano', + printerFont: 'Printer font (^A@)', + uploadFont: 'Upload font file', + uploadingFont: 'Uploading…', + replaceFont: 'Replace font', + fontLoaded: 'Font loaded', + fontMissing: 'Font not loaded', }, code128: { content: 'Sadržaj', diff --git a/src/locales/hu.ts b/src/locales/hu.ts index e22aa56..e52b184 100644 --- a/src/locales/hu.ts +++ b/src/locales/hu.ts @@ -109,6 +109,12 @@ const hu = { justifyC: 'C — Középre', justifyR: 'R — Jobbra', justifyJ: 'J — Sorkizárt', + printerFont: 'Printer font (^A@)', + uploadFont: 'Upload font file', + uploadingFont: 'Uploading…', + replaceFont: 'Replace font', + fontLoaded: 'Font loaded', + fontMissing: 'Font not loaded', }, code128: { content: 'Tartalom', diff --git a/src/locales/it.ts b/src/locales/it.ts index 427e2e4..316d681 100644 --- a/src/locales/it.ts +++ b/src/locales/it.ts @@ -109,6 +109,12 @@ const it = { justifyC: 'C — Centro', justifyR: 'R — Destra', justifyJ: 'J — Giustificato', + printerFont: 'Printer font (^A@)', + uploadFont: 'Upload font file', + uploadingFont: 'Uploading…', + replaceFont: 'Replace font', + fontLoaded: 'Font loaded', + fontMissing: 'Font not loaded', }, code128: { content: 'Contenuto', diff --git a/src/locales/ja.ts b/src/locales/ja.ts index 95febb1..0e3f830 100644 --- a/src/locales/ja.ts +++ b/src/locales/ja.ts @@ -109,6 +109,12 @@ const ja = { justifyC: 'C — 中央揃え', justifyR: 'R — 右揃え', justifyJ: 'J — 両端揃え', + printerFont: 'Printer font (^A@)', + uploadFont: 'Upload font file', + uploadingFont: 'Uploading…', + replaceFont: 'Replace font', + fontLoaded: 'Font loaded', + fontMissing: 'Font not loaded', }, code128: { content: 'コンテンツ', diff --git a/src/locales/ko.ts b/src/locales/ko.ts index ab3d57d..c3cc09e 100644 --- a/src/locales/ko.ts +++ b/src/locales/ko.ts @@ -109,6 +109,12 @@ const ko = { justifyC: 'C — 가운데', justifyR: 'R — 오른쪽', justifyJ: 'J — 균등 배분', + printerFont: 'Printer font (^A@)', + uploadFont: 'Upload font file', + uploadingFont: 'Uploading…', + replaceFont: 'Replace font', + fontLoaded: 'Font loaded', + fontMissing: 'Font not loaded', }, code128: { content: '내용', diff --git a/src/locales/lt.ts b/src/locales/lt.ts index d38da50..8007fe9 100644 --- a/src/locales/lt.ts +++ b/src/locales/lt.ts @@ -109,6 +109,12 @@ const lt = { justifyC: 'C — Centras', justifyR: 'R — Dešinė', justifyJ: 'J — Abipusė', + printerFont: 'Printer font (^A@)', + uploadFont: 'Upload font file', + uploadingFont: 'Uploading…', + replaceFont: 'Replace font', + fontLoaded: 'Font loaded', + fontMissing: 'Font not loaded', }, code128: { content: 'Turinys', diff --git a/src/locales/lv.ts b/src/locales/lv.ts index d73ef07..50b2742 100644 --- a/src/locales/lv.ts +++ b/src/locales/lv.ts @@ -109,6 +109,12 @@ const lv = { justifyC: 'C — Centrēts', justifyR: 'R — Pa labi', justifyJ: 'J — Izlīdzināts', + printerFont: 'Printer font (^A@)', + uploadFont: 'Upload font file', + uploadingFont: 'Uploading…', + replaceFont: 'Replace font', + fontLoaded: 'Font loaded', + fontMissing: 'Font not loaded', }, code128: { content: 'Saturs', diff --git a/src/locales/nl.ts b/src/locales/nl.ts index 102c468..0391c85 100644 --- a/src/locales/nl.ts +++ b/src/locales/nl.ts @@ -109,6 +109,12 @@ const nl = { justifyC: 'C — Gecentreerd', justifyR: 'R — Rechts', justifyJ: 'J — Uitgevuld', + printerFont: 'Printer font (^A@)', + uploadFont: 'Upload font file', + uploadingFont: 'Uploading…', + replaceFont: 'Replace font', + fontLoaded: 'Font loaded', + fontMissing: 'Font not loaded', }, code128: { content: 'Inhoud', diff --git a/src/locales/no.ts b/src/locales/no.ts index 6636884..1e80c9b 100644 --- a/src/locales/no.ts +++ b/src/locales/no.ts @@ -109,6 +109,12 @@ const no = { justifyC: 'C — Sentrert', justifyR: 'R — Høyre', justifyJ: 'J — Blokkjustert', + printerFont: 'Printer font (^A@)', + uploadFont: 'Upload font file', + uploadingFont: 'Uploading…', + replaceFont: 'Replace font', + fontLoaded: 'Font loaded', + fontMissing: 'Font not loaded', }, code128: { content: 'Innhold', diff --git a/src/locales/pl.ts b/src/locales/pl.ts index 4a235ba..3c74f2b 100644 --- a/src/locales/pl.ts +++ b/src/locales/pl.ts @@ -109,6 +109,12 @@ const pl = { justifyC: 'C — Środek', justifyR: 'R — Prawo', justifyJ: 'J — Wyjustowane', + printerFont: 'Printer font (^A@)', + uploadFont: 'Upload font file', + uploadingFont: 'Uploading…', + replaceFont: 'Replace font', + fontLoaded: 'Font loaded', + fontMissing: 'Font not loaded', }, code128: { content: 'Zawartość', diff --git a/src/locales/pt.ts b/src/locales/pt.ts index b3c50b4..408dbef 100644 --- a/src/locales/pt.ts +++ b/src/locales/pt.ts @@ -109,6 +109,12 @@ const pt = { justifyC: 'C — Centro', justifyR: 'R — Direita', justifyJ: 'J — Justificado', + printerFont: 'Printer font (^A@)', + uploadFont: 'Upload font file', + uploadingFont: 'Uploading…', + replaceFont: 'Replace font', + fontLoaded: 'Font loaded', + fontMissing: 'Font not loaded', }, code128: { content: 'Conteúdo', diff --git a/src/locales/ro.ts b/src/locales/ro.ts index 628c1f7..2cc6e8a 100644 --- a/src/locales/ro.ts +++ b/src/locales/ro.ts @@ -109,6 +109,12 @@ const ro = { justifyC: 'C — Centru', justifyR: 'R — Dreapta', justifyJ: 'J — Justify', + printerFont: 'Printer font (^A@)', + uploadFont: 'Upload font file', + uploadingFont: 'Uploading…', + replaceFont: 'Replace font', + fontLoaded: 'Font loaded', + fontMissing: 'Font not loaded', }, code128: { content: 'Conținut', diff --git a/src/locales/sk.ts b/src/locales/sk.ts index 6f7e5fc..2cf7166 100644 --- a/src/locales/sk.ts +++ b/src/locales/sk.ts @@ -109,6 +109,12 @@ const sk = { justifyC: 'C — Na stred', justifyR: 'R — Vpravo', justifyJ: 'J — Do bloku', + printerFont: 'Printer font (^A@)', + uploadFont: 'Upload font file', + uploadingFont: 'Uploading…', + replaceFont: 'Replace font', + fontLoaded: 'Font loaded', + fontMissing: 'Font not loaded', }, code128: { content: 'Obsah', diff --git a/src/locales/sl.ts b/src/locales/sl.ts index d88a9c3..357c113 100644 --- a/src/locales/sl.ts +++ b/src/locales/sl.ts @@ -109,6 +109,12 @@ const sl = { justifyC: 'C — Sredina', justifyR: 'R — Desno', justifyJ: 'J — Obojestransko', + printerFont: 'Printer font (^A@)', + uploadFont: 'Upload font file', + uploadingFont: 'Uploading…', + replaceFont: 'Replace font', + fontLoaded: 'Font loaded', + fontMissing: 'Font not loaded', }, code128: { content: 'Vsebina', diff --git a/src/locales/sr.ts b/src/locales/sr.ts index c3e96e8..87bc1f5 100644 --- a/src/locales/sr.ts +++ b/src/locales/sr.ts @@ -109,6 +109,12 @@ const sr = { justifyC: 'C — Центар', justifyR: 'R — Десно', justifyJ: 'J — Обострано', + printerFont: 'Printer font (^A@)', + uploadFont: 'Upload font file', + uploadingFont: 'Uploading…', + replaceFont: 'Replace font', + fontLoaded: 'Font loaded', + fontMissing: 'Font not loaded', }, code128: { content: 'Sadržaj', diff --git a/src/locales/sv.ts b/src/locales/sv.ts index 47ad9e5..4ede245 100644 --- a/src/locales/sv.ts +++ b/src/locales/sv.ts @@ -109,6 +109,12 @@ const sv = { justifyC: 'C — Centrerad', justifyR: 'R — Höger', justifyJ: 'J — Marginaljust.', + printerFont: 'Printer font (^A@)', + uploadFont: 'Upload font file', + uploadingFont: 'Uploading…', + replaceFont: 'Replace font', + fontLoaded: 'Font loaded', + fontMissing: 'Font not loaded', }, code128: { content: 'Innehåll', diff --git a/src/locales/tr.ts b/src/locales/tr.ts index 907fccb..d8f9e98 100644 --- a/src/locales/tr.ts +++ b/src/locales/tr.ts @@ -109,6 +109,12 @@ const tr = { justifyC: 'C — Orta', justifyR: 'R — Sağ', justifyJ: 'J — İki yana', + printerFont: 'Printer font (^A@)', + uploadFont: 'Upload font file', + uploadingFont: 'Uploading…', + replaceFont: 'Replace font', + fontLoaded: 'Font loaded', + fontMissing: 'Font not loaded', }, code128: { content: 'İçerik', diff --git a/src/locales/zh-hans.ts b/src/locales/zh-hans.ts index b662ad2..793ea75 100644 --- a/src/locales/zh-hans.ts +++ b/src/locales/zh-hans.ts @@ -109,6 +109,12 @@ const zhHans = { justifyC: 'C — 居中', justifyR: 'R — 右对齐', justifyJ: 'J — 两端对齐', + printerFont: 'Printer font (^A@)', + uploadFont: 'Upload font file', + uploadingFont: 'Uploading…', + replaceFont: 'Replace font', + fontLoaded: 'Font loaded', + fontMissing: 'Font not loaded', }, code128: { content: '内容', diff --git a/src/locales/zh-hant.ts b/src/locales/zh-hant.ts index ed06785..2aad0b8 100644 --- a/src/locales/zh-hant.ts +++ b/src/locales/zh-hant.ts @@ -109,6 +109,12 @@ const zhHant = { justifyC: 'C — 置中', justifyR: 'R — 靠右', justifyJ: 'J — 左右對齊', + printerFont: 'Printer font (^A@)', + uploadFont: 'Upload font file', + uploadingFont: 'Uploading…', + replaceFont: 'Replace font', + fontLoaded: 'Font loaded', + fontMissing: 'Font not loaded', }, code128: { content: '內容', diff --git a/src/registry/text.tsx b/src/registry/text.tsx index a79ca44..af5be9a 100644 --- a/src/registry/text.tsx +++ b/src/registry/text.tsx @@ -1,7 +1,9 @@ +import { useRef, useState, useCallback } from 'react'; import type { ObjectTypeDefinition } from '../types/ObjectType'; import { useT } from '../lib/useT'; import { inputCls, labelCls } from '../components/Properties/styles'; import { fieldPos } from './zplHelpers'; +import { getFont, loadFontFile, useFontCacheVersion } from '../lib/fontCache'; export interface TextProps { content: string; @@ -9,6 +11,8 @@ export interface TextProps { fontWidth: number; rotation: 'N' | 'R' | 'I' | 'B'; reverse?: boolean; + /** Printer TrueType font filename from ^A@ (e.g. "ARIAL.TTF") */ + printerFontName?: string; /** ^FB field block properties */ blockWidth?: number; blockLines?: number; @@ -30,13 +34,16 @@ export const text: ObjectTypeDefinition = { toZPL: (obj) => { const p = obj.props; + const fontCmd = p.printerFontName + ? `^A@${p.rotation},${p.fontHeight},${p.fontWidth},E:${p.printerFontName}` + : `^A0${p.rotation},${p.fontHeight},${p.fontWidth}`; const fbCmd = p.blockWidth ? `^FB${p.blockWidth},${p.blockLines ?? 1},${p.blockLineSpacing ?? 0},${p.blockJustify ?? 'L'},0` : ''; return [ p.reverse ? '^LRY' : '', fieldPos(obj), - `^A0${p.rotation},${p.fontHeight},${p.fontWidth}`, + fontCmd, fbCmd, `^FD${p.content}^FS`, p.reverse ? '^LRN' : '', @@ -46,8 +53,61 @@ export const text: ObjectTypeDefinition = { PropertiesPanel: ({ obj, onChange }) => { const t = useT(); const p = obj.props; + const fileRef = useRef(null); + const [uploading, setUploading] = useState(false); + useFontCacheVersion(); + + const cachedFont = p.printerFontName ? getFont(p.printerFontName) : undefined; + const fontLoaded = !!cachedFont; + + const handleFontUpload = useCallback(async (file: File) => { + if (!p.printerFontName) return; + setUploading(true); + try { + await loadFontFile(file, p.printerFontName); + } finally { + setUploading(false); + } + }, [p.printerFontName]); + return (
+ {p.printerFontName && ( +
+ + {p.printerFontName} + { + const file = e.target.files?.[0]; + if (file) handleFontUpload(file); + e.target.value = ''; + }} + /> + + {fontLoaded && ( + {t.registry.text.fontLoaded} + )} + {!fontLoaded && ( + {t.registry.text.fontMissing} + )} +
+ )} +
Date: Tue, 21 Apr 2026 18:05:43 +0200 Subject: [PATCH 2/3] feat: Use font cache version hook --- src/components/Canvas/KonvaObject.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/Canvas/KonvaObject.tsx b/src/components/Canvas/KonvaObject.tsx index ab74fd0..a526a3f 100644 --- a/src/components/Canvas/KonvaObject.tsx +++ b/src/components/Canvas/KonvaObject.tsx @@ -1,5 +1,5 @@ import { useState, useEffect } from "react"; -import { getFontFamily } from "../../lib/fontCache"; +import { getFontFamily, useFontCacheVersion } from "../../lib/fontCache"; import { Circle, Ellipse, @@ -371,6 +371,7 @@ function KonvaObjectInner({ onChange, snap, }: Props) { + useFontCacheVersion(); // If the object was imported with ^FT (baseline position), compute display offset. // ^FT positions text at the baseline; ^FO at the top-left corner. // We need to convert FT→FO for canvas rendering only. From d93af4d3995d125413440c2d490dea4e96c4252c Mon Sep 17 00:00:00 2001 From: u8array Date: Tue, 21 Apr 2026 18:35:12 +0200 Subject: [PATCH 3/3] Adjust y-coordinate for barcode and QR code --- src/components/Canvas/KonvaObject.tsx | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/components/Canvas/KonvaObject.tsx b/src/components/Canvas/KonvaObject.tsx index a526a3f..6b90f33 100644 --- a/src/components/Canvas/KonvaObject.tsx +++ b/src/components/Canvas/KonvaObject.tsx @@ -399,6 +399,27 @@ function KonvaObjectInner({ } else if (p.rotation === "B") { displayX -= renderedH; } + } else if ( + obj.type === "code128" || + obj.type === "code39" || + obj.type === "ean13" || + obj.type === "upca" || + obj.type === "ean8" || + obj.type === "upce" || + obj.type === "interleaved2of5" || + obj.type === "code93" + ) { + const p = obj.props as { height: number }; + displayY -= p.height; + } else if (obj.type === "pdf417") { + const p = obj.props as { rowHeight: number }; + displayY -= p.rowHeight * 10; + } else if (obj.type === "qrcode") { + const p = obj.props as { magnification: number }; + displayY -= p.magnification * 25; + } else if (obj.type === "datamatrix") { + const p = obj.props as { dimension: number }; + displayY -= p.dimension * 20; } }