diff --git a/web/src/card/marks.test.ts b/web/src/card/marks.test.ts
index 5c587d1..fbe5be8 100644
--- a/web/src/card/marks.test.ts
+++ b/web/src/card/marks.test.ts
@@ -2,7 +2,7 @@ import { readdirSync } from 'node:fs';
import { join } from 'node:path';
import { afterEach, beforeEach, describe, it, expect, vi } from 'vitest';
import favicon from '../../public/favicon.svg?raw';
-import { BRAND_MARKS, getBrandMark, loadMarkImage, svgAspect, tintSvg } from './marks';
+import { BRAND_MARKS, getBrandMark, loadMarkImage, svgAspect, tintSvg, viewBoxAspect } from './marks';
const BRAND_IDS = ['aircards-symbol', 'aircards-text'];
@@ -61,6 +61,15 @@ describe('SVG-Inhalt', () => {
expect(svgAspect('')).toBeCloseTo(3, 1);
});
+ it('viewBoxAspect: Breite durch Höhe aus dem Wert einer viewBox (Leerraum oder Kommas), sonst null', () => {
+ expect(viewBoxAspect('0 0 300 100')).toBeCloseTo(3, 5);
+ expect(viewBoxAspect(' 10,20,200,100 ')).toBeCloseTo(2, 5);
+ expect(viewBoxAspect('0 0 100 100')).toBe(1);
+ for (const unusable of ['', 'none', '0 0 0 10', '0 0 10 0', '0 0 -10 5', '0 0 10', '0 0 1 2 3', '0 0 a b', '0 0 Infinity 5']) {
+ expect(viewBoxAspect(unusable), JSON.stringify(unusable)).toBeNull();
+ }
+ });
+
it('beide Marken sind einfärbbar und enthalten currentColor', () => {
for (const m of BRAND_MARKS) {
expect(m.tint, m.id).toBe(true);
diff --git a/web/src/card/marks.ts b/web/src/card/marks.ts
index 20eb899..70be8d9 100644
--- a/web/src/card/marks.ts
+++ b/web/src/card/marks.ts
@@ -36,13 +36,17 @@ export function getBrandMark(id: Exclude): MarkDef {
return BRAND_MARKS.find((m) => m.id === id) ?? BRAND_MARKS[0];
}
+/** Breite durch Höhe aus dem Wert einer viewBox („x y Breite Höhe“, mit Leerraum oder Kommas); null, wenn er nicht brauchbar ist. */
+export function viewBoxAspect(value: string): number | null {
+ const parts = value.trim().split(/[\s,]+/).map(Number);
+ const [w, h] = [parts[2], parts[3]];
+ return parts.length === 4 && Number.isFinite(w) && Number.isFinite(h) && w > 0 && h > 0 ? w / h : null;
+}
+
/** Breite durch Höhe aus der viewBox; 1, wenn keine brauchbare viewBox da ist. */
export function svgAspect(svg: string): number {
const hit = /viewBox\s*=\s*"([^"]+)"/.exec(svg);
- if (!hit) return 1;
- const parts = hit[1].trim().split(/[\s,]+/).map(Number);
- const [w, h] = [parts[2], parts[3]];
- return parts.length === 4 && Number.isFinite(w) && Number.isFinite(h) && w > 0 && h > 0 ? w / h : 1;
+ return (hit && viewBoxAspect(hit[1])) ?? 1;
}
/** Ersetzt jedes `currentColor` durch die Details-Farbe. */
diff --git a/web/src/logos/prepare-logo.test.ts b/web/src/logos/prepare-logo.test.ts
index fd227fb..9afd898 100644
--- a/web/src/logos/prepare-logo.test.ts
+++ b/web/src/logos/prepare-logo.test.ts
@@ -1,12 +1,19 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { setLang } from '../i18n/i18n';
import {
+ ALPHA_EMPTY,
LOGO_ACCEPT,
+ LOGO_MAX_H,
+ LOGO_MAX_W,
+ LOGO_MIN,
+ LOGO_SOURCE_MAX_BYTES,
+ LOGO_SOURCE_MAX_PIXELS,
+ SVG_RASTER_MAX,
fitWithin,
hasTransparency,
planLogo,
prepareLogo,
- sizeSvgRoot,
+ sizeSvg,
svgRasterSize,
trimBox,
validateLogoFile,
@@ -39,6 +46,13 @@ describe('LOGO_ACCEPT', () => {
});
});
+describe('Konstanten', () => {
+ 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_SOURCE_MAX_BYTES, LOGO_SOURCE_MAX_PIXELS]).toEqual([5_242_880, 40_000_000]);
+ });
+});
+
describe('validateLogoFile', () => {
const check = (type: string, size = 1000, name = 'logo') => validateLogoFile({ type, size, name });
@@ -243,79 +257,192 @@ describe('planLogo', () => {
});
});
-describe('sizeSvgRoot', () => {
+describe('sizeSvg', () => {
const NS = 'xmlns="http://www.w3.org/2000/svg"';
- /** Wie oft das Attribut `name` im öffnenden Tag des Wurzelelements steht. */
- const countIn = (svg: string, name: string): number => {
- const root = /^[^]*?(';
- it('schreibt Breite und Höhe ins Wurzelelement und lässt den Rest unverändert', () => {
- const svg = ``;
- expect(sizeSvgRoot(svg, 2048, 683)).toBe(
- ``,
- );
+ // [Fall, Wurzelelement vorher, Wurzelelement nachher, Rasterbreite, Rasterhöhe]
+ it.each<[string, string, string, number, number]>([
+ [
+ 'Größe ohne Einheit und ohne viewBox: Seitenverhältnis aus Breite und Höhe, viewBox ergänzt',
+ '