The logo, signature, stamp and cancelled watermark were placed with ExcelJS two-cell (tl/br) anchors, which stretch each image to fill a cell range — distorting them and making the watermark text small/squished. The PDF looked fine because CSS sizes by aspect. - New lib/image-size.ts: getImageSize (PNG/JPEG/WebP header parse) + scaleToBox. - Export route now places each image with a oneCell `tl` + pixel `ext`, aspect preserved and matched to the PDF sizes (logo ≤96×52, signature ≤165×44, stamp ≤80×66, watermark ≤880×720). - Watermark regenerated as a landscape canvas with the text filling it, so it spans the page like the PDF instead of sitting small in the centre. - Unit test for getImageSize + scaleToBox. Verified structurally: generated XLSX uses oneCellAnchors with fixed pixel ext sizes (49×52 / 45×44 / 67×66 / 880×629), not stretched cell ranges. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
55 lines
1.9 KiB
TypeScript
55 lines
1.9 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
import { getImageSize, scaleToBox } from "@/lib/image-size";
|
|
|
|
function fakePng(width: number, height: number): Buffer {
|
|
const b = Buffer.alloc(24);
|
|
b[0] = 0x89; b[1] = 0x50; b[2] = 0x4e; b[3] = 0x47; // PNG signature start
|
|
b.writeUInt32BE(width, 16);
|
|
b.writeUInt32BE(height, 20);
|
|
return b;
|
|
}
|
|
|
|
function fakeJpeg(width: number, height: number): Buffer {
|
|
const b = Buffer.alloc(20);
|
|
b[0] = 0xff; b[1] = 0xd8; // SOI
|
|
b[2] = 0xff; b[3] = 0xc0; // SOF0 marker
|
|
b.writeUInt16BE(0x11, 4); // segment length
|
|
b[6] = 8; // precision
|
|
b.writeUInt16BE(height, 7);
|
|
b.writeUInt16BE(width, 9);
|
|
return b;
|
|
}
|
|
|
|
describe("getImageSize", () => {
|
|
it("reads PNG dimensions", () => {
|
|
expect(getImageSize(fakePng(640, 480))).toEqual({ width: 640, height: 480 });
|
|
});
|
|
|
|
it("reads JPEG dimensions from the SOF marker", () => {
|
|
expect(getImageSize(fakeJpeg(1024, 768))).toEqual({ width: 1024, height: 768 });
|
|
});
|
|
|
|
it("returns null for non-image data", () => {
|
|
expect(getImageSize(Buffer.from("not an image at all"))).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("scaleToBox", () => {
|
|
it("preserves a square aspect ratio (downscale by the binding side)", () => {
|
|
const r = scaleToBox({ width: 200, height: 200 }, 96, 52);
|
|
expect(r.width).toBe(r.height); // stays square — never stretched
|
|
expect(r.height).toBeLessThanOrEqual(52);
|
|
});
|
|
|
|
it("fits a wide image to the width and keeps the ratio", () => {
|
|
const r = scaleToBox({ width: 360, height: 96 }, 165, 44);
|
|
expect(r.width).toBeLessThanOrEqual(165);
|
|
expect(r.height).toBeLessThanOrEqual(44);
|
|
expect(r.width / r.height).toBeCloseTo(360 / 96, 1);
|
|
});
|
|
|
|
it("keeps the watermark's landscape ratio", () => {
|
|
const r = scaleToBox({ width: 1400, height: 1000 }, 880, 720);
|
|
expect(r).toEqual({ width: 880, height: 629 });
|
|
});
|
|
});
|