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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions src/components/Canvas/KonvaObject.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useState, useEffect } from "react";
import { getFontFamily, useFontCacheVersion } from "../../lib/fontCache";
import {
Circle,
Ellipse,
Expand Down Expand Up @@ -370,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.
Expand Down Expand Up @@ -462,6 +464,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<typeof p.rotation, number> = {
N: 0,
R: 90,
Expand Down Expand Up @@ -496,7 +501,7 @@ function KonvaObjectInner({
<Text
text={p.content}
fontSize={fontSize}
fontFamily="'Roboto Condensed', sans-serif"
fontFamily={fontFamily}
fontStyle="bold"
fill="#ffffff"
y={approxH * 0.1}
Expand All @@ -512,7 +517,7 @@ function KonvaObjectInner({
y={y}
text={p.content}
fontSize={fontSize}
fontFamily="'Roboto Condensed', sans-serif"
fontFamily={fontFamily}
fontStyle="bold"
rotation={zplRotationDeg[p.rotation]}
fill="#000000"
Expand Down
113 changes: 113 additions & 0 deletions src/lib/fontCache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/**
* Font cache for printer TrueType fonts referenced by ^A@.
* Fonts are stored as data-URLs and persisted to localStorage.
* Each loaded font is registered with the browser's FontFace API
* so Konva (canvas) can render text using it.
*/

import { useState, useEffect } from 'react';

export interface CachedFont {
id: string;
/** Original printer filename e.g. "ARIAL.TTF" (uppercased for lookup) */
name: string;
/** data-URL — data:font/truetype;base64,... */
dataUrl: string;
/** Registered CSS font-family name e.g. "zpl-ARIAL" */
fontFamily: string;
}

const LS_PREFIX = 'zpl-font-';
const cache = new Map<string, CachedFont>();
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<void> {
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<CachedFont> {
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 };
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

crypto.randomUUID() is only available in secure contexts (HTTPS or localhost). If the application is accessed over a standard HTTP connection, this call will throw an error and prevent font uploads. Consider providing a fallback for non-secure contexts or using a different method for generating unique IDs if HTTP support is required.

cache.set(name, entry);
try {
localStorage.setItem(LS_PREFIX + name, JSON.stringify(entry));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Storing font files as data URLs in localStorage is problematic because localStorage has a strict size limit (typically 5MB). A single TrueType font can be several hundred kilobytes or even megabytes, meaning the cache will fill up very quickly. Consider using IndexedDB for persisting large binary data like fonts, as it offers much higher storage limits and better performance for binary blobs.

} catch {
// localStorage full — font stays in memory only
}
Comment on lines +96 to +98
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

When localStorage.setItem fails (e.g., due to quota exceeded), the error is caught silently. This results in an inconsistent state where the font is available in the current session's memory but will disappear upon page reload. It would be better to notify the user that the font could not be persisted permanently.

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();
}
9 changes: 9 additions & 0 deletions src/lib/zplParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Expand Down
6 changes: 6 additions & 0 deletions src/locales/ar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: 'المحتوى',
Expand Down
6 changes: 6 additions & 0 deletions src/locales/bg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: 'Съдържание',
Expand Down
6 changes: 6 additions & 0 deletions src/locales/cs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
6 changes: 6 additions & 0 deletions src/locales/da.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
6 changes: 6 additions & 0 deletions src/locales/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
6 changes: 6 additions & 0 deletions src/locales/el.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: 'Περιεχόμενο',
Expand Down
6 changes: 6 additions & 0 deletions src/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
6 changes: 6 additions & 0 deletions src/locales/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
6 changes: 6 additions & 0 deletions src/locales/et.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
6 changes: 6 additions & 0 deletions src/locales/fa.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: 'محتوا',
Expand Down
6 changes: 6 additions & 0 deletions src/locales/fi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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ö',
Expand Down
6 changes: 6 additions & 0 deletions src/locales/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
6 changes: 6 additions & 0 deletions src/locales/he.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: 'תוכן',
Expand Down
6 changes: 6 additions & 0 deletions src/locales/hr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
6 changes: 6 additions & 0 deletions src/locales/hu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
6 changes: 6 additions & 0 deletions src/locales/it.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Loading
Loading