Skip to content

Commit df89a26

Browse files
committed
feat(image): the delivery select names an inline graphic on the printer itself
1 parent 3b82b19 commit df89a26

40 files changed

Lines changed: 262 additions & 101 deletions

‎packages/core/src/lib/resourceDelivery.test.ts‎

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, expect, it } from "vitest";
2-
import { applyFontDelivery, applyGraphicDelivery, fontDelivery, graphicDelivery, graphicJobShips } from "./resourceDelivery";
2+
import { applyFontDelivery, applyGraphicDelivery, draftGraphicIdentity, fontDelivery, graphicDelivery, graphicJobShips, graphicWayBlocks } from "./resourceDelivery";
33
import type { ImageProps } from "../registry/image";
44

55
const GFA = "^GFA,4,4,1,00FFFF00";
@@ -42,8 +42,8 @@ describe("fontDelivery", () => {
4242
});
4343

4444
describe("graphicDelivery", () => {
45-
it("has no way to choose for an inline ^GF, and reads job while the design ships the bytes", () => {
46-
expect(graphicDelivery(image(null), [entry])).toBeUndefined();
45+
it("reads job for an inline ^GF and for a design that ships the bytes", () => {
46+
expect(graphicDelivery(image(null), [entry])).toBe("job");
4747
expect(graphicDelivery(image(), [entry])).toBe("job");
4848
expect(graphicDelivery(image({ embedInZpl: false }), [entry])).toBe("setup");
4949
expect(graphicDelivery(image({ embedInZpl: false }), undefined)).toBe("printer");
@@ -80,7 +80,39 @@ describe("graphicDelivery", () => {
8080
expect(graphicJobShips(image({ embedInZpl: false }))).toBe(true);
8181
expect(graphicJobShips({ ...image(), _gfaCache: undefined })).toBe(false);
8282
expect(graphicJobShips({ ...image(), _gfaCache: "^GFA,4,4,1," })).toBe(false);
83-
expect(graphicJobShips(image(null))).toBe(false);
83+
});
84+
85+
it("names an inline graphic only on the way off the job, and never opaque bytes", () => {
86+
const identity = { device: "R", name: "IMG_1" };
87+
expect(applyGraphicDelivery("job", image(null), undefined, identity)).toEqual({ patch: null, setupGraphics: undefined });
88+
expect(applyGraphicDelivery("printer", image(null), undefined, identity)).toEqual({ patch: { storedAs: { ...identity, embedInZpl: false } }, setupGraphics: undefined });
89+
const provided = applyGraphicDelivery("setup", image(null), undefined, identity);
90+
expect(provided).toMatchObject({ patch: { storedAs: { ...identity, embedInZpl: false }, _gfaCache: expect.stringContaining("^GFA") }, setupGraphics: [{ path: "R:IMG_1.GRF", gfa: expect.stringContaining("^GFA") }] });
91+
expect(applyGraphicDelivery("printer", { ...image(null), rawGf: "^GFA,4,4,1,00FFFF00" }, undefined, identity)).toBeUndefined();
92+
});
93+
94+
it("names why a way is shut, in the order the delivery would refuse it", () => {
95+
const identity = { device: "R", name: "IMG_1" };
96+
expect(graphicWayBlocks(image(null), identity, undefined, null)).toEqual({ job: undefined, setup: undefined });
97+
expect(graphicWayBlocks({ ...image(null), _gfaCache: undefined }, identity, undefined, null)).toEqual({ job: "noJobBytes", setup: "noUploadBytes" });
98+
expect(graphicWayBlocks({ ...image(), _gfaCache: undefined }, identity, undefined, null)).toEqual({ job: "noUploadBytes", setup: "noUploadBytes" });
99+
expect(graphicWayBlocks({ ...image(null), _gfaCache: undefined, rawGf: GFA }, identity, undefined, null)).toEqual({ job: undefined, setup: "opaque", printer: "opaque" });
100+
expect(graphicWayBlocks(image(null), identity, [{ path: "R:IMG_1.GRF", gfa: GFA }], null)).toEqual({ job: undefined });
101+
expect(graphicWayBlocks(image(null), identity, undefined, "unshippable")).toEqual({ job: undefined, setup: "tooWide" });
102+
expect(graphicWayBlocks(image(null), identity, undefined, "tooLarge")).toEqual({ job: undefined, setup: "tooLarge" });
103+
});
104+
105+
it("drafts a printer name no profile entry holds", () => {
106+
const names = ["IMG_0000", "IMG_1111"];
107+
const taken = names.map((n) => ({ path: `R:${n}.GRF`, gfa: GFA }));
108+
for (let i = 0; i < 50; i++) expect(names).not.toContain(draftGraphicIdentity(taken).name);
109+
});
110+
111+
it("ships an inline job from an upright cache or a raw ^GF and from nothing else", () => {
112+
expect(graphicJobShips(image(null))).toBe(true);
113+
expect(graphicJobShips({ ...image(null), rotation: "R" })).toBe(false);
114+
expect(graphicJobShips({ ...image(null), _gfaCache: undefined })).toBe(false);
115+
expect(graphicJobShips({ ...image(null), _gfaCache: undefined, rawGf: "^GFA,4,4,1,00FFFF00" })).toBe(true);
84116
});
85117

86118
it("refuses setup for bytes the script cannot carry, and does nothing without a printer name", () => {

‎packages/core/src/lib/resourceDelivery.ts‎

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { CustomFontMapping } from "../types/LabelConfig";
22
import type { PrinterProfile, SetupGraphic } from "../types/PrinterProfile";
3-
import { setupGraphicOf, storedGraphicShips, type ImageProps } from "../registry/image";
4-
import { uploadedGraphicPath } from "./storagePath";
3+
import { canSendSetupGraphic, inlineGraphicShips, setupGraphicOf, setupGraphicState, storedGraphicShips, type ImageProps } from "../registry/image";
4+
import { defaultStorageName, uploadedGraphicPath } from "./storagePath";
55
import { findSetupEntry, withSetupEntry, withoutSetupEntry } from "./setupEntries";
66

77
/** How a file reaches the printer: with every job, once through the setup script, or assumed to be there. */
@@ -22,10 +22,9 @@ export function fontDelivery(mapping: FontShip, path: string, setupFonts: readon
2222
return findSetupEntry(setupFonts, path) ? "setup" : "printer";
2323
}
2424

25-
/** Undefined for an image without a printer name, which prints as inline ^GF and has no way to choose. */
26-
export function graphicDelivery(p: ImageProps, setupGraphics: readonly SetupGraphic[] | undefined): ResourceDelivery | undefined {
27-
if (!p.storedAs) return undefined;
28-
if (p.storedAs.embedInZpl !== false) return "job";
25+
/** Job while the design ships the bytes, else setup or printer by the profile entry. */
26+
export function graphicDelivery(p: ImageProps, setupGraphics: readonly SetupGraphic[] | undefined): ResourceDelivery {
27+
if (!p.storedAs || p.storedAs.embedInZpl !== false) return "job";
2928
return findSetupEntry(setupGraphics, uploadedGraphicPath(p.storedAs)) ? "setup" : "printer";
3029
}
3130

@@ -45,7 +44,8 @@ export function applyFontDelivery(
4544

4645
/** Whether "job" would put bytes into the stream, so the choice never claims what the emit cannot send. */
4746
export function graphicJobShips(p: ImageProps): boolean {
48-
return p.storedAs !== undefined && storedGraphicShips({ ...p, storedAs: { ...p.storedAs, embedInZpl: true } });
47+
if (!p.storedAs) return inlineGraphicShips(p);
48+
return storedGraphicShips({ ...p, storedAs: { ...p.storedAs, embedInZpl: true } });
4949
}
5050

5151
/** A delivery that provides the file names it as the upload does, so a recall spelled with another
@@ -56,17 +56,53 @@ export function providedGraphic(p: ImageProps): ImageProps {
5656
return { ...p, storedAs };
5757
}
5858

59+
export type GraphicIdentity = Pick<NonNullable<ImageProps["storedAs"]>, "device" | "name">;
60+
61+
/** A printer name no profile entry holds yet, so sending the graphic cannot adopt another one's bytes. */
62+
export function draftGraphicIdentity(setupGraphics: readonly SetupGraphic[] | undefined): GraphicIdentity {
63+
let identity: GraphicIdentity;
64+
do identity = { device: "R", name: defaultStorageName() };
65+
while (findSetupEntry(setupGraphics, uploadedGraphicPath(identity)));
66+
return identity;
67+
}
68+
5969
type GraphicDeliveryPatch =
6070
| { patch: Partial<ImageProps> | null; setupGraphics: readonly SetupGraphic[] | undefined }
6171
| { refused: "tooLarge" | "unshippable" };
6272

63-
/** A profile entry the design already has stays as it is, so choosing "setup" encodes only when none exists. */
73+
export type GraphicWayBlock = "opaque" | "tooLarge" | "tooWide" | "noUploadBytes" | "noJobBytes";
74+
75+
/** Why a way cannot be chosen, so the select and its reasons never disagree with the delivery that follows. */
76+
export function graphicWayBlocks(
77+
p: ImageProps,
78+
identity: GraphicIdentity,
79+
setupGraphics: readonly SetupGraphic[] | undefined,
80+
refusal: "tooLarge" | "unshippable" | null,
81+
): Partial<Record<ResourceDelivery, GraphicWayBlock>> {
82+
const job = graphicJobShips(p) ? undefined : p.storedAs ? "noUploadBytes" : "noJobBytes";
83+
if (p.rawGf) return { job, setup: "opaque", printer: "opaque" };
84+
const identified = providedGraphic({ ...p, storedAs: p.storedAs ?? identity });
85+
// An existing entry needs no encode, so only a missing one lets the bytes decide.
86+
if (findSetupEntry(setupGraphics, uploadedGraphicPath(identified.storedAs ?? identity))) return { job };
87+
const setup = setupGraphicState(identified, setupGraphics) === "tooLarge" || refusal === "tooLarge"
88+
? "tooLarge"
89+
: refusal === "unshippable"
90+
? "tooWide"
91+
: canSendSetupGraphic(identified)
92+
? undefined
93+
: "noUploadBytes";
94+
return { job, setup };
95+
}
96+
97+
/** An existing profile entry stays, and an inline graphic takes `identity` only on the way off the job. */
6498
export function applyGraphicDelivery(
6599
next: ResourceDelivery,
66100
p: ImageProps,
67101
setupGraphics: readonly SetupGraphic[] | undefined,
102+
identity?: GraphicIdentity,
68103
): GraphicDeliveryPatch | undefined {
69-
const storedAs = p.storedAs;
104+
// Opaque bytes emit before any recall, so a printer name could never replace them.
105+
const storedAs: ImageProps["storedAs"] = p.storedAs ?? (p.rawGf ? undefined : identity);
70106
if (!storedAs) return undefined;
71107
const path = uploadedGraphicPath(storedAs);
72108
const ships = storedAs.embedInZpl !== false;

‎packages/core/src/registry/image.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,12 @@ export function storedGraphicShips(p: ImageProps): boolean {
273273
return !!getImage(p.imageId) || (!!p._gfaCache && gfShipsSafely(p._gfaCache));
274274
}
275275

276+
/** Whether an inline ^GF ships bytes, the counterpart of storedGraphicShips. */
277+
export function inlineGraphicShips(p: ImageProps): boolean {
278+
if (p.rawGf) return gfShipsSafely(p.rawGf);
279+
return !!getImage(p.imageId) || gfaCacheUsable(p);
280+
}
281+
276282
/** The path a recall names: the upload's when one ships, else the reference as written. */
277283
export function recallStoragePath(p: ImageProps): StoragePath | undefined {
278284
if (!p.storedAs) return undefined;

‎src/components/Properties/DeliverySelect.tsx‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,11 @@ interface Props {
1616
issue?: string;
1717
/** Jumps to the printer tab that provisions the file, offered once the file is delivered at setup. */
1818
onOpenSetup?: { label: string; open: () => void };
19+
disabled?: boolean;
1920
}
2021

2122
/** One control for every file the printer needs, so job, setup and printer cannot diverge across two widgets. */
22-
export function DeliverySelect({ value, onChange, resource, subject, blocked, issue, onOpenSetup }: Props) {
23+
export function DeliverySelect({ value, onChange, resource, subject, blocked, issue, onOpenSetup, disabled }: Props) {
2324
const t = useT();
2425
const hints: Record<ResourceDelivery, string> = resource === 'format'
2526
? { job: t.delivery.formatJobHint, setup: t.delivery.formatSetupHint, printer: t.delivery.formatPrinterHint }
@@ -34,6 +35,7 @@ export function DeliverySelect({ value, onChange, resource, subject, blocked, is
3435
if (next !== value) onChange(next);
3536
}}
3637
aria-label={`${t.delivery.label}: ${subject}`}
38+
disabled={disabled}
3739
groups={[{ options: RESOURCE_DELIVERIES.map((way) => ({
3840
value: way,
3941
label: t.delivery[way],

‎src/components/Properties/PropertiesPanel.tsx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,7 @@ export function PropertiesPanel({ canvasRef }: PropertiesPanelProps) {
262262
key={obj.id}
263263
obj={obj}
264264
onChange={(props: object) => updateObject(obj.id, { props })}
265+
locked={switchLocked}
265266
/>
266267
)}
267268

‎src/locales/ar.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1278,7 +1278,6 @@ const ar = {
12781278
widthDots: 'العرض',
12791279
threshold: 'عتبة أحادية',
12801280
storage: 'تخزين الطابعة',
1281-
storeOnPrinter: 'تفعيل',
12821281
storeOnPrinterHint: 'يشير إلى رسم مخزن في الطابعة عبر ^XG. يحدد التسليم ما إذا كانت البايتات تُرسل مع كل مهمة، أو مرة واحدة في نص الإعداد، أو لا تُرسل إطلاقًا.',
12831282
storeInline: 'تضمين مباشر',
12841283
openObjects: 'إدارة الكائنات المخزّنة',
@@ -1732,6 +1731,8 @@ const ar = {
17321731
printerGraphicHint: 'تكتفي المهام بالإشارة إلى الرسم. إن لم يكن موجودًا على الطابعة، لا يُطبع شيء مكانه.',
17331732
jobNeedsAlias: 'يحتاج اسمًا مستعارًا، ليتمكن ^CW من تسمية الملف.',
17341733
setupNeedsBytes: 'يحتاج بيانات الصورة لترميز الرفع.',
1734+
jobNeedsBytes: 'يحتاج بيانات الصورة لإرسالها مع مهمة الطباعة.',
1735+
opaqueBytes: 'تبقى البيانات كما استُورِدت ولا يمكن تخزينها على الطابعة.',
17351736
formatJobHint: 'كل مهمة طباعة تخزّن التنسيق من جديد وتطبعه بصفوف مجموعة بيانات.',
17361737
formatSetupHint: 'يخزّن نص إعداد ملف الطابعة هذا التنسيق مرة واحدة. تكتفي المهام بإرسال البيانات.',
17371738
formatPrinterHint: 'تكتفي المهام بإرسال البيانات. إذا كان التنسيق مفقودًا على الطابعة، لا يُطبع شيء.',

‎src/locales/bg.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1278,7 +1278,6 @@ const bg = {
12781278
widthDots: 'Ширина',
12791279
threshold: 'Моно праг',
12801280
storage: 'Памет на принтера',
1281-
storeOnPrinter: 'Активирай',
12821281
storeOnPrinterHint: 'Реферира графика, съхранена в принтера, чрез ^XG. Доставката определя дали байтовете се изпращат с всяко задание, веднъж в скрипта за настройка, или изобщо не се изпращат.',
12831282
storeInline: 'Вграждане',
12841283
openObjects: 'Управление на съхранените обекти',
@@ -1732,6 +1731,8 @@ const bg = {
17321731
printerGraphicHint: 'Заданията само препращат към графиката. Ако принтерът няма я, на нейно място не се отпечатва нищо.',
17331732
jobNeedsAlias: 'Изисква псевдоним, за да може ^CW да именува файла.',
17341733
setupNeedsBytes: 'Изисква данните на изображението, за да кодира качването.',
1734+
jobNeedsBytes: 'Изисква данните на изображението, за да бъдат изпратени със заданието за печат.',
1735+
opaqueBytes: 'Данните остават както са импортирани и не могат да се съхранят на принтера.',
17351736
formatJobHint: 'Всяко задание за печат съхранява формата отново и го отпечатва с редовете на набор от данни.',
17361737
formatSetupHint: 'Скриптът за настройка на този принтерен профил съхранява формата веднъж. Заданията изпращат само данните.',
17371738
formatPrinterHint: 'Заданията изпращат само данните. Ако форматът липсва на принтера, не се отпечатва нищо.',

‎src/locales/cs.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1278,7 +1278,6 @@ const cs = {
12781278
widthDots: 'Šířka',
12791279
threshold: 'Mono práh',
12801280
storage: 'Úložiště tiskárny',
1281-
storeOnPrinter: 'Aktivovat',
12821281
storeOnPrinterHint: 'Odkazuje na grafiku uloženou v tiskárně pomocí ^XG. Doručení určuje, zda se bajty odešlou s každou úlohou, jednou v instalačním skriptu, nebo vůbec.',
12831282
storeInline: 'Vložit přímo',
12841283
openObjects: 'Spravovat uložené objekty',
@@ -1732,6 +1731,8 @@ const cs = {
17321731
printerGraphicHint: 'Úlohy jen odkazují na grafiku. Pokud ji tiskárna nemá, na jejím místě se nic nevytiskne.',
17331732
jobNeedsAlias: 'Vyžaduje alias, aby ^CW mohl soubor pojmenovat.',
17341733
setupNeedsBytes: 'Vyžaduje obrazová data ke kódování nahrávání.',
1734+
jobNeedsBytes: 'Vyžaduje obrazová data pro odeslání s tiskovou úlohou.',
1735+
opaqueBytes: 'Data zůstávají tak, jak byla importována, a nelze je uložit do tiskárny.',
17351736
formatJobHint: 'Každá tisková úloha znovu uloží formát a vytiskne ho s řádky datové sady.',
17361737
formatSetupHint: 'Instalační skript tohoto profilu tiskárny uloží formát jednou. Úlohy odesílají jen data.',
17371738
formatPrinterHint: 'Úlohy odesílají jen data. Pokud na tiskárně chybí formát, nic se nevytiskne.',

‎src/locales/da.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1278,7 +1278,6 @@ const da = {
12781278
widthDots: 'Bredde',
12791279
threshold: 'Mono-tærskel',
12801280
storage: 'Printer-lager',
1281-
storeOnPrinter: 'Aktivér',
12821281
storeOnPrinterHint: 'Refererer en grafik gemt på printeren via ^XG. Leveringen afgør, om bytene sendes med hvert job, én gang i opsætningsscriptet, eller slet ikke.',
12831282
storeInline: 'Indlejr direkte',
12841283
openObjects: 'Administrer gemte objekter',
@@ -1732,6 +1731,8 @@ const da = {
17321731
printerGraphicHint: 'Job refererer kun til grafikken. Mangler printeren den, printes der ikke noget i stedet.',
17331732
jobNeedsAlias: 'Kræver et alias, så en ^CW kan navngive filen.',
17341733
setupNeedsBytes: 'Kræver billeddataene for at kode uploadet.',
1734+
jobNeedsBytes: 'Kræver billeddataene for at sende dem med printjobbet.',
1735+
opaqueBytes: 'Dataene forbliver som importeret og kan ikke lagres på printeren.',
17351736
formatJobHint: 'Hvert printjob gemmer formatet igen og udskriver det med rækkerne fra et datasæt.',
17361737
formatSetupHint: 'Opsætningsscriptet til denne printerprofil gemmer formatet én gang. Job sender kun dataene.',
17371738
formatPrinterHint: 'Job sender kun dataene. Mangler formatet på printeren, udskrives der ikke noget.',

‎src/locales/de.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1315,7 +1315,6 @@ const de = {
13151315
widthDots: 'Breite',
13161316
threshold: 'Mono-Schwellenwert',
13171317
storage: 'Drucker-Speicher',
1318-
storeOnPrinter: 'Aktivieren',
13191318
storeOnPrinterHint: 'Referenziert eine Grafik auf der Drucker-Storage via ^XG. Die Bereitstellung entscheidet, ob die Bytes mit jedem Job, einmalig im Setup-Skript oder gar nicht mitgesendet werden.',
13201319
storeInline: 'Inline einbetten',
13211320
openObjects: 'Gespeicherte Objekte verwalten',
@@ -1769,6 +1768,8 @@ const de = {
17691768
printerGraphicHint: 'Jobs referenzieren nur die Grafik. Fehlt sie auf dem Drucker, wird an ihrer Stelle nichts gedruckt.',
17701769
jobNeedsAlias: 'Braucht einen Alias, damit ein ^CW die Datei benennen kann.',
17711770
setupNeedsBytes: 'Braucht die Bilddaten, um den Upload zu kodieren.',
1771+
jobNeedsBytes: 'Braucht Bilddaten, die mit dem Auftrag gesendet werden.',
1772+
opaqueBytes: 'Die Bytes bleiben wie importiert und lassen sich nicht auf dem Drucker ablegen.',
17721773
formatJobHint: 'Jeder Druckjob speichert das Format erneut und druckt es mit den Zeilen eines Datensatzes.',
17731774
formatSetupHint: 'Das Setup-Skript dieses Druckerprofils speichert das Format einmalig. Jobs senden nur die Daten.',
17741775
formatPrinterHint: 'Jobs senden nur die Daten. Fehlt das Format auf dem Drucker, wird nichts gedruckt.',

0 commit comments

Comments
 (0)