diff --git a/src/components/Canvas/ImageObject.tsx b/src/components/Canvas/ImageObject.tsx
index 7a30ed49..53d564a5 100644
--- a/src/components/Canvas/ImageObject.tsx
+++ b/src/components/Canvas/ImageObject.tsx
@@ -1,5 +1,5 @@
-import { useState, useEffect, useRef } from "react";
-import { Group, Image as KImage, Rect, Text } from "react-konva";
+import { useState, useEffect, useRef, type ReactElement } from "react";
+import { Group, Image as KImage, Path, Rect } from "react-konva";
import type Konva from "konva";
import type { LabelObject } from "../../types/Group";
import { dotsToPx, pxToDots } from "../../lib/coordinates";
@@ -29,12 +29,13 @@ export function ImageObject({
const colors = useColorScheme();
const cached = getImage(p.imageId);
const w = dotsToPx(p.widthDots, scale, dpmm);
- // Guard against a 0-width cached image: the imageCache pipeline
- // doesn't normally produce one, but a malformed file could leak
- // through and div-by-zero would render NaN-sized canvas nodes.
+ // Aspect-lock when a real PNG is cached; fall back to `heightDots` for
+ // recall-only placeholders so the user can shape the box freely. Guard
+ // against 0-width cached images (malformed file edge case) — div-by-
+ // zero would otherwise render NaN-sized canvas nodes.
const h = cached && cached.width > 0
? w * (cached.height / cached.width)
- : w;
+ : dotsToPx(p.heightDots ?? p.widthDots, scale, dpmm);
const x = offsetX + dotsToPx(obj.x, scale, dpmm);
const y = offsetY + dotsToPx(obj.y, scale, dpmm);
@@ -120,13 +121,37 @@ export function ImageObject({
strokeWidth={isSelected ? 2 : 1}
dash={[4, 2]}
/>
-
+
);
}
+
+/** Heroicons "photo" outline rendered via Konva.Path so the placeholder
+ * is OS-independent. We previously used the 🖼 emoji which rendered
+ * inconsistently (color on macOS, monochrome on Linux, missing on
+ * some Windows configurations). */
+const PHOTO_ICON_PATH =
+ "M2.25 15.75l5.159-5.159a2.25 2.25 0 0 1 3.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 0 1 3.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 0 0 1.5-1.5V6a1.5 1.5 0 0 0-1.5-1.5H3.75A1.5 1.5 0 0 0 2.25 6v12a1.5 1.5 0 0 0 1.5 1.5Zm10.5-11.25h.008v.008h-.008V8.25Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z";
+const PHOTO_ICON_VIEWBOX = 24;
+
+function PlaceholderIcon({ w, h }: { w: number; h: number }): ReactElement {
+ // Centre the icon at 60% of the smaller dimension; clamp so a thin
+ // image strip still shows something visible. Stroke-only mirrors
+ // Heroicons' outline style.
+ const target = Math.max(Math.min(w, h) * 0.6, 12);
+ const scale = target / PHOTO_ICON_VIEWBOX;
+ return (
+
+ );
+}
diff --git a/src/components/Properties/styles.ts b/src/components/Properties/styles.ts
index 306b76ea..c209cd7b 100644
--- a/src/components/Properties/styles.ts
+++ b/src/components/Properties/styles.ts
@@ -1,2 +1,6 @@
export const inputCls = 'w-full bg-surface-2 border border-border rounded px-2 py-1 text-xs font-mono text-text focus:border-accent focus:outline-none';
export const labelCls = 'font-mono text-[10px] text-muted uppercase tracking-wider';
+/** Secondary-action button: file upload, toggle row, etc. Matches the
+ * surface-2 + border styling used by `inputCls` so buttons sit naturally
+ * next to form fields without dominating the visual hierarchy. */
+export const buttonCls = 'px-3 py-1.5 rounded text-xs font-mono bg-surface-2 border border-border text-text hover:bg-border transition-colors';
diff --git a/src/lib/storagePath.ts b/src/lib/storagePath.ts
index a464e69f..4464765a 100644
--- a/src/lib/storagePath.ts
+++ b/src/lib/storagePath.ts
@@ -8,6 +8,27 @@
* drifting apart across the parser, emitter, and image registry.
*/
+/** Storage device prefixes Zebra firmware recognises. R: volatile RAM
+ * (default, fastest); E: non-volatile flash; B: alternate flash;
+ * A: alias drive on some models. All four round-trip through `~DY` and
+ * `^XG`; the parser accepts them and the UI exposes them in the device
+ * picker. */
+export const STORAGE_DEVICES = ["R", "E", "B", "A"] as const;
+export type StorageDevice = (typeof STORAGE_DEVICES)[number];
+
+/** Zebra DOS-style filename: up to 8 chars, uppercase alphanumeric +
+ * underscore. Both constants are exported so input filters in the UI
+ * and the parser stay in lockstep. */
+export const MAX_STORAGE_NAME_LEN = 8;
+export const STORAGE_NAME_FILTER_RE = /[^A-Z0-9_]/g;
+
+/** Default name when the user first enables printer-storage on an image.
+ * Short UUID slice avoids collisions across multiple images on the same
+ * label without forcing the user to pick a name up front. */
+export function defaultStorageName(): string {
+ return `IMG_${crypto.randomUUID().slice(0, 4).toUpperCase()}`;
+}
+
export interface StoragePath {
/** Storage device prefix without trailing colon: "R", "E", "B", "A". */
device: string;
diff --git a/src/lib/zplGenerator.test.ts b/src/lib/zplGenerator.test.ts
index b1f44a06..15973df1 100644
--- a/src/lib/zplGenerator.test.ts
+++ b/src/lib/zplGenerator.test.ts
@@ -1,4 +1,5 @@
import { describe, it, expect } from 'vitest';
+import { zlibSync } from 'fflate';
import { generateZPL, generateMultiPageZPL } from './zplGenerator';
import { parseZPL } from './zplParser';
import type { LabelConfig } from '../types/ObjectType';
@@ -573,21 +574,27 @@ describe('generateZPL — ~DY graphic upload + ^XG recall', () => {
// Zebra firmware rejects format A with a :Z64: payload. The shared
// cache uses ^GF{format} so the format letter survives both the GF
// and DY round-trip paths.
- const z64Payload = ':Z64:eJxjYAACBAAACgAB:1234';
+ const bytes = new Uint8Array([0, 0xff, 0xff, 0]);
+ const deflated = zlibSync(bytes);
+ let bin = '';
+ for (const b of deflated) bin += String.fromCharCode(b);
+ const b64 = btoa(bin);
+ function crc(s: string): string {
+ let c = 0;
+ for (const ch of s) {
+ c ^= ch.charCodeAt(0) << 8;
+ for (let j = 0; j < 8; j++)
+ c = (c & 0x8000) ? ((c << 1) ^ 0x1021) & 0xffff : (c << 1) & 0xffff;
+ }
+ return c.toString(16).padStart(4, '0').toUpperCase();
+ }
const zpl =
- `~DYR:CLOGO,C,G,4,1,${z64Payload}\n` +
+ `~DYR:CLOGO,C,G,4,1,:Z64:${b64}:${crc(b64)}\n` +
`^XA^FO0,0^XGR:CLOGO.GRF,1,1^FS^XZ`;
const parsed = parseZPL(zpl, 8);
- // ^XG only resolves if ~DY was registered; in this case the Z64
- // stream is malformed (junk base64) so the upload fails — the test
- // is then about the parser surfacing that as browserLimit instead
- // of mis-pairing format letters. We accept either path; if it does
- // round-trip, the format letter must be C.
- if (parsed.objects.length > 0) {
- const out = generateZPL(BASE_LABEL, parsed.objects);
- expect(out).toContain('~DYR:CLOGO,C,G,');
- expect(out).not.toContain('~DYR:CLOGO,A,G,');
- }
+ const out = generateZPL(BASE_LABEL, parsed.objects);
+ expect(out).toContain('~DYR:CLOGO,C,G,');
+ expect(out).not.toContain('~DYR:CLOGO,A,G,');
});
it('deduplicates the ~DY preamble when the same upload is referenced twice', () => {
diff --git a/src/lib/zplGenerator.ts b/src/lib/zplGenerator.ts
index fcae56a4..909ebd72 100644
--- a/src/lib/zplGenerator.ts
+++ b/src/lib/zplGenerator.ts
@@ -113,6 +113,10 @@ export function generateZPL(label: LabelConfig, objects: LabelObject[]): string
if (obj.type !== 'image') continue;
const p = obj.props as ImageProps;
if (!p.storedAs || !p._gfaCache) continue;
+ // Recall-only mode: the printer already has the bytes (admin uploaded
+ // them out-of-band), so skip the ~DY preamble. Image stays a preview
+ // in the designer; ZPL output only carries the ^XG references.
+ if (p.storedAs.embedInZpl === false) continue;
const key = formatStoragePath(p.storedAs, false);
if (seenGraphics.has(key)) continue;
seenGraphics.add(key);
diff --git a/src/lib/zplParser.test.ts b/src/lib/zplParser.test.ts
index 27600639..68d54d2b 100644
--- a/src/lib/zplParser.test.ts
+++ b/src/lib/zplParser.test.ts
@@ -676,7 +676,7 @@ describe('parseZPL — ~DY + ^XG graphic upload/recall', () => {
expect(objects).toHaveLength(1);
expect(objects[0]?.type).toBe('image');
expect(props(objects[0]).widthDots).toBe(8);
- expect(props(objects[0]).storedAs).toEqual({ device: 'R', name: 'LOGO' });
+ expect(props(objects[0]).storedAs).toEqual({ device: 'R', name: 'LOGO', embedInZpl: true });
expect(objects[0]?.x).toBe(50);
expect(objects[0]?.y).toBe(80);
expect(importReport.browserLimit).toHaveLength(0);
@@ -690,15 +690,22 @@ describe('parseZPL — ~DY + ^XG graphic upload/recall', () => {
`^XA^FO50,80^XGR:LOGO,1,1^FS^XZ`;
const { objects, importReport } = parseZPL(zpl, 8);
expect(objects).toHaveLength(1);
- expect(props(objects[0]).storedAs).toEqual({ device: 'R', name: 'LOGO' });
+ expect(props(objects[0]).storedAs).toEqual({ device: 'R', name: 'LOGO', embedInZpl: true });
expect(importReport.browserLimit).toHaveLength(0);
});
- it('^XG without a preceding ~DY surfaces as browserLimit', () => {
+ it('^XG without a preceding ~DY imports as recall-only image', () => {
+ // Admin pre-loaded the file on the printer; we just emit the ^XG
+ // reference without ~DY bytes. Object is created so the user can
+ // position/edit it; embedInZpl=false stops the emitter from
+ // re-uploading bytes we never received.
const zpl = `^XA^FO0,0^XGR:MISSING.GRF,1,1^FS^XZ`;
const { objects, importReport } = parseZPL(zpl, 8);
- expect(objects).toHaveLength(0);
- expect(importReport.browserLimit.some((s) => s.startsWith('^XG'))).toBe(true);
+ expect(objects).toHaveLength(1);
+ expect(props(objects[0]).storedAs).toEqual({
+ device: 'R', name: 'MISSING', embedInZpl: false,
+ });
+ expect(importReport.partial).toContain('^XG');
});
it('accepts :Z64:-wrapped graphic payloads in ~DY (format C)', () => {
@@ -709,7 +716,7 @@ describe('parseZPL — ~DY + ^XG graphic upload/recall', () => {
`^XA^FO0,0^XG${PATH}.GRF,1,1^FS^XZ`;
const { objects, importReport } = parseZPL(zpl, 8);
expect(objects).toHaveLength(1);
- expect(props(objects[0]).storedAs).toEqual({ device: 'R', name: 'LOGO' });
+ expect(props(objects[0]).storedAs).toEqual({ device: 'R', name: 'LOGO', embedInZpl: true });
expect(importReport.partial).not.toContain('~DY');
});
});
diff --git a/src/lib/zplParser.ts b/src/lib/zplParser.ts
index 8314d4a8..0f806e25 100644
--- a/src/lib/zplParser.ts
+++ b/src/lib/zplParser.ts
@@ -1468,41 +1468,56 @@ export function parseZPL(zpl: string, dpmm = 8): ParsedZPL {
// ── Recall stored graphic ──────────────────────────────────────────────
XG(_, rest) {
// ^XGd:f.x,mx,my — references a graphic uploaded earlier via ~DY.
- // We look the path up in downloadedGraphics; if found, instantiate an
- // image object at the current ^FO/^FT and tag it with storedAs so
- // re-emit produces the same upload+recall pair.
+ // Two valid imports:
+ // - With preceding ~DY in the stream: full image (bytes + storedAs
+ // with embedInZpl=true) so re-emit produces the same upload+recall.
+ // - Without ~DY: the printer is assumed to host the file out-of-band
+ // (admin pre-loaded). Object gets storedAs.embedInZpl=false and
+ // no cached bitmap; the canvas falls back to a placeholder, the
+ // emitter skips the ~DY preamble but keeps the ^XG reference.
const firstComma = rest.indexOf(",");
const xgPath = firstComma === -1 ? rest : rest.slice(0, firstComma);
- const surfaceXgFailure = (): void => {
- skipped.push(`^XG${rest}`);
- browserLimit.push(`^XG${rest}`);
- };
- // ^XG omitting the `.GRF` suffix is valid ZPL (Labelary accepts
- // `^XGR:LOGO,…` for an upload stored as `R:LOGO.GRF`). Normalise
- // through the storage-path helpers so the map lookup matches the
- // canonical key the ~DY side wrote.
const parsed = parseStoragePath(xgPath);
if (!parsed) {
- surfaceXgFailure();
+ skipped.push(`^XG${rest}`);
+ browserLimit.push(`^XG${rest}`);
return;
}
const uploaded = downloadedGraphics.get(formatStoragePath(parsed, true));
- if (!uploaded) {
- surfaceXgFailure();
+ const posType: "FT" | "FO" = positionIsFT ? "FT" : "FO";
+ if (uploaded) {
+ objects.push(
+ makeObj(
+ "image",
+ x,
+ y,
+ {
+ imageId: uploaded.imageId,
+ widthDots: uploaded.widthDots,
+ threshold: 128,
+ _gfaCache: uploaded.gfaCache,
+ storedAs: { ...parsed, embedInZpl: true },
+ } satisfies ImageProps,
+ posType,
+ takeComment(),
+ ),
+ );
return;
}
- const posType: "FT" | "FO" = positionIsFT ? "FT" : "FO";
+ // Recall-only: no bytes available, but the ZPL is valid and the
+ // printer side is assumed to resolve. Surface as partial so the
+ // import report flags the degraded preview.
+ partialCmds.add("^XG");
objects.push(
makeObj(
"image",
x,
y,
{
- imageId: uploaded.imageId,
- widthDots: uploaded.widthDots,
+ imageId: "",
+ widthDots: 200,
threshold: 128,
- _gfaCache: uploaded.gfaCache,
- storedAs: parsed,
+ storedAs: { ...parsed, embedInZpl: false },
} satisfies ImageProps,
posType,
takeComment(),
diff --git a/src/locales/ar.ts b/src/locales/ar.ts
index 6c5804ac..15459ca1 100644
--- a/src/locales/ar.ts
+++ b/src/locales/ar.ts
@@ -295,12 +295,20 @@ const ar = {
image: {
source: 'مصدر الصورة',
selectImage: 'اختر صورة…',
+ removeFromCache: 'إزالة من الذاكرة المؤقتة',
+ removeFromCacheConfirm: 'إزالة هذه الصورة من الذاكرة المؤقتة المحلية؟ ستصبح غير متاحة لجميع الملصقات التي تشير إليها.',
upload: 'رفع صورة',
uploading: 'جارٍ الرفع…',
uploadError: 'تعذر تحميل الصورة',
preview: 'معاينة',
widthDots: 'العرض (نقاط)',
threshold: 'عتبة أحادية',
+ storage: 'تخزين الطابعة',
+ storeOnPrinter: 'تفعيل',
+ storeOnPrinterHint: 'يشير إلى رسم مخزن في الطابعة عبر ^XG. افتراضيًا تُرسل البايتات مرة عبر ~DY ليكون العمل قائمًا بذاته؛ أوقفه عندما يكون الملف موجودًا بالفعل على الطابعة (رفع من المدير).',
+ embedInZpl: 'إرسال البايتات',
+ embedInZplHint: 'إيقاف = الطابعة لديها الملف بالفعل؛ يتم إرسال ^XG فقط. تشغيل = البايتات تُرسل مع كل مهمة عبر ~DY (افتراضي).',
+ storeInline: 'تضمين مباشر',
},
upca: {
content: 'المحتوى (11 رقم)',
diff --git a/src/locales/bg.ts b/src/locales/bg.ts
index b8c8637f..ff08b956 100644
--- a/src/locales/bg.ts
+++ b/src/locales/bg.ts
@@ -295,12 +295,20 @@ const bg = {
image: {
source: 'Източник на изображение',
selectImage: 'Изберете изображение…',
+ removeFromCache: 'Премахни от кеша',
+ removeFromCacheConfirm: 'Премахни това изображение от локалния кеш? Ще стане недостъпно за всички етикети, които го реферират.',
upload: 'Качване на изображение',
uploading: 'Качване…',
uploadError: 'Изображението не може да бъде заредено',
preview: 'Преглед',
widthDots: 'Ширина (точки)',
threshold: 'Моно праг',
+ storage: 'Памет на принтера',
+ storeOnPrinter: 'Активирай',
+ storeOnPrinterHint: 'Реферира графика, съхранена в принтера, чрез ^XG. По подразбиране байтовете се изпращат веднъж чрез ~DY, така че заданието е самодостатъчно; изключи, когато файлът вече е на принтера (качен от администратор).',
+ embedInZpl: 'Изпрати байтове',
+ embedInZplHint: 'Изкл = принтерът вече има файла; излъчва се само ^XG. Вкл = байтовете се изпращат с всяка задача чрез ~DY (по подразбиране).',
+ storeInline: 'Вграждане',
},
upca: {
content: 'Съдържание (11 цифри)',
diff --git a/src/locales/cs.ts b/src/locales/cs.ts
index 379ce2b9..c3d8a65c 100644
--- a/src/locales/cs.ts
+++ b/src/locales/cs.ts
@@ -295,12 +295,20 @@ const cs = {
image: {
source: 'Zdroj obrázku',
selectImage: 'Vybrat obrázek…',
+ removeFromCache: 'Odstranit z mezipaměti',
+ removeFromCacheConfirm: 'Odebrat tento obrázek z místní mezipaměti? Bude nedostupný pro všechny štítky, které na něj odkazují.',
upload: 'Nahrát obrázek',
uploading: 'Nahrávání…',
uploadError: 'Obrázek nelze načíst',
preview: 'Náhled',
widthDots: 'Šířka (body)',
threshold: 'Mono práh',
+ storage: 'Úložiště tiskárny',
+ storeOnPrinter: 'Aktivovat',
+ storeOnPrinterHint: 'Odkazuje na grafiku uloženou v tiskárně pomocí ^XG. Ve výchozím nastavení se bajty odešlou jednou pomocí ~DY, takže úloha je samostatná; vypněte, pokud je soubor již v tiskárně (nahrál administrátor).',
+ embedInZpl: 'Odeslat bajty',
+ embedInZplHint: 'Vyp = tiskárna soubor již má; emituje se pouze ^XG. Zap = bajty se odesílají s každou úlohou pomocí ~DY (výchozí).',
+ storeInline: 'Vložit přímo',
},
upca: {
content: 'Obsah (11 číslic)',
diff --git a/src/locales/da.ts b/src/locales/da.ts
index 64d72c28..08a0ba37 100644
--- a/src/locales/da.ts
+++ b/src/locales/da.ts
@@ -295,12 +295,20 @@ const da = {
image: {
source: 'Billedkilde',
selectImage: 'Vælg billede…',
+ removeFromCache: 'Fjern fra cache',
+ removeFromCacheConfirm: 'Fjern dette billede fra den lokale cache? Det bliver utilgængeligt for alle etiketter, der refererer til det.',
upload: 'Upload billede',
uploading: 'Uploader…',
uploadError: 'Kunne ikke indlæse billede',
preview: 'Forhåndsvisning',
widthDots: 'Bredde (punkter)',
threshold: 'Mono-tærskel',
+ storage: 'Printer-lager',
+ storeOnPrinter: 'Aktivér',
+ storeOnPrinterHint: 'Refererer en grafik gemt på printeren via ^XG. Som standard sendes bytes én gang via ~DY, så jobbet er selvbærende; slå fra når filen allerede er på printeren (administrator-upload).',
+ embedInZpl: 'Send bytes',
+ embedInZplHint: 'Fra = printeren har allerede filen; kun ^XG udsendes. Til = bytes sendes med hvert job via ~DY (standard).',
+ storeInline: 'Indlejr direkte',
},
upca: {
content: 'Indhold (11 cifre)',
diff --git a/src/locales/de.ts b/src/locales/de.ts
index 3d52cdb8..37852c8d 100644
--- a/src/locales/de.ts
+++ b/src/locales/de.ts
@@ -316,12 +316,20 @@ const de = {
image: {
source: 'Bildquelle',
selectImage: 'Bild auswählen…',
+ removeFromCache: 'Aus Cache entfernen',
+ removeFromCacheConfirm: 'Dieses Bild aus dem lokalen Cache entfernen? Es wird für alle Etiketten unverfügbar, die es referenzieren.',
upload: 'Bild hochladen',
uploading: 'Hochladen…',
uploadError: 'Bild konnte nicht geladen werden',
preview: 'Vorschau',
widthDots: 'Breite (Punkte)',
threshold: 'Mono-Schwellenwert',
+ storage: 'Drucker-Speicher',
+ storeOnPrinter: 'Aktivieren',
+ storeOnPrinterHint: 'Referenziert eine Grafik auf der Drucker-Storage via ^XG. Standardmäßig werden die Bytes einmal via ~DY mitgesendet, sodass der Job autark ist; ausschalten wenn die Datei schon auf dem Drucker liegt (Admin-Upload).',
+ embedInZpl: 'Bytes mitsenden',
+ embedInZplHint: 'Aus = Drucker hat die Datei bereits; es wird nur ^XG emittiert. An = Bytes werden mit jedem Druckjob via ~DY mitgesendet (Standard).',
+ storeInline: 'Inline einbetten',
},
upca: {
content: 'Inhalt (11 Ziffern)',
diff --git a/src/locales/el.ts b/src/locales/el.ts
index d7d23531..c05e8098 100644
--- a/src/locales/el.ts
+++ b/src/locales/el.ts
@@ -295,12 +295,20 @@ const el = {
image: {
source: 'Πηγή εικόνας',
selectImage: 'Επιλογή εικόνας…',
+ removeFromCache: 'Αφαίρεση από την κρυφή μνήμη',
+ removeFromCacheConfirm: 'Αφαίρεση αυτής της εικόνας από την τοπική κρυφή μνήμη; Θα είναι μη διαθέσιμη για όλες τις ετικέτες που την αναφέρουν.',
upload: 'Μεταφόρτωση εικόνας',
uploading: 'Μεταφόρτωση…',
uploadError: 'Αδυναμία φόρτωσης εικόνας',
preview: 'Προεπισκόπηση',
widthDots: 'Πλάτος (κουκκίδες)',
threshold: 'Κατώφλι mono',
+ storage: 'Αποθήκευση εκτυπωτή',
+ storeOnPrinter: 'Ενεργοποίηση',
+ storeOnPrinterHint: 'Αναφέρεται σε γραφικό αποθηκευμένο στον εκτυπωτή μέσω ^XG. Εξ ορισμού τα bytes αποστέλλονται μία φορά μέσω ~DY ώστε η εργασία να είναι αυτοτελής· απενεργοποιήστε όταν το αρχείο είναι ήδη στον εκτυπωτή (ανέβηκε από διαχειριστή).',
+ embedInZpl: 'Αποστολή bytes',
+ embedInZplHint: 'Off = ο εκτυπωτής έχει ήδη το αρχείο, εκπέμπεται μόνο ^XG. On = τα bytes αποστέλλονται με κάθε εργασία μέσω ~DY (προεπιλογή).',
+ storeInline: 'Ενσωμάτωση',
},
upca: {
content: 'Περιεχόμενο (11 ψηφία)',
diff --git a/src/locales/en.ts b/src/locales/en.ts
index b62362a8..c58dced9 100644
--- a/src/locales/en.ts
+++ b/src/locales/en.ts
@@ -316,12 +316,20 @@ const en = {
image: {
source: 'Image source',
selectImage: 'Select image…',
+ removeFromCache: 'Remove from cache',
+ removeFromCacheConfirm: 'Remove this image from the local cache? It will be unavailable for all labels that reference it.',
upload: 'Upload image',
uploading: 'Uploading…',
uploadError: 'Could not load image',
preview: 'Preview',
widthDots: 'Width (dots)',
threshold: 'Mono threshold',
+ storage: 'Printer storage',
+ storeOnPrinter: 'Activate',
+ storeOnPrinterHint: 'Reference a graphic stored on the printer via ^XG. By default the bytes ship once via ~DY so the job is self-contained; toggle off when the file is already on the printer (admin-uploaded).',
+ embedInZpl: 'Ship bytes',
+ embedInZplHint: 'Off = printer already has the file; only ^XG is emitted. On = bytes ship with every job via ~DY (default).',
+ storeInline: 'Embed inline',
},
upca: {
content: 'Content (11 digits)',
diff --git a/src/locales/es.ts b/src/locales/es.ts
index 545a7998..9a10b3d4 100644
--- a/src/locales/es.ts
+++ b/src/locales/es.ts
@@ -295,12 +295,20 @@ const es = {
image: {
source: 'Fuente de imagen',
selectImage: 'Seleccionar imagen…',
+ removeFromCache: 'Eliminar de la caché',
+ removeFromCacheConfirm: '¿Eliminar esta imagen de la caché local? Dejará de estar disponible para todas las etiquetas que la referencian.',
upload: 'Subir imagen',
uploading: 'Subiendo…',
uploadError: 'No se pudo cargar la imagen',
preview: 'Vista previa',
widthDots: 'Ancho (puntos)',
threshold: 'Umbral mono',
+ storage: 'Almacenamiento de impresora',
+ storeOnPrinter: 'Activar',
+ storeOnPrinterHint: 'Referencia un gráfico almacenado en la impresora con ^XG. Por defecto los bytes se envían una vez con ~DY para que el trabajo sea autocontenido; desactívalo cuando el archivo ya está en la impresora (subido por el administrador).',
+ embedInZpl: 'Enviar bytes',
+ embedInZplHint: 'Off = la impresora ya tiene el archivo, solo se emite ^XG. On = los bytes se envían en cada trabajo vía ~DY (predeterminado).',
+ storeInline: 'Incrustar en línea',
},
upca: {
content: 'Contenido (11 dígitos)',
diff --git a/src/locales/et.ts b/src/locales/et.ts
index a054938c..f0d19418 100644
--- a/src/locales/et.ts
+++ b/src/locales/et.ts
@@ -295,12 +295,20 @@ const et = {
image: {
source: 'Pildi allikas',
selectImage: 'Vali pilt…',
+ removeFromCache: 'Eemalda vahemälust',
+ removeFromCacheConfirm: 'Eemalda see pilt kohalikust vahemälust? See pole enam saadaval ühelegi sellele viitavale sildile.',
upload: 'Laadi pilt üles',
uploading: 'Üleslaadimine…',
uploadError: 'Pildi laadimine ebaõnnestus',
preview: 'Eelvaade',
widthDots: 'Laius (punktid)',
threshold: 'Mono lävi',
+ storage: 'Printeri salvestus',
+ storeOnPrinter: 'Aktiveeri',
+ storeOnPrinterHint: 'Viitab printerisse salvestatud graafikale ^XG kaudu. Vaikimisi saadetakse baidid korra ~DY kaudu, et töö oleks isemajandav; lülita välja, kui fail on juba printeris (administraatori üleslaaditud).',
+ embedInZpl: 'Saada baidid',
+ embedInZplHint: 'Väljas = printeril on fail juba olemas, väljastatakse ainult ^XG. Sees = baidid saadetakse iga tööga ~DY kaudu (vaikimisi).',
+ storeInline: 'Manusta otse',
},
upca: {
content: 'Sisu (11 numbrit)',
diff --git a/src/locales/fa.ts b/src/locales/fa.ts
index 22a64d49..51397d4c 100644
--- a/src/locales/fa.ts
+++ b/src/locales/fa.ts
@@ -295,12 +295,20 @@ const fa = {
image: {
source: 'منبع تصویر',
selectImage: 'انتخاب تصویر…',
+ removeFromCache: 'حذف از حافظه نهان',
+ removeFromCacheConfirm: 'این تصویر را از حافظه نهان محلی حذف میکنید؟ برای همه برچسبهایی که به آن ارجاع میدهند در دسترس نخواهد بود.',
upload: 'بارگذاری تصویر',
uploading: 'در حال بارگذاری…',
uploadError: 'بارگذاری تصویر ممکن نشد',
preview: 'پیشنمایش',
widthDots: 'عرض (نقاط)',
threshold: 'آستانه تکرنگ',
+ storage: 'ذخیره چاپگر',
+ storeOnPrinter: 'فعالسازی',
+ storeOnPrinterHint: 'به یک گرافیک ذخیرهشده در چاپگر از طریق ^XG ارجاع میدهد. بهطور پیشفرض، بایتها یکبار از طریق ~DY ارسال میشوند تا کار خودکفا باشد؛ زمانی خاموش کنید که فایل از قبل روی چاپگر است (بارگذاری مدیر).',
+ embedInZpl: 'ارسال بایتها',
+ embedInZplHint: 'خاموش = چاپگر فایل را دارد؛ فقط ^XG ارسال میشود. روشن = بایتها با هر کار از طریق ~DY ارسال میشوند (پیشفرض).',
+ storeInline: 'جاسازی درونخطی',
},
upca: {
content: 'محتوا (۱۱ رقم)',
diff --git a/src/locales/fi.ts b/src/locales/fi.ts
index efc3ea23..4a6e7afb 100644
--- a/src/locales/fi.ts
+++ b/src/locales/fi.ts
@@ -295,12 +295,20 @@ const fi = {
image: {
source: 'Kuvan lähde',
selectImage: 'Valitse kuva…',
+ removeFromCache: 'Poista välimuistista',
+ removeFromCacheConfirm: 'Poista tämä kuva paikallisesta välimuistista? Se ei ole enää käytettävissä yhdessäkään siihen viittaavassa etiketissä.',
upload: 'Lataa kuva',
uploading: 'Ladataan…',
uploadError: 'Kuvaa ei voitu ladata',
preview: 'Esikatselu',
widthDots: 'Leveys (pisteet)',
threshold: 'Mono-kynnys',
+ storage: 'Tulostimen tallennus',
+ storeOnPrinter: 'Aktivoi',
+ storeOnPrinterHint: 'Viittaa tulostimelle tallennettuun grafiikkaan ^XG:llä. Oletuksena tavut lähetetään kerran ~DY:llä, joten työ on itsenäinen; ota pois käytöstä, kun tiedosto on jo tulostimella (ylläpitäjän lataama).',
+ embedInZpl: 'Lähetä tavut',
+ embedInZplHint: 'Pois = tulostimella on tiedosto jo; lähetetään vain ^XG. Päällä = tavut lähetetään jokaisen työn mukana ~DY:n kautta (oletus).',
+ storeInline: 'Upota suoraan',
},
upca: {
content: 'Sisältö (11 numeroa)',
diff --git a/src/locales/fr.ts b/src/locales/fr.ts
index 98105d2e..4d7cb0db 100644
--- a/src/locales/fr.ts
+++ b/src/locales/fr.ts
@@ -295,12 +295,20 @@ const fr = {
image: {
source: 'Source de l\'image',
selectImage: 'Sélectionner une image…',
+ removeFromCache: 'Supprimer du cache',
+ removeFromCacheConfirm: 'Supprimer cette image du cache local ? Elle ne sera plus disponible pour toutes les étiquettes qui y font référence.',
upload: 'Télécharger une image',
uploading: 'Téléchargement…',
uploadError: 'Impossible de charger l\'image',
preview: 'Aperçu',
widthDots: 'Largeur (points)',
threshold: 'Seuil mono',
+ storage: 'Stockage de l’imprimante',
+ storeOnPrinter: 'Activer',
+ storeOnPrinterHint: 'Référence un graphique stocké sur l’imprimante via ^XG. Par défaut, les octets sont envoyés une fois via ~DY pour que le travail soit autonome ; désactivez quand le fichier est déjà sur l’imprimante (téléversé par l’administrateur).',
+ embedInZpl: 'Envoyer les octets',
+ embedInZplHint: 'Désactivé = l’imprimante a déjà le fichier ; seul ^XG est émis. Activé = les octets sont envoyés à chaque travail via ~DY (par défaut).',
+ storeInline: 'Intégrer en ligne',
},
upca: {
content: 'Contenu (11 chiffres)',
diff --git a/src/locales/he.ts b/src/locales/he.ts
index 18d47b37..01852054 100644
--- a/src/locales/he.ts
+++ b/src/locales/he.ts
@@ -295,12 +295,20 @@ const he = {
image: {
source: 'מקור תמונה',
selectImage: 'בחר תמונה…',
+ removeFromCache: 'הסר מהמטמון',
+ removeFromCacheConfirm: 'להסיר תמונה זו מהמטמון המקומי? היא לא תהיה זמינה לכל התוויות שמפנות אליה.',
upload: 'העלאת תמונה',
uploading: 'מעלה…',
uploadError: 'לא ניתן לטעון את התמונה',
preview: 'תצוגה מקדימה',
widthDots: 'רוחב (נקודות)',
threshold: 'סף מונו',
+ storage: 'אחסון מדפסת',
+ storeOnPrinter: 'הפעלה',
+ storeOnPrinterHint: 'מפנה לגרפיקה השמורה במדפסת באמצעות ^XG. כברירת מחדל הבתים נשלחים פעם אחת באמצעות ~DY כך שהעבודה עצמאית; כבה כשהקובץ כבר במדפסת (הועלה על ידי מנהל).',
+ embedInZpl: 'שליחת בתים',
+ embedInZplHint: 'כבוי = למדפסת כבר יש את הקובץ; רק ^XG נשלח. דלוק = הבתים נשלחים עם כל עבודה דרך ~DY (ברירת מחדל).',
+ storeInline: 'הטמע בתוך',
},
upca: {
content: 'תוכן (11 ספרות)',
diff --git a/src/locales/hr.ts b/src/locales/hr.ts
index e5740d9d..49a39546 100644
--- a/src/locales/hr.ts
+++ b/src/locales/hr.ts
@@ -295,12 +295,20 @@ const hr = {
image: {
source: 'Izvor slike',
selectImage: 'Odaberite sliku…',
+ removeFromCache: 'Ukloni iz priručne memorije',
+ removeFromCacheConfirm: 'Ukloniti ovu sliku iz lokalne priručne memorije? Postat će nedostupna za sve naljepnice koje je referenciraju.',
upload: 'Prenesi sliku',
uploading: 'Prenošenje…',
uploadError: 'Sliku nije moguće učitati',
preview: 'Pregled',
widthDots: 'Širina (točke)',
threshold: 'Mono prag',
+ storage: 'Pohrana pisača',
+ storeOnPrinter: 'Aktiviraj',
+ storeOnPrinterHint: 'Referencira grafiku pohranjenu na pisaču putem ^XG. Po zadanom se bajtovi šalju jednom putem ~DY tako da je zadatak samostalan; isključi kad je datoteka već na pisaču (učitao administrator).',
+ embedInZpl: 'Pošalji bajtove',
+ embedInZplHint: 'Isključeno = pisač već ima datoteku; emitira se samo ^XG. Uključeno = bajtovi se šalju sa svakim zadatkom putem ~DY (zadano).',
+ storeInline: 'Ugradi izravno',
},
upca: {
content: 'Sadržaj (11 znamenki)',
diff --git a/src/locales/hu.ts b/src/locales/hu.ts
index 4f76d8e9..7f3f9800 100644
--- a/src/locales/hu.ts
+++ b/src/locales/hu.ts
@@ -295,12 +295,20 @@ const hu = {
image: {
source: 'Képforrás',
selectImage: 'Kép kiválasztása…',
+ removeFromCache: 'Eltávolítás a gyorsítótárból',
+ removeFromCacheConfirm: 'Eltávolítja ezt a képet a helyi gyorsítótárból? Nem lesz elérhető a rá hivatkozó címkék számára.',
upload: 'Kép feltöltése',
uploading: 'Feltöltés…',
uploadError: 'A kép nem tölthető be',
preview: 'Előnézet',
widthDots: 'Szélesség (pont)',
threshold: 'Mono küszöb',
+ storage: 'Nyomtató tárhely',
+ storeOnPrinter: 'Aktiválás',
+ storeOnPrinterHint: 'A nyomtatón tárolt grafikára hivatkozik ^XG-vel. Alapértelmezés szerint a bájtok egyszer kerülnek ~DY-vel elküldésre, így a feladat önálló; kapcsold ki, ha a fájl már a nyomtatón van (admin által feltöltve).',
+ embedInZpl: 'Bájtok küldése',
+ embedInZplHint: 'Ki = a nyomtatón már megvan a fájl, csak ^XG kerül kibocsátásra. Be = a bájtokat minden feladattal ~DY-vel küldjük (alapértelmezett).',
+ storeInline: 'Beágyazás',
},
upca: {
content: 'Tartalom (11 számjegy)',
diff --git a/src/locales/it.ts b/src/locales/it.ts
index 41bbdd64..5ed5a71b 100644
--- a/src/locales/it.ts
+++ b/src/locales/it.ts
@@ -295,12 +295,20 @@ const it = {
image: {
source: 'Origine immagine',
selectImage: 'Seleziona immagine…',
+ removeFromCache: 'Rimuovi dalla cache',
+ removeFromCacheConfirm: 'Rimuovere questa immagine dalla cache locale? Non sarà più disponibile per tutte le etichette che la referenziano.',
upload: 'Carica immagine',
uploading: 'Caricamento…',
uploadError: 'Impossibile caricare l\'immagine',
preview: 'Anteprima',
widthDots: 'Larghezza (punti)',
threshold: 'Soglia mono',
+ storage: 'Archiviazione stampante',
+ storeOnPrinter: 'Attiva',
+ storeOnPrinterHint: 'Riferimento a una grafica memorizzata sulla stampante tramite ^XG. Per impostazione predefinita i byte vengono inviati una volta con ~DY affinché il lavoro sia autonomo; disattivalo quando il file è già sulla stampante (caricato dall’amministratore).',
+ embedInZpl: 'Invia byte (~DY)',
+ embedInZplHint: 'Off = la stampante ha già il file; viene emesso solo ^XG. On = i byte vengono inviati con ogni lavoro tramite ~DY (predefinito).',
+ storeInline: 'Incorpora',
},
upca: {
content: 'Contenuto (11 cifre)',
diff --git a/src/locales/ja.ts b/src/locales/ja.ts
index 232b70b4..60654285 100644
--- a/src/locales/ja.ts
+++ b/src/locales/ja.ts
@@ -295,12 +295,20 @@ const ja = {
image: {
source: '画像ソース',
selectImage: '画像を選択…',
+ removeFromCache: 'キャッシュから削除',
+ removeFromCacheConfirm: 'この画像をローカルキャッシュから削除しますか?参照しているすべてのラベルで利用できなくなります。',
upload: '画像をアップロード',
uploading: 'アップロード中…',
uploadError: '画像を読み込めませんでした',
preview: 'プレビュー',
widthDots: '幅(ドット)',
threshold: 'モノクロしきい値',
+ storage: 'プリンター記憶',
+ storeOnPrinter: '有効化',
+ storeOnPrinterHint: 'プリンターに保存されたグラフィックを ^XG で参照します。既定では ~DY で一度バイトを送信してジョブを自己完結させます。プリンターに既にファイルがある(管理者がアップロード済み)場合はオフにします。',
+ embedInZpl: 'バイト送信',
+ embedInZplHint: 'オフ = プリンターにファイルが既にあり、^XG のみ送信。オン = 毎ジョブで ~DY によりバイトを送信(既定)。',
+ storeInline: 'インライン埋め込み',
},
upca: {
content: '内容(11桁)',
diff --git a/src/locales/ko.ts b/src/locales/ko.ts
index 61a19520..441fe93f 100644
--- a/src/locales/ko.ts
+++ b/src/locales/ko.ts
@@ -295,12 +295,20 @@ const ko = {
image: {
source: '이미지 소스',
selectImage: '이미지 선택…',
+ removeFromCache: '캐시에서 제거',
+ removeFromCacheConfirm: '이 이미지를 로컬 캐시에서 제거하시겠습니까? 이를 참조하는 모든 라벨에서 사용할 수 없게 됩니다.',
upload: '이미지 업로드',
uploading: '업로드 중…',
uploadError: '이미지를 불러올 수 없습니다',
preview: '미리보기',
widthDots: '너비 (도트)',
threshold: '모노 임계값',
+ storage: '프린터 저장소',
+ storeOnPrinter: '활성화',
+ storeOnPrinterHint: '프린터에 저장된 그래픽을 ^XG로 참조합니다. 기본적으로 ~DY로 바이트를 한 번 전송하여 작업이 자기 완결되도록 합니다. 파일이 이미 프린터에 있는 경우(관리자 업로드)에는 끄세요.',
+ embedInZpl: '바이트 전송',
+ embedInZplHint: '끔 = 프린터에 파일이 이미 있음, ^XG만 전송. 켬 = 매 작업마다 ~DY로 바이트 전송 (기본값).',
+ storeInline: '인라인 포함',
},
upca: {
content: '내용 (11자리)',
diff --git a/src/locales/lt.ts b/src/locales/lt.ts
index 9b523b4c..768e8d0e 100644
--- a/src/locales/lt.ts
+++ b/src/locales/lt.ts
@@ -295,12 +295,20 @@ const lt = {
image: {
source: 'Vaizdo šaltinis',
selectImage: 'Pasirinkite vaizdą…',
+ removeFromCache: 'Pašalinti iš talpyklos',
+ removeFromCacheConfirm: 'Pašalinti šį paveikslėlį iš vietinės talpyklos? Jis nebebus prieinamas visoms etiketėms, kurios į jį nukreipia.',
upload: 'Įkelti vaizdą',
uploading: 'Įkeliama…',
uploadError: 'Nepavyko įkelti vaizdo',
preview: 'Peržiūra',
widthDots: 'Plotis (taškai)',
threshold: 'Mono slenkstis',
+ storage: 'Spausdintuvo saugykla',
+ storeOnPrinter: 'Aktyvinti',
+ storeOnPrinterHint: 'Nurodo spausdintuve saugomą grafiką per ^XG. Pagal numatytuosius nustatymus baitai siunčiami vieną kartą per ~DY, kad darbas būtų savarankiškas; išjunkite, kai failas jau yra spausdintuve (administratoriaus įkelta).',
+ embedInZpl: 'Siųsti baitus',
+ embedInZplHint: 'Išjungta = spausdintuvas jau turi failą; siunčiamas tik ^XG. Įjungta = baitai siunčiami su kiekvienu darbu per ~DY (numatyta).',
+ storeInline: 'Įterpti tiesiogiai',
},
upca: {
content: 'Turinys (11 skaitmenų)',
diff --git a/src/locales/lv.ts b/src/locales/lv.ts
index 406db758..3dccb284 100644
--- a/src/locales/lv.ts
+++ b/src/locales/lv.ts
@@ -295,12 +295,20 @@ const lv = {
image: {
source: 'Attēla avots',
selectImage: 'Izvēlēties attēlu…',
+ removeFromCache: 'Noņemt no kešatmiņas',
+ removeFromCacheConfirm: 'Noņemt šo attēlu no vietējās kešatmiņas? Tas vairs nebūs pieejams visām etiķetēm, kas uz to atsaucas.',
upload: 'Augšupielādēt attēlu',
uploading: 'Augšupielāde…',
uploadError: 'Neizdevās ielādēt attēlu',
preview: 'Priekšskatījums',
widthDots: 'Platums (punkti)',
threshold: 'Mono slieksnis',
+ storage: 'Printera krātuve',
+ storeOnPrinter: 'Aktivizēt',
+ storeOnPrinterHint: 'Atsaucas uz printerī saglabātu grafiku, izmantojot ^XG. Pēc noklusējuma baiti tiek sūtīti vienreiz, izmantojot ~DY, lai darbs būtu pašpietiekams; izslēdziet, kad fails jau ir printerī (administratora augšupielādēts).',
+ embedInZpl: 'Sūtīt baitus',
+ embedInZplHint: 'Izsl. = printerim jau ir fails; tiek izvadīts tikai ^XG. Iesl. = baiti tiek sūtīti ar katru darbu, izmantojot ~DY (noklusējums).',
+ storeInline: 'Iegult tieši',
},
upca: {
content: 'Saturs (11 cipari)',
diff --git a/src/locales/nl.ts b/src/locales/nl.ts
index 33185505..5cdbcf2a 100644
--- a/src/locales/nl.ts
+++ b/src/locales/nl.ts
@@ -295,12 +295,20 @@ const nl = {
image: {
source: 'Afbeeldingsbron',
selectImage: 'Afbeelding selecteren…',
+ removeFromCache: 'Uit cache verwijderen',
+ removeFromCacheConfirm: 'Deze afbeelding uit de lokale cache verwijderen? Ze is dan niet meer beschikbaar voor labels die ernaar verwijzen.',
upload: 'Afbeelding uploaden',
uploading: 'Uploaden…',
uploadError: 'Kon afbeelding niet laden',
preview: 'Voorbeeld',
widthDots: 'Breedte (punten)',
threshold: 'Mono-drempelwaarde',
+ storage: 'Printer-opslag',
+ storeOnPrinter: 'Activeren',
+ storeOnPrinterHint: 'Verwijst naar een afbeelding op de printer via ^XG. Standaard worden de bytes één keer meegestuurd via ~DY zodat de taak op zichzelf staat; schakel uit wanneer het bestand al op de printer staat (door beheerder geüpload).',
+ embedInZpl: 'Bytes meesturen',
+ embedInZplHint: 'Uit = printer heeft het bestand al; alleen ^XG wordt verzonden. Aan = bytes worden bij elke taak meegestuurd via ~DY (standaard).',
+ storeInline: 'Inline insluiten',
},
upca: {
content: 'Inhoud (11 cijfers)',
diff --git a/src/locales/no.ts b/src/locales/no.ts
index 136ed305..66184120 100644
--- a/src/locales/no.ts
+++ b/src/locales/no.ts
@@ -295,12 +295,20 @@ const no = {
image: {
source: 'Bildekilde',
selectImage: 'Velg bilde…',
+ removeFromCache: 'Fjern fra hurtigminne',
+ removeFromCacheConfirm: 'Fjerne dette bildet fra det lokale hurtigminnet? Det blir utilgjengelig for alle etiketter som refererer til det.',
upload: 'Last opp bilde',
uploading: 'Laster opp…',
uploadError: 'Kunne ikke laste bilde',
preview: 'Forhåndsvisning',
widthDots: 'Bredde (punkter)',
threshold: 'Mono-terskel',
+ storage: 'Skriverlagring',
+ storeOnPrinter: 'Aktivér',
+ storeOnPrinterHint: 'Refererer til en grafikk lagret på skriveren via ^XG. Som standard sendes byte én gang via ~DY slik at jobben er selvstendig; slå av når filen allerede er på skriveren (administrator-opplastet).',
+ embedInZpl: 'Send bytes',
+ embedInZplHint: 'Av = skriveren har allerede filen; kun ^XG sendes. På = byte sendes med hver jobb via ~DY (standard).',
+ storeInline: 'Bygg inn direkte',
},
upca: {
content: 'Innhold (11 siffer)',
diff --git a/src/locales/pl.ts b/src/locales/pl.ts
index ca0dace9..e388e272 100644
--- a/src/locales/pl.ts
+++ b/src/locales/pl.ts
@@ -295,12 +295,20 @@ const pl = {
image: {
source: 'Źródło obrazu',
selectImage: 'Wybierz obraz…',
+ removeFromCache: 'Usuń z pamięci podręcznej',
+ removeFromCacheConfirm: 'Usunąć ten obraz z lokalnej pamięci podręcznej? Stanie się niedostępny dla wszystkich etykiet, które się do niego odwołują.',
upload: 'Prześlij obraz',
uploading: 'Przesyłanie…',
uploadError: 'Nie można załadować obrazu',
preview: 'Podgląd',
widthDots: 'Szerokość (punkty)',
threshold: 'Próg mono',
+ storage: 'Pamięć drukarki',
+ storeOnPrinter: 'Aktywuj',
+ storeOnPrinterHint: 'Odwołuje się do grafiki przechowywanej na drukarce za pomocą ^XG. Domyślnie bajty są wysyłane raz przez ~DY, dzięki czemu zadanie jest samowystarczalne; wyłącz, gdy plik jest już na drukarce (przesłany przez administratora).',
+ embedInZpl: 'Wyślij bajty',
+ embedInZplHint: 'Wył = drukarka już posiada plik; emitowane jest tylko ^XG. Wł = bajty wysyłane z każdym zadaniem przez ~DY (domyślnie).',
+ storeInline: 'Osadź bezpośrednio',
},
upca: {
content: 'Zawartość (11 cyfr)',
diff --git a/src/locales/pt.ts b/src/locales/pt.ts
index d144b7e3..2aec97eb 100644
--- a/src/locales/pt.ts
+++ b/src/locales/pt.ts
@@ -295,12 +295,20 @@ const pt = {
image: {
source: 'Fonte da imagem',
selectImage: 'Selecionar imagem…',
+ removeFromCache: 'Remover do cache',
+ removeFromCacheConfirm: 'Remover esta imagem do cache local? Ficará indisponível para todas as etiquetas que a referenciam.',
upload: 'Carregar imagem',
uploading: 'Carregando…',
uploadError: 'Não foi possível carregar a imagem',
preview: 'Pré-visualização',
widthDots: 'Largura (pontos)',
threshold: 'Limiar mono',
+ storage: 'Armazenamento da impressora',
+ storeOnPrinter: 'Ativar',
+ storeOnPrinterHint: 'Referencia uma imagem armazenada na impressora via ^XG. Por padrão, os bytes são enviados uma vez via ~DY para que o trabalho seja autocontido; desligue quando o arquivo já estiver na impressora (carregado pelo administrador).',
+ embedInZpl: 'Enviar bytes',
+ embedInZplHint: 'Desligado = a impressora já tem o ficheiro; apenas ^XG é emitido. Ligado = bytes enviados em cada trabalho via ~DY (padrão).',
+ storeInline: 'Incorporar',
},
upca: {
content: 'Conteúdo (11 dígitos)',
diff --git a/src/locales/ro.ts b/src/locales/ro.ts
index c85e19d0..4eb925c5 100644
--- a/src/locales/ro.ts
+++ b/src/locales/ro.ts
@@ -295,12 +295,20 @@ const ro = {
image: {
source: 'Sursă imagine',
selectImage: 'Selectați imaginea…',
+ removeFromCache: 'Elimină din cache',
+ removeFromCacheConfirm: 'Elimini această imagine din cache-ul local? Va deveni indisponibilă pentru toate etichetele care o referențiază.',
upload: 'Încărcați imaginea',
uploading: 'Se încarcă…',
uploadError: 'Imaginea nu a putut fi încărcată',
preview: 'Previzualizare',
widthDots: 'Lățime (puncte)',
threshold: 'Prag mono',
+ storage: 'Stocare imprimantă',
+ storeOnPrinter: 'Activează',
+ storeOnPrinterHint: 'Referențiază un grafic stocat pe imprimantă prin ^XG. Implicit, octeții sunt trimiși o dată prin ~DY astfel încât lucrarea să fie autonomă; dezactivează când fișierul este deja pe imprimantă (încărcat de administrator).',
+ embedInZpl: 'Trimite octeții',
+ embedInZplHint: 'Off = imprimanta are deja fișierul; se emite doar ^XG. On = octeții sunt trimiși cu fiecare sarcină prin ~DY (implicit).',
+ storeInline: 'Încorporează direct',
},
upca: {
content: 'Conținut (11 cifre)',
diff --git a/src/locales/sk.ts b/src/locales/sk.ts
index c0f5b683..98b87aa0 100644
--- a/src/locales/sk.ts
+++ b/src/locales/sk.ts
@@ -295,12 +295,20 @@ const sk = {
image: {
source: 'Zdroj obrázka',
selectImage: 'Vybrať obrázok…',
+ removeFromCache: 'Odstrániť z vyrovnávacej pamäte',
+ removeFromCacheConfirm: 'Odstrániť tento obrázok z lokálnej vyrovnávacej pamäte? Bude nedostupný pre všetky etikety, ktoré naň odkazujú.',
upload: 'Nahrať obrázok',
uploading: 'Nahrávanie…',
uploadError: 'Obrázok sa nedá načítať',
preview: 'Náhľad',
widthDots: 'Šírka (body)',
threshold: 'Mono prah',
+ storage: 'Úložisko tlačiarne',
+ storeOnPrinter: 'Aktivovať',
+ storeOnPrinterHint: 'Odkazuje na grafiku uloženú v tlačiarni cez ^XG. Predvolene sa bajty odošlú raz cez ~DY, takže úloha je samostatná; vypnite, keď je súbor už v tlačiarni (nahral správca).',
+ embedInZpl: 'Odoslať bajty',
+ embedInZplHint: 'Vyp = tlačiareň už má súbor; vysiela sa iba ^XG. Zap = bajty sa posielajú s každou úlohou cez ~DY (predvolené).',
+ storeInline: 'Vložiť priamo',
},
upca: {
content: 'Obsah (11 číslic)',
diff --git a/src/locales/sl.ts b/src/locales/sl.ts
index 690b82a1..995049c4 100644
--- a/src/locales/sl.ts
+++ b/src/locales/sl.ts
@@ -295,12 +295,20 @@ const sl = {
image: {
source: 'Vir slike',
selectImage: 'Izberite sliko…',
+ removeFromCache: 'Odstrani iz predpomnilnika',
+ removeFromCacheConfirm: 'Odstraniti to sliko iz lokalnega predpomnilnika? Postala bo nedostopna za vse etikete, ki jo referencirajo.',
upload: 'Naloži sliko',
uploading: 'Nalaganje…',
uploadError: 'Slike ni bilo mogoče naložiti',
preview: 'Predogled',
widthDots: 'Širina (pike)',
threshold: 'Mono prag',
+ storage: 'Shramba tiskalnika',
+ storeOnPrinter: 'Aktiviraj',
+ storeOnPrinterHint: 'Sklicuje se na grafiko, shranjeno v tiskalniku, prek ^XG. Privzeto se bajti pošljejo enkrat prek ~DY, tako da je opravilo samostojno; izklopite, ko je datoteka že v tiskalniku (administrator je naložil).',
+ embedInZpl: 'Pošlji bajte',
+ embedInZplHint: 'Izklopljeno = tiskalnik že ima datoteko; oddaja se samo ^XG. Vklopljeno = bajti se pošljejo z vsakim opravilom prek ~DY (privzeto).',
+ storeInline: 'Vstavi neposredno',
},
upca: {
content: 'Vsebina (11 števk)',
diff --git a/src/locales/sr.ts b/src/locales/sr.ts
index a123752d..294ea3a9 100644
--- a/src/locales/sr.ts
+++ b/src/locales/sr.ts
@@ -295,12 +295,20 @@ const sr = {
image: {
source: 'Извор слике',
selectImage: 'Изаберите слику…',
+ removeFromCache: 'Уклони из кеша',
+ removeFromCacheConfirm: 'Уклонити ову слику из локалног кеша? Биће недоступна за све налепнице које је реферишу.',
upload: 'Отпремите слику',
uploading: 'Отпремање…',
uploadError: 'Слика није могла да се учита',
preview: 'Преглед',
widthDots: 'Ширина (тачке)',
threshold: 'Моно праг',
+ storage: 'Меморија штампача',
+ storeOnPrinter: 'Активирај',
+ storeOnPrinterHint: 'Реферише на графику ускладиштену у штампачу преко ^XG. Подразумевано се бајтови шаљу једном преко ~DY, тако да је посао самосталан; искључите када је датотека већ на штампачу (администратор је учитао).',
+ embedInZpl: 'Пошаљи бајтове',
+ embedInZplHint: 'Искључено = штампач већ има датотеку; шаље се само ^XG. Укључено = бајтови се шаљу са сваким послом преко ~DY (подразумевано).',
+ storeInline: 'Уграђено',
},
upca: {
content: 'Садржај (11 цифара)',
diff --git a/src/locales/sv.ts b/src/locales/sv.ts
index 1ba78277..1ebfbbe9 100644
--- a/src/locales/sv.ts
+++ b/src/locales/sv.ts
@@ -295,12 +295,20 @@ const sv = {
image: {
source: 'Bildkälla',
selectImage: 'Välj bild…',
+ removeFromCache: 'Ta bort från cache',
+ removeFromCacheConfirm: 'Ta bort denna bild från den lokala cachen? Den blir otillgänglig för alla etiketter som refererar till den.',
upload: 'Ladda upp bild',
uploading: 'Laddar upp…',
uploadError: 'Kunde inte ladda bilden',
preview: 'Förhandsvisning',
widthDots: 'Bredd (punkter)',
threshold: 'Mono-tröskel',
+ storage: 'Skrivarlagring',
+ storeOnPrinter: 'Aktivera',
+ storeOnPrinterHint: 'Refererar till en grafik som är lagrad på skrivaren via ^XG. Som standard skickas byten en gång via ~DY så att jobbet är fristående; stäng av när filen redan finns på skrivaren (uppladdad av administratör).',
+ embedInZpl: 'Skicka bytes',
+ embedInZplHint: 'Av = skrivaren har redan filen; endast ^XG skickas. På = bytes skickas med varje jobb via ~DY (standard).',
+ storeInline: 'Bädda in direkt',
},
upca: {
content: 'Innehåll (11 siffror)',
diff --git a/src/locales/tr.ts b/src/locales/tr.ts
index 97bfd584..589e55d2 100644
--- a/src/locales/tr.ts
+++ b/src/locales/tr.ts
@@ -295,12 +295,20 @@ const tr = {
image: {
source: 'Görsel kaynağı',
selectImage: 'Görsel seçin…',
+ removeFromCache: 'Önbellekten kaldır',
+ removeFromCacheConfirm: 'Bu görüntüyü yerel önbellekten kaldır? Ona başvuran tüm etiketler için kullanılamaz hale gelir.',
upload: 'Görsel yükle',
uploading: 'Yükleniyor…',
uploadError: 'Görsel yüklenemedi',
preview: 'Önizleme',
widthDots: 'Genişlik (nokta)',
threshold: 'Mono eşik',
+ storage: 'Yazıcı depolama',
+ storeOnPrinter: 'Etkinleştir',
+ storeOnPrinterHint: 'Yazıcıda saklanan bir grafiğe ^XG ile başvurur. Varsayılan olarak baytlar bir kez ~DY ile gönderilir, böylece iş bağımsızdır; dosya zaten yazıcıdaysa (yönetici tarafından yüklendi) kapatın.',
+ embedInZpl: 'Baytları gönder',
+ embedInZplHint: 'Kapalı = yazıcıda dosya zaten var, yalnızca ^XG yayılır. Açık = baytlar her işle ~DY üzerinden gönderilir (varsayılan).',
+ storeInline: 'Satır içi göm',
},
upca: {
content: 'İçerik (11 hane)',
diff --git a/src/locales/zh-hans.ts b/src/locales/zh-hans.ts
index 4fd4146a..92c047e0 100644
--- a/src/locales/zh-hans.ts
+++ b/src/locales/zh-hans.ts
@@ -295,12 +295,20 @@ const zhHans = {
image: {
source: '图片来源',
selectImage: '选择图片…',
+ removeFromCache: '从缓存中移除',
+ removeFromCacheConfirm: '从本地缓存中移除此图像?所有引用它的标签都将无法使用它。',
upload: '上传图片',
uploading: '上传中…',
uploadError: '无法加载图像',
preview: '预览',
widthDots: '宽度(点)',
threshold: '单色阈值',
+ storage: '打印机存储',
+ storeOnPrinter: '启用',
+ storeOnPrinterHint: '通过 ^XG 引用存储在打印机上的图形。默认通过 ~DY 一次发送字节,使作业自包含;当文件已在打印机上(由管理员上传)时关闭。',
+ embedInZpl: '随附字节',
+ embedInZplHint: '关 = 打印机已有文件,仅发出 ^XG。开 = 每次任务通过 ~DY 携带字节(默认)。',
+ storeInline: '内嵌',
},
upca: {
content: '内容(11位)',
diff --git a/src/locales/zh-hant.ts b/src/locales/zh-hant.ts
index 469dc221..ba387c38 100644
--- a/src/locales/zh-hant.ts
+++ b/src/locales/zh-hant.ts
@@ -295,12 +295,20 @@ const zhHant = {
image: {
source: '圖片來源',
selectImage: '選擇圖片…',
+ removeFromCache: '從快取中移除',
+ removeFromCacheConfirm: '從本地快取中移除此影像?所有引用它的標籤都將無法使用它。',
upload: '上傳圖片',
uploading: '上傳中…',
uploadError: '無法載入圖片',
preview: '預覽',
widthDots: '寬度(點)',
threshold: '單色閾值',
+ storage: '印表機儲存',
+ storeOnPrinter: '啟用',
+ storeOnPrinterHint: '透過 ^XG 引用儲存在印表機上的圖形。預設透過 ~DY 一次傳送位元組,使工作自包含;當檔案已在印表機上(由管理員上傳)時關閉。',
+ embedInZpl: '附帶位元組',
+ embedInZplHint: '關 = 印表機已有檔案,僅發出 ^XG。開 = 每次工作透過 ~DY 攜帶位元組(預設)。',
+ storeInline: '內嵌',
},
upca: {
content: '內容(11位)',
diff --git a/src/registry/image.tsx b/src/registry/image.tsx
index cb95bf3e..d1d58bdb 100644
--- a/src/registry/image.tsx
+++ b/src/registry/image.tsx
@@ -1,17 +1,33 @@
import { useState, useRef, useCallback } from 'react';
+import { InformationCircleIcon, TrashIcon } from '@heroicons/react/16/solid';
import type { ObjectTypeDefinition } from '../types/ObjectType';
import { useT } from '../lib/useT';
-import { inputCls, labelCls } from '../components/Properties/styles';
+import { buttonCls, inputCls, labelCls } from '../components/Properties/styles';
import { fieldPos } from './zplHelpers';
-import { loadImageFile, getImage, getAllImages } from '../lib/imageCache';
+import { loadImageFile, getImage, getAllImages, removeImage } from '../lib/imageCache';
import { imageToGFA } from '../lib/imageToZpl';
-import { formatStoragePath } from '../lib/storagePath';
+import {
+ defaultStorageName,
+ formatStoragePath,
+ MAX_STORAGE_NAME_LEN,
+ STORAGE_DEVICES,
+ STORAGE_NAME_FILTER_RE,
+ type StorageDevice,
+} from '../lib/storagePath';
+import { ConfirmDialog } from '../components/ui/ConfirmDialog';
export interface ImageProps {
/** ID into the image cache */
imageId: string;
- /** Target width in dots (height derived from aspect ratio) */
+ /** Target width in dots (height derived from aspect ratio when a cached
+ * PNG is available; falls back to `heightDots` for recall-only
+ * placeholders). */
widthDots: number;
+ /** Override height for placeholder/recall-only images that have no
+ * cached bytes — without it the box would snap to a fixed default
+ * and ignore the user's drag. Only consulted when `imageId` does
+ * not resolve to a cached image. */
+ heightDots?: number;
/** Luminance threshold for mono conversion (0–255) */
threshold: number;
/** Cached GFA ZPL string — regenerated when image/width/threshold changes */
@@ -25,6 +41,11 @@ export interface ImageProps {
device: string;
/** Filename stem (no extension); paired with `.GRF` for graphics. */
name: string;
+ /** Ship the bitmap bytes via `~DY` alongside the `^XG` reference.
+ * Default true on first toggle so a single-job ZPL is self-contained.
+ * False = recall-only: assume the file is already on printer storage,
+ * emit only `^XG`. Mirrors the customFonts `embedInZpl` pattern. */
+ embedInZpl?: boolean;
};
}
@@ -83,6 +104,37 @@ export const image: ObjectTypeDefinition = {
},
defaultSize: { width: 200, height: 200 },
+ // Resize via canvas-handle:
+ // - With cached PNG → aspect locked, height re-derives from widthDots.
+ // Pick the dominant scale (largest deviation from 1) so all eight
+ // handles work for both grow and shrink. Math.max would mis-handle
+ // inward single-axis drags (sx=0.5, sy=1 → max=1 → no change).
+ // - Without cache (recall-only placeholder) → free-form. widthDots
+ // and heightDots scale independently so the user can shape the
+ // placeholder box for layout purposes.
+ // _gfaCache always cleared — for cached images the hex needs regen at
+ // the new width; for placeholders it's empty anyway.
+ commitTransform: (obj, ctx) => {
+ const { sx, sy, snap } = ctx;
+ const cached = getImage(obj.props.imageId);
+ const widthDots = (scale: number): number =>
+ Math.max(8, snap(Math.round(obj.props.widthDots * scale)));
+ if (cached) {
+ const dominant = Math.abs(sx - 1) >= Math.abs(sy - 1) ? sx : sy;
+ return { widthDots: widthDots(dominant), _gfaCache: undefined };
+ }
+ // First-resize fallback for heightDots: use the current widthDots so
+ // the implicit default (square placeholder) matches what the canvas
+ // renders before the user has dragged. Drifting from that — e.g. a
+ // hard-coded 200 — would mean the first drag visibly snaps the box.
+ const baseHeight = obj.props.heightDots ?? obj.props.widthDots;
+ return {
+ widthDots: widthDots(sx),
+ heightDots: Math.max(8, snap(Math.round(baseHeight * sy))),
+ _gfaCache: undefined,
+ };
+ },
+
toZPL: (obj) => {
const p = obj.props;
// Recall path: upload happened in the preamble; here we just reference
@@ -105,6 +157,7 @@ export const image: ObjectTypeDefinition = {
const fileRef = useRef(null);
const [uploading, setUploading] = useState(false);
const [uploadFailed, setUploadFailed] = useState(false);
+ const [pendingCacheDelete, setPendingCacheDelete] = useState(false);
const cached = getImage(p.imageId);
const allImages = getAllImages();
@@ -129,6 +182,14 @@ export const image: ObjectTypeDefinition = {
}, [onChange, p.widthDots, p.threshold]);
const handleImageSelect = useCallback(async (imageId: string) => {
+ // Empty selection = "no image bytes". Legitimate when the user is
+ // setting up a recall-only reference (storedAs without a local
+ // preview image). Clear the cache pointer + ^GFA cache so the
+ // ZPL emitter doesn't carry stale bytes from the previous source.
+ if (!imageId) {
+ onChange({ imageId: '', _gfaCache: undefined });
+ return;
+ }
const img = getImage(imageId);
if (!img) return;
const result = await imageToGFA(img.dataUrl, p.widthDots, p.threshold);
@@ -149,22 +210,45 @@ export const image: ObjectTypeDefinition = {
onChange({ threshold, _gfaCache: result.zpl });
}, [onChange, p.imageId, p.widthDots]);
+ // Lifted local const so the storage-section closures get a narrowed
+ // reference that survives into onChange callbacks. Without it TS
+ // re-widens `p.storedAs` to `... | undefined` inside the handlers
+ // and we'd need `?.`-fallbacks for every field access.
+ const storedAs = p.storedAs;
+
return (
{/* Image select / upload */}
{allImages.length > 0 && (
-
+
+
+ {/* Delete the *cached* file (data-URL) from imageCache +
+ localStorage. Different from removing the image-object
+ via Del: this clears the bytes shared across all
+ objects referencing the same imageId. Skip when the
+ current image-object has no source selected. */}
+ {p.imageId && (
+
+ )}
+
)}
= {
/>
+
+ {/* Printer storage (~DY + ^XG). Section label + info icon are
+ always visible so the feature is discoverable in both states;
+ the body switches between an Activate-button (off) and the
+ device/name editor (on). Border-top separates it visually
+ from the rendering properties above. */}
+
+
+
+
+
+ {storedAs ? (
+ <>
+
+
+ {
+ const next = e.target.value
+ .toUpperCase()
+ .replace(STORAGE_NAME_FILTER_RE, '')
+ .slice(0, MAX_STORAGE_NAME_LEN);
+ // Silently ignore keystrokes that would empty the name:
+ // an empty stem produces broken ZPL (`~DYR:,A,G,...`),
+ // and a controlled-component "refuses-to-delete-last-char"
+ // is a clearer constraint signal than a tooltip.
+ if (!next) return;
+ onChange({
+ storedAs: { ...storedAs, name: next },
+ });
+ }}
+ />
+