Files
png2pdf/src/lib/layout.ts
T

109 lines
2.8 KiB
TypeScript
Raw Normal View History

2026-10-23 15:30:19 -05:00
import { Png2PdfError } from "./errors.js";
export const PAGE_SIZES = ["fit", "a4", "letter"] as const;
export type PageSize = (typeof PAGE_SIZES)[number];
export type PageLayoutOptions = {
pixelWidth: number;
pixelHeight: number;
dpi?: number;
pageSize?: PageSize;
margin?: number;
};
export type PageLayout = {
pageWidth: number;
pageHeight: number;
imageX: number;
imageY: number;
imageWidth: number;
imageHeight: number;
};
const POINTS_PER_INCH = 72;
const DEFAULT_DPI = 96;
const PAPER_DIMENSIONS = {
a4: { width: 595.28, height: 841.89 },
letter: { width: 612, height: 792 },
} as const satisfies Record<
Exclude<PageSize, "fit">,
{ width: number; height: number }
>;
export function isPageSize(value: string): value is PageSize {
return (PAGE_SIZES as readonly string[]).includes(value);
}
export function calculatePageLayout(options: PageLayoutOptions): PageLayout {
const {
pixelWidth,
pixelHeight,
dpi = DEFAULT_DPI,
pageSize = "fit",
margin = 0,
} = options;
assertPositiveFinite(pixelWidth, "Image width");
assertPositiveFinite(pixelHeight, "Image height");
assertPositiveFinite(dpi, "DPI");
assertNonNegativeFinite(margin, "Margin");
const imageWidth = pixelsToPoints(pixelWidth, dpi);
const imageHeight = pixelsToPoints(pixelHeight, dpi);
if (pageSize === "fit") {
return {
pageWidth: imageWidth + margin * 2,
pageHeight: imageHeight + margin * 2,
imageX: margin,
imageY: margin,
imageWidth,
imageHeight,
};
}
const paper = PAPER_DIMENSIONS[pageSize];
const landscape = imageWidth > imageHeight;
const pageWidth = landscape ? paper.height : paper.width;
const pageHeight = landscape ? paper.width : paper.height;
const availableWidth = pageWidth - margin * 2;
const availableHeight = pageHeight - margin * 2;
if (availableWidth <= 0 || availableHeight <= 0) {
throw new Png2PdfError(
`Margin ${margin} pt is too large for ${pageSize.toUpperCase()} paper.`,
);
}
const scale = Math.min(
availableWidth / imageWidth,
availableHeight / imageHeight,
);
const drawnWidth = imageWidth * scale;
const drawnHeight = imageHeight * scale;
return {
pageWidth,
pageHeight,
imageX: (pageWidth - drawnWidth) / 2,
imageY: (pageHeight - drawnHeight) / 2,
imageWidth: drawnWidth,
imageHeight: drawnHeight,
};
}
function pixelsToPoints(pixels: number, dpi: number): number {
return (pixels / dpi) * POINTS_PER_INCH;
}
function assertPositiveFinite(value: number, label: string): void {
if (!Number.isFinite(value) || value <= 0) {
throw new Png2PdfError(`${label} must be a positive number.`);
}
}
function assertNonNegativeFinite(value: number, label: string): void {
if (!Number.isFinite(value) || value < 0) {
throw new Png2PdfError(`${label} must be zero or greater.`);
}
}