fix: code-review findings in the print module
Security and robustness: - EXIF orientation is now applied before any geometry. Phone photos carry the rotation only as metadata; sharp was cropping the unrotated raster, so a portrait shot came out of the printer sideways and wrongly framed. - renderCell no longer materialises the padded image at source resolution. It is one extract-resize-extend chain now, which is also sharp's internal order. A panorama into a narrow contain target used to build a ~960 MB intermediate and then fail; it is 90 ms and a few MB now. - Target size is capped (300 Mpx) and bleedMm is clamped in /api/print/single, which had no bound at all. - The delivery gallery is validated before use - posixpath.join let a crafted name escape the target's base folder and create directories there. - Sheet requests are capped at 500 pieces and the packer has a step budget, so a degenerate request cannot block the single-threaded server. - Print presets: delete only your own (admins all), config size limit, count limit, and by_name honours anonymous_generations. - Telegram callbacks require an active pairing, like every other path. - Error responses no longer leak storage paths or delivery hostnames. Correctness: - allowRotate:undefined now means allowed, consistently with the packer. - The many-formats shortcut no longer drops a format that only fits rotated. - unplaced names the format that is actually missing, not the first one. - Crop marks never sit inside the printed bleed - the offset is raised. - capacity() computes the grid instead of probing with 200 copies. - Image keys in the sheet cannot collide with a cell literally named x::rot. - labelMm keeps real decimals; parseSizeMm reads a:b as width:height, so 3:4/15 is portrait and 4:3/15 is landscape. - The footer is skipped when there is no free space at the bottom. - The UI warns when corner marks do not fit the margin, and when continuous guides are used with mixed sizes. Tests: 21 -> 31, each finding has a regression test.
This commit is contained in:
+7
-6
@@ -95,11 +95,11 @@ export function parseSizeMm(input: string): { w: number; h: number } | null {
|
||||
// Seitenverhältnis mit Zielkante: „4:3 / 15" oder „4:3 15cm"
|
||||
const ratio = /^(\d+(?:\.\d+)?)\s*[:/]\s*(\d+(?:\.\d+)?)\s*(?:[/@ ]\s*(\d+(?:\.\d+)?)\s*(mm|cm)?)?$/.exec(t);
|
||||
if (ratio && ratio[3]) {
|
||||
// a:b wird als Breite:Höhe gelesen — „3:4/15" ist also hochkant, „4:3/15" quer.
|
||||
const a = parseFloat(ratio[1]), b = parseFloat(ratio[2]);
|
||||
const long = ratio[4] === 'mm' ? parseFloat(ratio[3]) : parseFloat(ratio[3]) * 10;
|
||||
if (!a || !b || !long) return null;
|
||||
const [lo, hi] = a >= b ? [b, a] : [a, b];
|
||||
return norm(long * (lo / hi), long);
|
||||
return a >= b ? norm(long, long * (b / a)) : norm(long * (a / b), long);
|
||||
}
|
||||
|
||||
const unit = /mm\s*$/.test(t) ? 1 : 10; // ohne Einheit: cm
|
||||
@@ -123,9 +123,10 @@ function norm(w: number, h: number): { w: number; h: number } | null {
|
||||
export const mmToPx = (mm: number, dpi: number) => Math.round((mm / 25.4) * dpi);
|
||||
export const mmToPt = (mm: number) => (mm / 25.4) * 72;
|
||||
|
||||
/** Hübsche Beschriftung eines Maßes für die Oberfläche. */
|
||||
/** Hübsche Beschriftung eines Maßes — ohne die echten Nachkommastellen zu verlieren. */
|
||||
export function labelMm(w: number, h: number): string {
|
||||
const f = (n: number) => (n % 10 === 0 ? String(n / 10) : String(Math.round(n) / 10).replace('.', ','));
|
||||
return w < 100 && h < 100 ? `${fmt(w)} × ${fmt(h)} mm` : `${f(w)} × ${f(h)} cm`;
|
||||
const mm = (n: number) => de(Math.round(n * 10) / 10);
|
||||
const cm = (n: number) => de(Math.round(n * 100) / 1000);
|
||||
return w < 100 && h < 100 ? `${mm(w)} × ${mm(h)} mm` : `${cm(w)} × ${cm(h)} cm`;
|
||||
}
|
||||
const fmt = (n: number) => String(Math.round(n * 10) / 10).replace('.', ',');
|
||||
const de = (n: number) => String(n).replace('.', ',');
|
||||
|
||||
Reference in New Issue
Block a user