From 5b0200bba2ccb9250214f386f7cfabb576dae553 Mon Sep 17 00:00:00 2001 From: u8array Date: Thu, 21 May 2026 23:57:29 +0200 Subject: [PATCH 1/2] feat(image): printer-storage UI for ~DY+^XG upload/recall MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Makes the ~DY+^XG feature from PR #84 usable for new images in the designer — until now it only kicked in when importing existing labels that already used the upload+recall pattern. Properties-panel section at the end of the image properties, with a consistent 'Printer storage' header (info icon, border-t separator) visible in both states: - Off-state: 'Activate' button. Toggling on sets storedAs to { device: 'R', name: 'IMG_xxxx' } with an auto-generated 4-char UUID. - On-state: device dropdown (R/E/B/A as plain letters), name input (max 8 chars, auto-uppercase, [A-Z0-9_] filter, empty-guard against broken ZPL), path preview, 'Ship bytes' checkbox + hint (decouples upload from recall: off = ^XG only, on = ~DY+^XG), 'Embed inline' button as the toggle-off. Parser extension: ^XG without a preceding ~DY is now a valid import (recall-only). Creates an image object with storedAs.embedInZpl=false and no cached bytes; surfaced as a partial finding so the import report flags the degraded preview. Cache deletion: trash button next to the image-source dropdown opens a ConfirmDialog (destructive, autoFocus on Cancel) explaining that the bytes vanish for ALL labels referencing this image. Previously only the image *object* could be removed (Del key), not the cached bytes — so old uploads accumulated in localStorage with no UI to clean them up. Canvas resize via handles: the image registry had no commitTransform, so Konva applied sx/sy as a visual scale but nothing translated that back into widthDots — the only way to resize was the properties panel. Now: cached PNG = aspect-locked via the dominant-axis drag (Math.abs heuristic so all 8 handles work for both grow and shrink); no cache (placeholder) = free-form via heightDots so the user can shape the box as a layout placeholder. Empty source selection: handleImageSelect bailed early on an empty imageId, leaving the 'Select image…' placeholder option functionally dead. Now it clears imageId + _gfaCache, which is the right answer for recall-only setups where the user wants a storedAs path without local preview bytes. Canvas placeholder: 🖼 emoji replaced by a Konva.Path rendering the Heroicons 'photo' outline. The emoji rendered inconsistently across OSes (color on macOS, monochrome on Linux, missing on some Windows configurations); the vector path is deterministic. Shared infrastructure that fell out of the work: - src/lib/storagePath.ts extended with STORAGE_DEVICES, StorageDevice, MAX_STORAGE_NAME_LEN, STORAGE_NAME_FILTER_RE, defaultStorageName — all storage-path concepts living in one canonical place so the parser, emitter, and image registry stay in lockstep. - src/components/Properties/styles.ts: new buttonCls for secondary- action buttons (Upload, toggle, Embed inline), eliminating the four-times-repeated Tailwind string across image.tsx and text.tsx. ZPL codes (^XG, ~DY) are kept to tooltips only, never in user-visible labels. New locale keys (storage, storeOnPrinter, storeOnPrinterHint, storeInline, embedInZpl, embedInZplHint, removeFromCache, removeFromCacheConfirm) cover all 32 locales. --- src/components/Canvas/ImageObject.tsx | 51 ++++-- src/components/Properties/styles.ts | 4 + src/lib/storagePath.ts | 21 +++ src/lib/zplGenerator.test.ts | 31 ++-- src/lib/zplGenerator.ts | 4 + src/lib/zplParser.test.ts | 19 ++- src/lib/zplParser.ts | 53 +++--- src/locales/ar.ts | 8 + src/locales/bg.ts | 8 + src/locales/cs.ts | 8 + src/locales/da.ts | 8 + src/locales/de.ts | 8 + src/locales/el.ts | 8 + src/locales/en.ts | 8 + src/locales/es.ts | 8 + src/locales/et.ts | 8 + src/locales/fa.ts | 8 + src/locales/fi.ts | 8 + src/locales/fr.ts | 8 + src/locales/he.ts | 8 + src/locales/hr.ts | 8 + src/locales/hu.ts | 8 + src/locales/it.ts | 8 + src/locales/ja.ts | 8 + src/locales/ko.ts | 8 + src/locales/lt.ts | 8 + src/locales/lv.ts | 8 + src/locales/nl.ts | 8 + src/locales/no.ts | 8 + src/locales/pl.ts | 8 + src/locales/pt.ts | 8 + src/locales/ro.ts | 8 + src/locales/sk.ts | 8 + src/locales/sl.ts | 8 + src/locales/sr.ts | 8 + src/locales/sv.ts | 8 + src/locales/tr.ts | 8 + src/locales/zh-hans.ts | 8 + src/locales/zh-hant.ts | 8 + src/registry/image.tsx | 221 ++++++++++++++++++++++++-- src/registry/text.tsx | 4 +- 41 files changed, 597 insertions(+), 67 deletions(-) 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..6f453a53 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: { device: storedAs.device, name: next }, + }); + }} + /> +
+ + {formatStoragePath(storedAs, true)} + + + + + ) : ( + + )} +
+ {pendingCacheDelete && ( + { + removeImage(p.imageId); + onChange({ imageId: '', _gfaCache: undefined }); + setPendingCacheDelete(false); + }} + onCancel={() => setPendingCacheDelete(false)} + /> + )}
); }, diff --git a/src/registry/text.tsx b/src/registry/text.tsx index ca5c9b65..8c7d6288 100644 --- a/src/registry/text.tsx +++ b/src/registry/text.tsx @@ -1,7 +1,7 @@ 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 { buttonCls, inputCls, labelCls } from "../components/Properties/styles"; import { textFieldPos, fdField, resolveFontCmd, wrapReverse } from "./zplHelpers"; import { effectiveScale } from "./transformHelpers"; import { getFont, loadFontFile } from "../lib/fontCache"; @@ -189,7 +189,7 @@ export const text: ObjectTypeDefinition = { />