fix(logos): SVG-Wurzelelement richtig finden, Quell-Canvas und Pixelpuffer früh freigeben, Konstanten umbenannt
- prepare-logo.ts, sizeSvg: Das Wurzelelement wird nicht mehr mit der ersten Fundstelle von „<svg“ gesucht. rootStart überspringt Byte-Order-Marke, Leerraum, XML-Deklaration und Anweisungen (<?…?>), Kommentare und den DOCTYPE (auch mit eigener DTD in […] samt Zeichenketten, Kommentaren und Anweisungen darin) und verlangt dahinter `<svg` mit Leerraum, „/“ oder „>“. Ein auskommentiertes <svg …> oder eines im DOCTYPE wurde sonst umgeschrieben, ein `<svg:svg …>` (Präfix) oder `<svg-x` zerstörte das Tag. Beides bleibt jetzt unverändert (quadratisch), wie ein Text ohne Wurzelelement. Ein Durchlauf ohne Zurücksetzen, auch ein feindlicher Prolog braucht nur linear viel Zeit. - prepare-logo.ts, prepareLogo: renderLogo zeichnet das Quellbild auf den Quell-Canvas, liest den Pixelpuffer einmal als Argument von planLogo (keine Referenz danach) und setzt den Quell-Canvas im finally auf 0 × 0, noch vor dem PNG und auch bei Fehlern. Das Ergebnis ist gleich. - prepare-logo.ts: LOGO_MAX_W/LOGO_MAX_H (720/250, die Dateigrenze) heißen LOGO_FILE_MAX_W/LOGO_FILE_MAX_H und kollidieren nicht mehr im Namen mit LOGO_MAX_H aus card/geometry.ts (125, Höhe auf der Karte). Einziger Verwender ist der Test. - Tests: Prolog-Fälle (Kommentar, Anweisung, DOCTYPE mit DTD, BOM, mehrere, Präfix, Bindestrich, nicht endender Prolog, feindliche Eingaben mit Zeitgrenze); Quell-Canvas ist frei (auch vor toBlob, bei leer, zu klein, Zeichenfehler, fehlendem Ergebnis-Canvas, fehlendem PNG), der Pixelpuffer wird einmal gelesen. Die vorhandenen Größenprüfungen lesen die Größe beim Zeichnen (`used`), weil der Quell-Canvas am Ende 0 × 0 ist. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
76c3c3aea2
commit
29c9b2efaf
2 files changed
+303
-49
No files matched your search
@@ -3,8 +3,8 @@ import { setLang } from '../i18n/i18n';
|
|||||||
import {
|
import {
|
||||||
ALPHA_EMPTY,
|
ALPHA_EMPTY,
|
||||||
LOGO_ACCEPT,
|
LOGO_ACCEPT,
|
||||||
LOGO_MAX_H,
|
LOGO_FILE_MAX_H,
|
||||||
LOGO_MAX_W,
|
LOGO_FILE_MAX_W,
|
||||||
LOGO_MIN,
|
LOGO_MIN,
|
||||||
LOGO_SOURCE_MAX_BYTES,
|
LOGO_SOURCE_MAX_BYTES,
|
||||||
LOGO_SOURCE_MAX_PIXELS,
|
LOGO_SOURCE_MAX_PIXELS,
|
||||||
@@ -48,7 +48,7 @@ describe('LOGO_ACCEPT', () => {
|
|||||||
|
|
||||||
describe('Konstanten', () => {
|
describe('Konstanten', () => {
|
||||||
it('die Werte der Spec', () => {
|
it('die Werte der Spec', () => {
|
||||||
expect([LOGO_MAX_W, LOGO_MAX_H, LOGO_MIN, ALPHA_EMPTY, SVG_RASTER_MAX]).toEqual([720, 250, 8, 8, 2048]);
|
expect([LOGO_FILE_MAX_W, LOGO_FILE_MAX_H, LOGO_MIN, ALPHA_EMPTY, SVG_RASTER_MAX]).toEqual([720, 250, 8, 8, 2048]);
|
||||||
expect([LOGO_SOURCE_MAX_BYTES, LOGO_SOURCE_MAX_PIXELS]).toEqual([5_242_880, 40_000_000]);
|
expect([LOGO_SOURCE_MAX_BYTES, LOGO_SOURCE_MAX_PIXELS]).toEqual([5_242_880, 40_000_000]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -407,6 +407,116 @@ describe('sizeSvg', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('das echte Wurzelelement: Prolog (Deklaration, Anweisungen, Kommentare, DOCTYPE) wird übersprungen, ein „<svg“ darin zählt nicht', () => {
|
||||||
|
const ROOT_BEFORE = '<svg width="200" height="60">';
|
||||||
|
const ROOT_AFTER = '<svg width="2048" height="614" viewBox="0 0 200 60">';
|
||||||
|
const converted = (head: string) => ({ svg: `${head}${ROOT_AFTER}${BODY}`, w: 2048, h: 614 });
|
||||||
|
|
||||||
|
it('ein auskommentiertes <svg …> vor dem Wurzelelement wird nicht umgeschrieben', () => {
|
||||||
|
const head = '<!-- Vorlage: <svg width="9" height="9"> bitte nicht ändern -->';
|
||||||
|
expect(sizeSvg(`${head}${ROOT_BEFORE}${BODY}`)).toEqual(converted(head));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ein <svg in einer Verarbeitungsanweisung zählt nicht', () => {
|
||||||
|
const head = '<?xml version="1.0"?><?editor <svg width="9" height="9"> ?>';
|
||||||
|
expect(sizeSvg(`${head}${ROOT_BEFORE}${BODY}`)).toEqual(converted(head));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ein <svg im DOCTYPE (in einer Entität der eigenen DTD, samt „>“ und „]“ in Anführungszeichen) zählt nicht', () => {
|
||||||
|
const head =
|
||||||
|
'<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [\n' +
|
||||||
|
' <!ENTITY ns_svg "http://www.w3.org/2000/svg">\n' +
|
||||||
|
' <!ENTITY stuck "<svg width=\'9\' height=\'9\'>]>">\n' +
|
||||||
|
' <!-- ein Kommentar mit ]> und <svg width="9" height="9"> -->\n' +
|
||||||
|
' <?pi ]> <svg width="9" height="9"> ?>\n' +
|
||||||
|
']>\n';
|
||||||
|
expect(sizeSvg(`${head}${ROOT_BEFORE}${BODY}`)).toEqual(converted(head));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ein typischer Kopf aus Adobe Illustrator (Deklaration, Kommentar, DOCTYPE mit Entitäten, Namensräume über &ns_svg;) wird übersprungen', () => {
|
||||||
|
const head =
|
||||||
|
'<?xml version="1.0" encoding="utf-8"?>\n' +
|
||||||
|
'<!-- Generator: Adobe Illustrator 24.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->\n' +
|
||||||
|
'<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [\n' +
|
||||||
|
'\t<!ENTITY ns_extend "http://ns.adobe.com/Extensibility/1.0/">\n' +
|
||||||
|
'\t<!ENTITY ns_ai "http://ns.adobe.com/AdobeIllustrator/10.0/">\n' +
|
||||||
|
'\t<!ENTITY ns_custom "http://ns.adobe.com/GenericCustomNamespace/1.0/">\n' +
|
||||||
|
']>\n';
|
||||||
|
const root = '<svg version="1.1" id="Layer_1" xmlns="&ns_svg;" x="0px" y="0px" width="200px" height="60px" viewBox="0 0 200 60">';
|
||||||
|
const result = sizeSvg(`${head}${root}${BODY}`);
|
||||||
|
expect(result.w).toBe(2048);
|
||||||
|
expect(result.h).toBe(614);
|
||||||
|
expect(result.svg).toBe(
|
||||||
|
`${head}<svg width="2048" height="614" version="1.1" id="Layer_1" xmlns="&ns_svg;" x="0px" y="0px" viewBox="0 0 200 60">${BODY}`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Byte-Order-Marke und Leerraum vor dem Prolog stören nicht', () => {
|
||||||
|
const head = '\uFEFF \n\t<?xml version="1.0"?>\n\n<!-- x -->\r\n';
|
||||||
|
expect(sizeSvg(`${head}${ROOT_BEFORE}${BODY}`)).toEqual(converted(head));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('mehrere Kommentare und Anweisungen hintereinander', () => {
|
||||||
|
const head = '<!--a--><?b?><!--c--> <?d e?><!---->';
|
||||||
|
expect(sizeSvg(`${head}${ROOT_BEFORE}${BODY}`)).toEqual(converted(head));
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
['mit Namensraum-Präfix', '<svg:svg xmlns:svg="http://www.w3.org/2000/svg" width="200" height="60">'],
|
||||||
|
['mit Bindestrich', '<svg-icon width="200" height="60">'],
|
||||||
|
['mit Punkt', '<svg.x width="200" height="60">'],
|
||||||
|
['mit Ziffer', '<svg2 width="200" height="60">'],
|
||||||
|
])('ein Wurzelelement, das nur mit „svg“ beginnt (%s), ist keins: der Text bleibt unverändert, quadratisch', (_name, root) => {
|
||||||
|
expect(sizeSvg(`${root}${BODY}`)).toEqual({ svg: `${root}${BODY}`, w: 2048, h: 2048 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ein „<svg:svg“ im Inneren oder in einem Kommentar macht daraus ebenfalls kein Wurzelelement', () => {
|
||||||
|
const text = `<!-- <svg width="9" height="9"> --><svg:svg width="200" height="60">${BODY}`;
|
||||||
|
expect(sizeSvg(text)).toEqual({ svg: text, w: 2048, h: 2048 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('vor dem Wurzelelement steht etwas anderes (Text, ein anderes Element): es ist kein SVG, der Text bleibt unverändert', () => {
|
||||||
|
for (const text of [`Hallo ${ROOT_BEFORE}${BODY}`, `<html><body>${ROOT_BEFORE}${BODY}</body></html>`, `<g/>${ROOT_BEFORE}${BODY}`]) {
|
||||||
|
expect(sizeSvg(text)).toEqual({ svg: text, w: 2048, h: 2048 });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('<svg ohne Attribute, mit Zeilenumbruch oder mit „/“ direkt dahinter ist ein Wurzelelement', () => {
|
||||||
|
expect(sizeSvg('<svg>' + BODY)).toEqual({ svg: '<svg width="2048" height="2048">' + BODY, w: 2048, h: 2048 });
|
||||||
|
expect(sizeSvg('<svg\nwidth="200"\nheight="60">' + BODY).svg).toBe(ROOT_AFTER + BODY);
|
||||||
|
expect(sizeSvg('<svg/>').svg).toBe('<svg width="2048" height="2048"/>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ein Prolog, der nicht endet (Kommentar, Anweisung, DOCTYPE, Anführungszeichen), ergibt kein Wurzelelement: unverändert, quadratisch', () => {
|
||||||
|
for (const text of [
|
||||||
|
`<!-- offen ${ROOT_BEFORE}${BODY}`,
|
||||||
|
`<?xml version="1.0" ${ROOT_BEFORE}${BODY}`,
|
||||||
|
`<!DOCTYPE svg [ ${ROOT_BEFORE}${BODY}`,
|
||||||
|
`<!DOCTYPE svg PUBLIC "offen ${ROOT_BEFORE}${BODY}`,
|
||||||
|
'<!DOCTYPE svg',
|
||||||
|
]) {
|
||||||
|
expect(sizeSvg(text)).toEqual({ svg: text, w: 2048, h: 2048 });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ein feindlicher Prolog braucht nur linear viel Zeit (kein Hängen im Tab)', () => {
|
||||||
|
const hostile = [
|
||||||
|
'<!--'.repeat(100_000),
|
||||||
|
'<?'.repeat(100_000),
|
||||||
|
`<!DOCTYPE x [${'<!-- '.repeat(50_000)}`,
|
||||||
|
`<!DOCTYPE x ${'" '.repeat(100_000)}`,
|
||||||
|
`<!DOCTYPE x [${"'".repeat(100_000)}`,
|
||||||
|
`<!DOCTYPE x [${'<?a '.repeat(50_000)}`,
|
||||||
|
`${' '.repeat(200_000)}<!--`,
|
||||||
|
'<!---->'.repeat(30_000) + `${ROOT_BEFORE}${BODY}`,
|
||||||
|
];
|
||||||
|
const start = performance.now();
|
||||||
|
for (const text of hostile) sizeSvg(text);
|
||||||
|
expect(performance.now() - start).toBeLessThan(500);
|
||||||
|
expect(sizeSvg(hostile.at(-1)!).svg.endsWith(`${ROOT_AFTER}${BODY}`)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('ein selbstschließendes Wurzelelement bleibt gültig', () => {
|
it('ein selbstschließendes Wurzelelement bleibt gültig', () => {
|
||||||
expect(sizeSvg(`<svg ${NS}/>`).svg).toBe(`<svg width="2048" height="2048" ${NS}/>`);
|
expect(sizeSvg(`<svg ${NS}/>`).svg).toBe(`<svg width="2048" height="2048" ${NS}/>`);
|
||||||
});
|
});
|
||||||
@@ -449,8 +559,14 @@ describe('prepareLogo (Bild und Canvas gestellt)', () => {
|
|||||||
/** Argumente aller `drawImage`-Aufrufe. */
|
/** Argumente aller `drawImage`-Aufrufe. */
|
||||||
draws: unknown[][];
|
draws: unknown[][];
|
||||||
smoothing: string;
|
smoothing: string;
|
||||||
|
/** Die Größe, als zum ersten Mal darauf gezeichnet oder von ihm gelesen wurde (danach kann der Canvas freigegeben, also 0 × 0, sein). */
|
||||||
|
used: [number, number] | null;
|
||||||
|
/** Wie oft seine Pixel gelesen wurden (`getImageData`). */
|
||||||
|
reads: number;
|
||||||
}
|
}
|
||||||
let canvases: FakeCanvas[];
|
let canvases: FakeCanvas[];
|
||||||
|
/** Die Größen aller Canvas im Moment, in dem das PNG erzeugt wird (`toBlob`). */
|
||||||
|
let sizesAtBlob: Array<[number, number]> | null;
|
||||||
let blobs: Blob[];
|
let blobs: Blob[];
|
||||||
let revoked: string[];
|
let revoked: string[];
|
||||||
let images: FakeImage[];
|
let images: FakeImage[];
|
||||||
@@ -502,6 +618,8 @@ describe('prepareLogo (Bild und Canvas gestellt)', () => {
|
|||||||
resultHole?: boolean;
|
resultHole?: boolean;
|
||||||
noBlob?: boolean;
|
noBlob?: boolean;
|
||||||
noContext?: boolean;
|
noContext?: boolean;
|
||||||
|
/** Nur der Ergebnis-Canvas (der zweite) hat keinen 2D-Kontext. */
|
||||||
|
resultNoContext?: boolean;
|
||||||
/** `drawImage` wirft diesen Fehler. */
|
/** `drawImage` wirft diesen Fehler. */
|
||||||
drawError?: unknown;
|
drawError?: unknown;
|
||||||
} = {},
|
} = {},
|
||||||
@@ -510,16 +628,18 @@ describe('prepareLogo (Bild und Canvas gestellt)', () => {
|
|||||||
vi.spyOn(document, 'createElement').mockImplementation(((tag: string) => {
|
vi.spyOn(document, 'createElement').mockImplementation(((tag: string) => {
|
||||||
if (tag !== 'canvas') return create(tag);
|
if (tag !== 'canvas') return create(tag);
|
||||||
const isSource = canvases.length === 0;
|
const isSource = canvases.length === 0;
|
||||||
const canvas: FakeCanvas = { width: 0, height: 0, draws: [], smoothing: '' };
|
const canvas: FakeCanvas = { width: 0, height: 0, draws: [], smoothing: '', used: null, reads: 0 };
|
||||||
const ctx = {
|
const ctx = {
|
||||||
set imageSmoothingQuality(value: string) {
|
set imageSmoothingQuality(value: string) {
|
||||||
canvas.smoothing = value;
|
canvas.smoothing = value;
|
||||||
},
|
},
|
||||||
drawImage: (...args: unknown[]) => {
|
drawImage: (...args: unknown[]) => {
|
||||||
|
canvas.used ??= [canvas.width, canvas.height];
|
||||||
if (opts.drawError) throw opts.drawError;
|
if (opts.drawError) throw opts.drawError;
|
||||||
canvas.draws.push(args);
|
canvas.draws.push(args);
|
||||||
},
|
},
|
||||||
getImageData: (_x: number, _y: number, w: number, h: number) => ({
|
getImageData: (_x: number, _y: number, w: number, h: number) => ({
|
||||||
|
...((canvas.used ??= [canvas.width, canvas.height]), (canvas.reads += 1), {}),
|
||||||
data: isSource
|
data: isSource
|
||||||
? opts.blank
|
? opts.blank
|
||||||
? new Uint8ClampedArray(w * h * 4)
|
? new Uint8ClampedArray(w * h * 4)
|
||||||
@@ -529,8 +649,11 @@ describe('prepareLogo (Bild und Canvas gestellt)', () => {
|
|||||||
};
|
};
|
||||||
canvases.push(canvas);
|
canvases.push(canvas);
|
||||||
return Object.assign(canvas, {
|
return Object.assign(canvas, {
|
||||||
getContext: () => (opts.noContext ? null : ctx),
|
getContext: () => (opts.noContext || (opts.resultNoContext && !isSource) ? null : ctx),
|
||||||
toBlob: (done: BlobCallback, type?: string) => done(opts.noBlob ? null : new Blob(['png'], { type })),
|
toBlob: (done: BlobCallback, type?: string) => {
|
||||||
|
sizesAtBlob = canvases.map((c) => [c.width, c.height]);
|
||||||
|
done(opts.noBlob ? null : new Blob(['png'], { type }));
|
||||||
|
},
|
||||||
}) as unknown as HTMLElement;
|
}) as unknown as HTMLElement;
|
||||||
}) as typeof document.createElement);
|
}) as typeof document.createElement);
|
||||||
}
|
}
|
||||||
@@ -540,6 +663,7 @@ describe('prepareLogo (Bild und Canvas gestellt)', () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
canvases = [];
|
canvases = [];
|
||||||
|
sizesAtBlob = null;
|
||||||
blobs = [];
|
blobs = [];
|
||||||
revoked = [];
|
revoked = [];
|
||||||
images = [];
|
images = [];
|
||||||
@@ -572,7 +696,7 @@ describe('prepareLogo (Bild und Canvas gestellt)', () => {
|
|||||||
expect(decodeCalls).toBe(1);
|
expect(decodeCalls).toBe(1);
|
||||||
expect(revoked).toEqual(['blob:test-1']);
|
expect(revoked).toEqual(['blob:test-1']);
|
||||||
expect(canvases).toHaveLength(2);
|
expect(canvases).toHaveLength(2);
|
||||||
expect(canvases[0]).toMatchObject({ width: 100, height: 40 });
|
expect(canvases[0].used).toEqual([100, 40]);
|
||||||
expect(canvases[0].draws).toEqual([[images[0], 0, 0, 100, 40]]);
|
expect(canvases[0].draws).toEqual([[images[0], 0, 0, 100, 40]]);
|
||||||
// Ergebnis: genau der sichtbare Ausschnitt, 1:1 (nie vergrößert).
|
// Ergebnis: genau der sichtbare Ausschnitt, 1:1 (nie vergrößert).
|
||||||
expect(canvases[1]).toMatchObject({ width: 20, height: 10, smoothing: 'high' });
|
expect(canvases[1]).toMatchObject({ width: 20, height: 10, smoothing: 'high' });
|
||||||
@@ -620,7 +744,7 @@ describe('prepareLogo (Bild und Canvas gestellt)', () => {
|
|||||||
stubCanvas({ blank: true });
|
stubCanvas({ blank: true });
|
||||||
await expect(prepareLogo(rasterFile())).rejects.toThrow(EMPTY);
|
await expect(prepareLogo(rasterFile())).rejects.toThrow(EMPTY);
|
||||||
expect(decodeCalls).toBe(1);
|
expect(decodeCalls).toBe(1);
|
||||||
expect(canvases[0]).toMatchObject({ width: 8000, height: 5000 });
|
expect(canvases[0].used).toEqual([8000, 5000]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -681,7 +805,7 @@ describe('prepareLogo (Bild und Canvas gestellt)', () => {
|
|||||||
// Das SVG kommt nie in die Seite, Skripte laufen darum nie.
|
// Das SVG kommt nie in die Seite, Skripte laufen darum nie.
|
||||||
expect(document.querySelector('svg, script, img')).toBeNull();
|
expect(document.querySelector('svg, script, img')).toBeNull();
|
||||||
|
|
||||||
expect(canvases[0]).toMatchObject({ width: 2048, height: 683 });
|
expect(canvases[0].used).toEqual([2048, 683]);
|
||||||
expect(canvases[0].draws).toEqual([[images[0], 0, 0, 2048, 683]]);
|
expect(canvases[0].draws).toEqual([[images[0], 0, 0, 2048, 683]]);
|
||||||
// Zuschnitt auf das Sichtbare (2048 × 483), dann auf die Zielgröße verkleinert.
|
// Zuschnitt auf das Sichtbare (2048 × 483), dann auf die Zielgröße verkleinert.
|
||||||
expect(canvases[1]).toMatchObject({ width: 720, height: 170 });
|
expect(canvases[1]).toMatchObject({ width: 720, height: 170 });
|
||||||
@@ -701,7 +825,7 @@ describe('prepareLogo (Bild und Canvas gestellt)', () => {
|
|||||||
expect(await blobs[0].text()).toBe(
|
expect(await blobs[0].text()).toBe(
|
||||||
'<svg width="2048" height="614" viewBox="0 0 200 60" xmlns="http://www.w3.org/2000/svg"><rect width="200" height="60"/></svg>',
|
'<svg width="2048" height="614" viewBox="0 0 200 60" xmlns="http://www.w3.org/2000/svg"><rect width="200" height="60"/></svg>',
|
||||||
);
|
);
|
||||||
expect(canvases[0]).toMatchObject({ width: 2048, height: 614 });
|
expect(canvases[0].used).toEqual([2048, 614]);
|
||||||
expect(canvases[0].draws).toEqual([[images[0], 0, 0, 2048, 614]]);
|
expect(canvases[0].draws).toEqual([[images[0], 0, 0, 2048, 614]]);
|
||||||
expect(prepared).toMatchObject({ width: 720, height: 216 });
|
expect(prepared).toMatchObject({ width: 720, height: 216 });
|
||||||
});
|
});
|
||||||
@@ -715,7 +839,7 @@ describe('prepareLogo (Bild und Canvas gestellt)', () => {
|
|||||||
expect(await blobs[0].text()).toBe(
|
expect(await blobs[0].text()).toBe(
|
||||||
'<svg width="2048" height="2048" xmlns="http://www.w3.org/2000/svg"><rect width="200" height="100"/></svg>',
|
'<svg width="2048" height="2048" xmlns="http://www.w3.org/2000/svg"><rect width="200" height="100"/></svg>',
|
||||||
);
|
);
|
||||||
expect(canvases[0]).toMatchObject({ width: 2048, height: 2048 });
|
expect(canvases[0].used).toEqual([2048, 2048]);
|
||||||
expect(canvases[0].draws).toEqual([[images[0], 0, 0, 2048, 2048]]);
|
expect(canvases[0].draws).toEqual([[images[0], 0, 0, 2048, 2048]]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -739,6 +863,71 @@ describe('prepareLogo (Bild und Canvas gestellt)', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Speicher: der Quell-Canvas wird frühzeitig freigegeben (0 × 0), der Pixelpuffer nur einmal gelesen', () => {
|
||||||
|
const released = (canvas: FakeCanvas) => [canvas.width, canvas.height];
|
||||||
|
|
||||||
|
it('Rasterbild: nach dem Zuschnitt ist der Quell-Canvas frei, schon bevor das PNG entsteht; das Ergebnis behält seine Größe', async () => {
|
||||||
|
stubCanvas({ visible: only(10, 5, 20, 10) });
|
||||||
|
await prepareLogo(rasterFile());
|
||||||
|
expect(canvases[0].used).toEqual([100, 40]);
|
||||||
|
expect(released(canvases[0])).toEqual([0, 0]);
|
||||||
|
expect(sizesAtBlob).toEqual([
|
||||||
|
[0, 0],
|
||||||
|
[20, 10],
|
||||||
|
]);
|
||||||
|
expect(canvases[1]).toMatchObject({ width: 20, height: 10 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('SVG: ebenso', async () => {
|
||||||
|
natural = { w: 2048, h: 614 };
|
||||||
|
stubCanvas({ visible: only(0, 0, 2048, 614) });
|
||||||
|
await prepareLogo(svgFile('<svg xmlns="http://www.w3.org/2000/svg" width="200" height="60"/>'));
|
||||||
|
expect(canvases[0].used).toEqual([2048, 614]);
|
||||||
|
expect(sizesAtBlob?.[0]).toEqual([0, 0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('der Pixelpuffer des Quellbildes wird genau einmal gelesen (für den Zuschnitt), nicht aufgehoben', async () => {
|
||||||
|
stubCanvas({ visible: only(10, 5, 20, 10) });
|
||||||
|
await prepareLogo(rasterFile());
|
||||||
|
expect(canvases[0].reads).toBe(1);
|
||||||
|
expect(canvases[1].reads).toBe(1); // das Ergebnis: nur für „hat Transparenz“
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
['leer', { visible: () => 0 }, EMPTY],
|
||||||
|
['zu klein', { visible: only(10, 5, 7, 7) }, SMALL],
|
||||||
|
])('auch wenn das Logo %s ist: der Quell-Canvas ist freigegeben', async (_name, opts, message) => {
|
||||||
|
stubCanvas(opts);
|
||||||
|
await expect(prepareLogo(rasterFile())).rejects.toThrow(message);
|
||||||
|
expect(canvases).toHaveLength(1); // ein Ergebnis-Canvas wird gar nicht erst angelegt
|
||||||
|
expect(released(canvases[0])).toEqual([0, 0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('auch wenn das Zeichnen des Quellbildes scheitert', async () => {
|
||||||
|
stubCanvas({ drawError: new DOMException('kaputt', 'InvalidStateError') });
|
||||||
|
await expect(prepareLogo(rasterFile())).rejects.toThrow(READ);
|
||||||
|
expect(released(canvases[0])).toEqual([0, 0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('auch wenn der Ergebnis-Canvas keinen 2D-Kontext hat', async () => {
|
||||||
|
stubCanvas({ visible: only(10, 5, 20, 10), resultNoContext: true });
|
||||||
|
await expect(prepareLogo(rasterFile())).rejects.toThrow(READ);
|
||||||
|
expect(canvases).toHaveLength(2);
|
||||||
|
expect(released(canvases[0])).toEqual([0, 0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('auch wenn das PNG nicht entsteht', async () => {
|
||||||
|
stubCanvas({ visible: only(10, 5, 20, 10), noBlob: true });
|
||||||
|
await expect(prepareLogo(rasterFile())).rejects.toThrow(READ);
|
||||||
|
expect(released(canvases[0])).toEqual([0, 0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ohne 2D-Canvas für das Quellbild bleibt es bei der Meldung (es gibt nichts freizugeben)', async () => {
|
||||||
|
stubCanvas({ noContext: true });
|
||||||
|
await expect(prepareLogo(rasterFile())).rejects.toThrow(READ);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('unerwartete Fehler: eine Warnung mit dem Originalfehler in der Konsole, der Nutzer liest weiter „nicht gelesen“', () => {
|
describe('unerwartete Fehler: eine Warnung mit dem Originalfehler in der Konsole, der Nutzer liest weiter „nicht gelesen“', () => {
|
||||||
it('der Browser wirft beim Zeichnen: genau eine Warnung mit genau diesem Fehler, die Meldung bleibt unverändert', async () => {
|
it('der Browser wirft beim Zeichnen: genau eine Warnung mit genau diesem Fehler, die Meldung bleibt unverändert', async () => {
|
||||||
const boom = new DOMException('The source image could not be decoded.', 'InvalidStateError');
|
const boom = new DOMException('The source image could not be decoded.', 'InvalidStateError');
|
||||||
|
|||||||
+103
-38
@@ -2,6 +2,7 @@
|
|||||||
// Ränder abschneiden, auf höchstens 720 × 250 px verkleinern (nie vergrößern) und als PNG ausgeben. Die Maße sind die des Dienstes
|
// Ränder abschneiden, auf höchstens 720 × 250 px verkleinern (nie vergrößern) und als PNG ausgeben. Die Maße sind die des Dienstes
|
||||||
// (deploy/api/logos.js), der trotzdem selbst prüft. Zuschnitt, Skalierung, Transparenz und die Größe eines SVGs sind reine Helfer
|
// (deploy/api/logos.js), der trotzdem selbst prüft. Zuschnitt, Skalierung, Transparenz und die Größe eines SVGs sind reine Helfer
|
||||||
// (einzeln testbar); `prepareLogo` fügt sie mit dem Canvas des Browsers zusammen. Kein Netzwerk, kein Appwrite.
|
// (einzeln testbar); `prepareLogo` fügt sie mit dem Canvas des Browsers zusammen. Kein Netzwerk, kein Appwrite.
|
||||||
|
// Das Quellbild belegt bis zu 40 Megapixel: sein Canvas und der Pixelpuffer sind nach dem Zuschnitt sofort wieder frei.
|
||||||
import { viewBoxAspect } from '../card/marks';
|
import { viewBoxAspect } from '../card/marks';
|
||||||
import type { CardImage } from '../card/render';
|
import type { CardImage } from '../card/render';
|
||||||
import { MAX_IMAGE_PIXELS, PIXELS_MESSAGE, decodeImage } from '../editor/upload';
|
import { MAX_IMAGE_PIXELS, PIXELS_MESSAGE, decodeImage } from '../editor/upload';
|
||||||
@@ -10,9 +11,12 @@ import { msg, t } from '../i18n/i18n';
|
|||||||
/** Für das `accept`-Attribut des Datei-Felds: die erlaubten Typen, dazu `.svg` für Systeme, die den Typ nicht kennen. */
|
/** Für das `accept`-Attribut des Datei-Felds: die erlaubten Typen, dazu `.svg` für Systeme, die den Typ nicht kennen. */
|
||||||
export const LOGO_ACCEPT = 'image/png,image/jpeg,image/webp,image/svg+xml,.svg';
|
export const LOGO_ACCEPT = 'image/png,image/jpeg,image/webp,image/svg+xml,.svg';
|
||||||
|
|
||||||
/** Höchstmaße und Mindestmaß des fertigen Logos in Pixeln (wie der Dienst sie prüft). */
|
/**
|
||||||
export const LOGO_MAX_W = 720;
|
* Höchstmaße und Mindestmaß der fertigen Logo-Datei in Pixeln (wie der Dienst sie prüft). Nicht zu verwechseln mit `LOGO_MAX_H`
|
||||||
export const LOGO_MAX_H = 250;
|
* in card/geometry.ts: das ist die Höhe, bis zu der das Logo auf der Karte wächst.
|
||||||
|
*/
|
||||||
|
export const LOGO_FILE_MAX_W = 720;
|
||||||
|
export const LOGO_FILE_MAX_H = 250;
|
||||||
export const LOGO_MIN = 8;
|
export const LOGO_MIN = 8;
|
||||||
/** Pixel mit Alpha bis hierher gelten als leer (nicht sichtbar). */
|
/** Pixel mit Alpha bis hierher gelten als leer (nicht sichtbar). */
|
||||||
export const ALPHA_EMPTY = 8;
|
export const ALPHA_EMPTY = 8;
|
||||||
@@ -123,7 +127,7 @@ export function hasTransparency(data: Uint8ClampedArray): boolean {
|
|||||||
export function planLogo(data: Uint8ClampedArray, w: number, h: number): { box: Box; size: Size } {
|
export function planLogo(data: Uint8ClampedArray, w: number, h: number): { box: Box; size: Size } {
|
||||||
const box = trimBox(data, w, h);
|
const box = trimBox(data, w, h);
|
||||||
if (!box) throw new LogoError(t(LOGO_EMPTY_MESSAGE));
|
if (!box) throw new LogoError(t(LOGO_EMPTY_MESSAGE));
|
||||||
const size = fitWithin(box.w, box.h, LOGO_MAX_W, LOGO_MAX_H);
|
const size = fitWithin(box.w, box.h, LOGO_FILE_MAX_W, LOGO_FILE_MAX_H);
|
||||||
if (size.w < LOGO_MIN || size.h < LOGO_MIN) throw new LogoError(t(LOGO_SMALL_MESSAGE));
|
if (size.w < LOGO_MIN || size.h < LOGO_MIN) throw new LogoError(t(LOGO_SMALL_MESSAGE));
|
||||||
return { box, size };
|
return { box, size };
|
||||||
}
|
}
|
||||||
@@ -144,6 +148,65 @@ function pixels(value: string | undefined): number | null {
|
|||||||
return n > 0 && Number.isFinite(n) ? n : null;
|
return n > 0 && Number.isFinite(n) ? n : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Leerraum samt Byte-Order-Marke (U+FEFF zählt in `\s` mit). */
|
||||||
|
const BLANK = /\s*/y;
|
||||||
|
/** Der Anfang des Wurzelelements: `<svg`, dahinter Leerraum, `/` oder `>`; `<svg:svg` (mit Präfix) und `<svg-x` sind andere Elemente. */
|
||||||
|
const ROOT = /<svg(?=[\s/>])/y;
|
||||||
|
/** Was in einem DOCTYPE ein „>“ oder „]“ enthalten darf, ohne ihn zu beenden: Anführungszeichen, Kommentare und Anweisungen (Anfang, Ende). */
|
||||||
|
const QUOTED: ReadonlyArray<readonly [string, string]> = [['"', '"'], ["'", "'"], ['<!--', '-->'], ['<?', '?>']];
|
||||||
|
|
||||||
|
/** Die Stelle hinter dem ersten `marker` ab `from`; -1, wenn er nicht mehr vorkommt. */
|
||||||
|
function after(text: string, marker: string, from: number): number {
|
||||||
|
const at = text.indexOf(marker, from);
|
||||||
|
return at < 0 ? -1 : at + marker.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Das Ende eines DOCTYPE, der bei `start` beginnt (die Stelle hinter dem schließenden „>“); -1, wenn er nicht endet. Zeichenketten,
|
||||||
|
* Kommentare und Anweisungen darin (auch in der eigenen DTD in `[…]`) können „>“, „]“ und „<svg“ enthalten und zählen nicht als Ende.
|
||||||
|
*/
|
||||||
|
function doctypeEnd(text: string, start: number): number {
|
||||||
|
let inSubset = false;
|
||||||
|
for (let i = start + '<!DOCTYPE'.length; i < text.length; i++) {
|
||||||
|
const quoted = QUOTED.find(([open]) => text.startsWith(open, i));
|
||||||
|
if (quoted) {
|
||||||
|
const end = after(text, quoted[1], i + quoted[0].length);
|
||||||
|
if (end < 0) return -1;
|
||||||
|
i = end - 1;
|
||||||
|
} else if (text[i] === '[') {
|
||||||
|
inSubset = true;
|
||||||
|
} else if (text[i] === ']') {
|
||||||
|
inSubset = false;
|
||||||
|
} else if (text[i] === '>' && !inSubset) {
|
||||||
|
return i + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wo das Wurzelelement `<svg …>` beginnt; -1, wenn der Text nicht damit anfängt. Davor dürfen nur Leerraum (auch eine Byte-Order-Marke),
|
||||||
|
* die XML-Deklaration und andere Anweisungen (`<?…?>`), Kommentare (`<!--…-->`) und ein DOCTYPE stehen; ein `<svg` darin (etwa in einem
|
||||||
|
* auskommentierten Beispiel) ist kein Wurzelelement. Ein Durchlauf von vorn nach hinten ohne Zurücksetzen: auch eine feindliche Datei
|
||||||
|
* braucht nur linear viel Zeit.
|
||||||
|
*/
|
||||||
|
function rootStart(svg: string): number {
|
||||||
|
let i = 0;
|
||||||
|
for (;;) {
|
||||||
|
BLANK.lastIndex = i;
|
||||||
|
BLANK.test(svg);
|
||||||
|
i = BLANK.lastIndex;
|
||||||
|
if (svg.startsWith('<?', i)) i = after(svg, '?>', i + 2);
|
||||||
|
else if (svg.startsWith('<!--', i)) i = after(svg, '-->', i + 4);
|
||||||
|
else if (svg.startsWith('<!DOCTYPE', i)) i = doctypeEnd(svg, i);
|
||||||
|
else {
|
||||||
|
ROOT.lastIndex = i;
|
||||||
|
return ROOT.test(svg) ? i : -1;
|
||||||
|
}
|
||||||
|
if (i < 0) return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Macht den Text eines SVGs rasterbar: Das Seitenverhältnis kommt aus der viewBox des Wurzelelements, wenn sie brauchbar ist, sonst
|
* Macht den Text eines SVGs rasterbar: Das Seitenverhältnis kommt aus der viewBox des Wurzelelements, wenn sie brauchbar ist, sonst
|
||||||
* aus dessen Breite und Höhe in Pixeln (oder ohne Einheit), sonst ist es quadratisch (Prozent und andere Einheiten zählen nicht).
|
* aus dessen Breite und Höhe in Pixeln (oder ohne Einheit), sonst ist es quadratisch (Prozent und andere Einheiten zählen nicht).
|
||||||
@@ -151,16 +214,16 @@ function pixels(value: string | undefined): number | null {
|
|||||||
* SVG scharf auf den Canvas zeichnen (wie `withIntrinsicSize` in card/marks.ts). Eine eigene Breite und Höhe fällt dabei weg: ein
|
* SVG scharf auf den Canvas zeichnen (wie `withIntrinsicSize` in card/marks.ts). Eine eigene Breite und Höhe fällt dabei weg: ein
|
||||||
* doppeltes Attribut machte das SVG ungültig. Fehlt eine brauchbare viewBox, gibt es aber eine Größe, wird `viewBox="0 0 <Breite> <Höhe>"`
|
* doppeltes Attribut machte das SVG ungültig. Fehlt eine brauchbare viewBox, gibt es aber eine Größe, wird `viewBox="0 0 <Breite> <Höhe>"`
|
||||||
* ergänzt (eine unbrauchbare viewBox wird dabei ersetzt), sonst stünde der Inhalt in Originalgröße oben links im Raster. Eine
|
* ergänzt (eine unbrauchbare viewBox wird dabei ersetzt), sonst stünde der Inhalt in Originalgröße oben links im Raster. Eine
|
||||||
* brauchbare viewBox bleibt immer unberührt. Nur das erste `<svg …>` zählt; ohne eines (oder ohne dessen Ende) bleibt der Text
|
* brauchbare viewBox bleibt immer unberührt. Nur das Wurzelelement zählt (siehe `rootStart`); ohne eines (oder ohne dessen Ende)
|
||||||
* unverändert und das Raster quadratisch (das Laden als Bild meldet dann den Fehler).
|
* bleibt der Text unverändert und das Raster quadratisch (das Laden als Bild meldet dann den Fehler).
|
||||||
*/
|
*/
|
||||||
export function sizeSvg(svg: string): SizedSvg {
|
export function sizeSvg(svg: string): SizedSvg {
|
||||||
const unchanged = { svg, ...svgRasterSize(NaN) };
|
const unchanged = { svg, ...svgRasterSize(NaN) };
|
||||||
const start = svg.search(/<svg\b/);
|
const start = rootStart(svg);
|
||||||
if (start < 0) return unchanged;
|
if (start < 0) return unchanged;
|
||||||
// Das öffnende Tag endet am ersten „>“ außerhalb von Anführungszeichen. Genau ein Versuch ab dem ersten „<svg“ (`y`): so braucht
|
// Das öffnende Tag endet am ersten „>“ außerhalb von Anführungszeichen. Genau ein Versuch ab dem Wurzelelement (`y`): so braucht
|
||||||
// auch eine Datei voller offener „<svg x="“ nur linear viel Zeit, ein Suchmuster, das an jedem „<svg“ neu ansetzt, hinge im Tab.
|
// auch eine Datei voller offener „<svg x="“ nur linear viel Zeit, ein Suchmuster, das an jedem „<svg“ neu ansetzt, hinge im Tab.
|
||||||
const tag = /<svg\b((?:[^>"']|"[^"]*"|'[^']*')*)>/y;
|
const tag = /<svg(?=[\s/>])((?:[^>"']|"[^"]*"|'[^']*')*)>/y;
|
||||||
tag.lastIndex = start;
|
tag.lastIndex = start;
|
||||||
const hit = tag.exec(svg);
|
const hit = tag.exec(svg);
|
||||||
if (!hit) return unchanged;
|
if (!hit) return unchanged;
|
||||||
@@ -189,14 +252,6 @@ export function sizeSvg(svg: string): SizedSvg {
|
|||||||
|
|
||||||
// ---- Dekodieren und Zeichnen (braucht einen echten Canvas) --------------------------------------------
|
// ---- Dekodieren und Zeichnen (braucht einen echten Canvas) --------------------------------------------
|
||||||
|
|
||||||
/** Das Quellbild in Originalgröße auf einem Canvas samt seinen Pixeln (RGBA). */
|
|
||||||
interface Drawn {
|
|
||||||
canvas: HTMLCanvasElement;
|
|
||||||
data: Uint8ClampedArray;
|
|
||||||
w: number;
|
|
||||||
h: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Ein Canvas der Größe w × h mit 2D-Kontext. Es werden Pixel gelesen, daher `willReadFrequently`. */
|
/** Ein Canvas der Größe w × h mit 2D-Kontext. Es werden Pixel gelesen, daher `willReadFrequently`. */
|
||||||
function newCanvas(w: number, h: number): { canvas: HTMLCanvasElement; ctx: CanvasRenderingContext2D } {
|
function newCanvas(w: number, h: number): { canvas: HTMLCanvasElement; ctx: CanvasRenderingContext2D } {
|
||||||
const canvas = document.createElement('canvas');
|
const canvas = document.createElement('canvas');
|
||||||
@@ -207,11 +262,10 @@ function newCanvas(w: number, h: number): { canvas: HTMLCanvasElement; ctx: Canv
|
|||||||
return { canvas, ctx };
|
return { canvas, ctx };
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Zeichnet die Quelle in Größe w × h auf einen neuen Canvas und liest dessen Pixel. */
|
/** Gibt den Speicher eines Canvas frei, den niemand mehr braucht (manche Browser behalten ihn sonst bis zur nächsten Speicherbereinigung). */
|
||||||
function draw(source: CanvasImageSource, w: number, h: number): Drawn {
|
function releaseCanvas(canvas: HTMLCanvasElement): void {
|
||||||
const { canvas, ctx } = newCanvas(w, h);
|
canvas.width = 0;
|
||||||
ctx.drawImage(source, 0, 0, w, h);
|
canvas.height = 0;
|
||||||
return { canvas, data: ctx.getImageData(0, 0, w, h).data, w, h };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -228,17 +282,33 @@ async function decode(blob: Blob): Promise<CardImage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** PNG, JPG oder WebP: laden und in Originalgröße zeichnen. */
|
|
||||||
async function drawRaster(file: File): Promise<Drawn> {
|
|
||||||
const { source, w, h } = await decode(file);
|
|
||||||
return draw(source, w, h);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** SVG: Größe aus dem Wurzelelement (siehe `sizeSvg`), nur als Bild geladen (so laufen Skripte darin nie), längste Seite 2048 px. */
|
/** SVG: Größe aus dem Wurzelelement (siehe `sizeSvg`), nur als Bild geladen (so laufen Skripte darin nie), längste Seite 2048 px. */
|
||||||
async function drawSvg(file: File): Promise<Drawn> {
|
async function decodeSvg(file: File): Promise<CardImage> {
|
||||||
const { svg, w, h } = sizeSvg(await file.text());
|
const { svg, w, h } = sizeSvg(await file.text());
|
||||||
const { source } = await decode(new Blob([svg], { type: SVG_TYPE }));
|
const { source } = await decode(new Blob([svg], { type: SVG_TYPE }));
|
||||||
return draw(source, w, h);
|
return { source, w, h };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zeichnet die Datei in Originalgröße (ein SVG in seiner Rastergröße) auf einen Quell-Canvas, schneidet auf das Sichtbare zu und
|
||||||
|
* verkleinert auf die Zielgröße. Der Quell-Canvas (bis zu 40 Megapixel) und der Pixelpuffer, aus dem der Zuschnitt folgt, sind danach
|
||||||
|
* schon wieder frei, noch bevor das PNG entsteht; das gilt auch, wenn etwas scheitert.
|
||||||
|
*/
|
||||||
|
async function renderLogo(file: File): Promise<{ canvas: HTMLCanvasElement; ctx: CanvasRenderingContext2D; size: Size }> {
|
||||||
|
const { source, w, h } = isSvgFile(file) ? await decodeSvg(file) : await decode(file);
|
||||||
|
const from = newCanvas(w, h);
|
||||||
|
try {
|
||||||
|
from.ctx.drawImage(source, 0, 0, w, h);
|
||||||
|
// Der Pixelpuffer (4 Byte je Pixel) steht nur in diesem Aufruf: nach dem Zuschnitt hält ihn niemand mehr.
|
||||||
|
const { box, size } = planLogo(from.ctx.getImageData(0, 0, w, h).data, w, h);
|
||||||
|
const out = newCanvas(size.w, size.h);
|
||||||
|
// Beim Verkleinern ist die Standardqualität (low) grob.
|
||||||
|
out.ctx.imageSmoothingQuality = 'high';
|
||||||
|
out.ctx.drawImage(from.canvas, box.x, box.y, box.w, box.h, 0, 0, size.w, size.h);
|
||||||
|
return { ...out, size };
|
||||||
|
} finally {
|
||||||
|
releaseCanvas(from.canvas);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function toPng(canvas: HTMLCanvasElement): Promise<Blob> {
|
function toPng(canvas: HTMLCanvasElement): Promise<Blob> {
|
||||||
@@ -255,17 +325,12 @@ export async function prepareLogo(file: File): Promise<PreparedLogo> {
|
|||||||
const check = validateLogoFile(file);
|
const check = validateLogoFile(file);
|
||||||
if (!check.ok) throw new LogoError(check.message);
|
if (!check.ok) throw new LogoError(check.message);
|
||||||
try {
|
try {
|
||||||
const { canvas, data, w, h } = isSvgFile(file) ? await drawSvg(file) : await drawRaster(file);
|
const { canvas, ctx, size } = await renderLogo(file);
|
||||||
const { box, size } = planLogo(data, w, h);
|
|
||||||
const out = newCanvas(size.w, size.h);
|
|
||||||
// Beim Verkleinern ist die Standardqualität (low) grob.
|
|
||||||
out.ctx.imageSmoothingQuality = 'high';
|
|
||||||
out.ctx.drawImage(canvas, box.x, box.y, box.w, box.h, 0, 0, size.w, size.h);
|
|
||||||
return {
|
return {
|
||||||
png: await toPng(out.canvas),
|
png: await toPng(canvas),
|
||||||
width: size.w,
|
width: size.w,
|
||||||
height: size.h,
|
height: size.h,
|
||||||
hasTransparency: hasTransparency(out.ctx.getImageData(0, 0, size.w, size.h).data),
|
hasTransparency: hasTransparency(ctx.getImageData(0, 0, size.w, size.h).data),
|
||||||
};
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof LogoError) throw err;
|
if (err instanceof LogoError) throw err;
|
||||||
|
|||||||
Reference in new issue
Block a user