patx/makeitpdf

/* Run with: node --test */

const test = require("node:test");
const assert = require("node:assert/strict");
const Pages = require("../pages.js");

const round = (n) => Math.round(n);

test("portrait images get a portrait A4 page", () => {
  const page = Pages.pageSize(1200, 1800);
  assert.equal(round(page.width), 595);
  assert.equal(round(page.height), 842);
  assert.equal(page.orientation, "p");
});

test("landscape images get a landscape A4 page", () => {
  const page = Pages.pageSize(1800, 1200);
  assert.equal(round(page.width), 842);
  assert.equal(round(page.height), 595);
  assert.equal(page.orientation, "l");
});

test("square images stay portrait", () => {
  assert.equal(Pages.pageSize(1000, 1000).orientation, "p");
});

test("a batch can mix orientations", () => {
  const orientations = [[400, 800], [800, 400], [600, 900]].map(
    ([w, h]) => Pages.pageSize(w, h).orientation
  );
  assert.deepEqual(orientations, ["p", "l", "p"]);
});

test("the image is centred on the page", () => {
  const page = Pages.pageSize(1000, 1000);
  const spot = Pages.placeImage(1000, 1000, page.width, page.height);
  const rightGap = page.width - (spot.x + spot.width);
  const bottomGap = page.height - (spot.y + spot.height);
  assert.ok(Math.abs(spot.x - rightGap) < 0.001, "equal side margins");
  assert.ok(Math.abs(spot.y - bottomGap) < 0.001, "equal top and bottom margins");
});

test("the image never crosses the margin", () => {
  const page = Pages.pageSize(1200, 1800);
  const spot = Pages.placeImage(1200, 1800, page.width, page.height);
  assert.ok(spot.x >= Pages.MARGIN_PT - 0.001);
  assert.ok(spot.y >= Pages.MARGIN_PT - 0.001);
  assert.ok(spot.width <= page.width - Pages.MARGIN_PT * 2 + 0.001);
  assert.ok(spot.height <= page.height - Pages.MARGIN_PT * 2 + 0.001);
});

test("aspect ratio survives placement", () => {
  const page = Pages.pageSize(1600, 900);
  const spot = Pages.placeImage(1600, 900, page.width, page.height);
  assert.ok(Math.abs(spot.width / spot.height - 1600 / 900) < 0.001);
});

test("a very wide image is limited by width, not height", () => {
  const page = Pages.pageSize(4000, 500);
  const spot = Pages.placeImage(4000, 500, page.width, page.height);
  assert.ok(Math.abs(spot.width - (page.width - Pages.MARGIN_PT * 2)) < 0.001);
  assert.ok(spot.height < page.height - Pages.MARGIN_PT * 2);
});

test("a very tall image is limited by height, not width", () => {
  const page = Pages.pageSize(500, 4000);
  const spot = Pages.placeImage(500, 4000, page.width, page.height);
  assert.ok(Math.abs(spot.height - (page.height - Pages.MARGIN_PT * 2)) < 0.001);
  assert.ok(spot.width < page.width - Pages.MARGIN_PT * 2);
});

test("a small image is scaled up to fill the page", () => {
  const page = Pages.pageSize(100, 150);
  const spot = Pages.placeImage(100, 150, page.width, page.height);
  assert.ok(spot.height > 700, "a thumbnail still fills an A4 page");
});

test("raster size follows the render density", () => {
  const spot = { width: 288, height: 144 };
  // 288pt = 4in; at 150 DPI that is 600px.
  assert.deepEqual(Pages.rasterSize(spot), { width: 600, height: 300 });
  assert.deepEqual(Pages.rasterSize(spot, 72), { width: 288, height: 144 });
});

test("raster size never collapses to zero", () => {
  const raster = Pages.rasterSize({ width: 0.01, height: 0.01 });
  assert.ok(raster.width >= 1 && raster.height >= 1);
});

test("an A4 content area stays a sane pixel size", () => {
  const page = Pages.pageSize(6000, 8000);
  const raster = Pages.rasterSize(Pages.placeImage(6000, 8000, page.width, page.height));
  assert.ok(raster.width < 1300, `expected a downscale, got ${raster.width}px`);
});

test("byte formatting", () => {
  assert.equal(Pages.formatBytes(512), "512 B");
  assert.equal(Pages.formatBytes(2048), "2 KB");
  assert.equal(Pages.formatBytes(5 * 1024 * 1024), "5.0 MB");
});

test("TIFF is detected from magic bytes in both byte orders", () => {
  assert.equal(Pages.isTiff(new Uint8Array([0x49, 0x49, 0x2a, 0x00])), true, "II*\\0");
  assert.equal(Pages.isTiff(new Uint8Array([0x4d, 0x4d, 0x00, 0x2a])), true, "MM\\0*");
});

test("other formats are not mistaken for TIFF", () => {
  assert.equal(Pages.isTiff(new Uint8Array([0xff, 0xd8, 0xff, 0xe0])), false, "JPEG");
  assert.equal(Pages.isTiff(new Uint8Array([0x89, 0x50, 0x4e, 0x47])), false, "PNG");
  assert.equal(Pages.isTiff(new Uint8Array([0x52, 0x49, 0x46, 0x46])), false, "WebP");
  assert.equal(Pages.isTiff(new Uint8Array([0x49, 0x49])), false, "truncated");
  assert.equal(Pages.isTiff(null), false, "nothing");
});

test("image pages use embedded physical resolution instead of A4", () => {
  const page = Pages.imagePage(1200, 600, 300, 300, 1);
  assert.equal(page.width, 288);
  assert.equal(page.height, 144);
  assert.equal(page.capped, false);
});

test("images without usable DPI metadata fall back to 96 DPI", () => {
  const page = Pages.imagePage(960, 480, undefined, 0, 1);
  assert.equal(page.width, 720);
  assert.equal(page.height, 360);
  assert.equal(page.dpiX, 96);
  assert.equal(page.dpiY, 96);
});

test("EXIF rotations swap pixel dimensions and resolution axes", () => {
  const page = Pages.imagePage(1200, 600, 300, 150, 6);
  assert.equal(page.pixelWidth, 600);
  assert.equal(page.pixelHeight, 1200);
  assert.equal(page.width, 288);
  assert.equal(page.height, 288);
});

test("oversized physical pages are capped proportionally", () => {
  const page = Pages.imagePage(40000, 20000, 1, 1, 1);
  assert.equal(page.width, Pages.MAX_PAGE_PT);
  assert.equal(page.height, Pages.MAX_PAGE_PT / 2);
  assert.equal(page.capped, true);
  assert.ok(page.scale < 1);
});

test("PNG pHYs metadata is converted from pixels per metre to DPI", () => {
  const bytes = new Uint8Array(54);
  bytes.set([137, 80, 78, 71, 13, 10, 26, 10]);
  const view = new DataView(bytes.buffer);
  view.setUint32(8, 13, false);
  bytes.set([73, 72, 68, 82], 12);
  view.setUint32(16, 1200, false);
  view.setUint32(20, 600, false);
  view.setUint32(33, 9, false);
  bytes.set([112, 72, 89, 115], 37);
  view.setUint32(41, 11811, false); // approximately 300 DPI
  view.setUint32(45, 5906, false); // approximately 150 DPI
  bytes[49] = 1;
  const metadata = Pages.parseRasterMetadata(bytes);
  assert.equal(metadata.width, 1200);
  assert.equal(metadata.height, 600);
  assert.ok(Math.abs(metadata.dpiX - 300) < 0.02);
  assert.ok(Math.abs(metadata.dpiY - 150) < 0.02);
});

test("BMP pixels-per-metre metadata is read", () => {
  const bytes = new Uint8Array(46);
  bytes.set([66, 77]);
  const view = new DataView(bytes.buffer);
  view.setInt32(18, 800, true);
  view.setInt32(22, 400, true);
  view.setInt32(38, 3780, true);
  view.setInt32(42, 3780, true);
  const metadata = Pages.parseRasterMetadata(bytes);
  assert.equal(metadata.width, 800);
  assert.equal(metadata.height, 400);
  assert.ok(Math.abs(metadata.dpiX - 96) < 0.02);
});

test("JPEG JFIF density and dimensions are read", () => {
  const bytes = new Uint8Array([
    0xff, 0xd8,
    0xff, 0xe0, 0x00, 0x10,
    0x4a, 0x46, 0x49, 0x46, 0x00, 0x01, 0x01, 0x01, 0x01, 0x2c, 0x00, 0x96, 0x00, 0x00,
    0xff, 0xc0, 0x00, 0x11, 0x08, 0x02, 0x58, 0x04, 0xb0,
    0x03, 0x01, 0x11, 0x00, 0x02, 0x11, 0x00, 0x03, 0x11, 0x00,
    0xff, 0xd9,
  ]);
  const metadata = Pages.parseRasterMetadata(bytes);
  assert.equal(metadata.width, 1200);
  assert.equal(metadata.height, 600);
  assert.equal(metadata.dpiX, 300);
  assert.equal(metadata.dpiY, 150);
});