fix: auto orientation in Fix Page Size now matches source page layout and add tests
This commit is contained in:
@@ -489,12 +489,26 @@ export async function fixPageSize(
|
|||||||
const { width: sourceWidth, height: sourceHeight } = sourcePage.getSize();
|
const { width: sourceWidth, height: sourceHeight } = sourcePage.getSize();
|
||||||
const embeddedPage = await outputDoc.embedPage(sourcePage);
|
const embeddedPage = await outputDoc.embedPage(sourcePage);
|
||||||
|
|
||||||
const outputPage = outputDoc.addPage([targetWidth, targetHeight]);
|
let pageTargetWidth = targetWidth;
|
||||||
|
let pageTargetHeight = targetHeight;
|
||||||
|
|
||||||
|
if (orientation === 'auto') {
|
||||||
|
const isSourceLandscape = sourceWidth > sourceHeight;
|
||||||
|
const isTargetLandscape = pageTargetWidth > pageTargetHeight;
|
||||||
|
if (isSourceLandscape !== isTargetLandscape) {
|
||||||
|
[pageTargetWidth, pageTargetHeight] = [
|
||||||
|
pageTargetHeight,
|
||||||
|
pageTargetWidth,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const outputPage = outputDoc.addPage([pageTargetWidth, pageTargetHeight]);
|
||||||
outputPage.drawRectangle({
|
outputPage.drawRectangle({
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
width: targetWidth,
|
width: pageTargetWidth,
|
||||||
height: targetHeight,
|
height: pageTargetHeight,
|
||||||
color: rgb(
|
color: rgb(
|
||||||
options.backgroundColor.r,
|
options.backgroundColor.r,
|
||||||
options.backgroundColor.g,
|
options.backgroundColor.g,
|
||||||
@@ -502,16 +516,16 @@ export async function fixPageSize(
|
|||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
const scaleX = targetWidth / sourceWidth;
|
const scaleX = pageTargetWidth / sourceWidth;
|
||||||
const scaleY = targetHeight / sourceHeight;
|
const scaleY = pageTargetHeight / sourceHeight;
|
||||||
const useFill = options.scalingMode.toLowerCase() === 'fill';
|
const useFill = options.scalingMode.toLowerCase() === 'fill';
|
||||||
const scale = useFill ? Math.max(scaleX, scaleY) : Math.min(scaleX, scaleY);
|
const scale = useFill ? Math.max(scaleX, scaleY) : Math.min(scaleX, scaleY);
|
||||||
|
|
||||||
const scaledWidth = sourceWidth * scale;
|
const scaledWidth = sourceWidth * scale;
|
||||||
const scaledHeight = sourceHeight * scale;
|
const scaledHeight = sourceHeight * scale;
|
||||||
|
|
||||||
const x = (targetWidth - scaledWidth) / 2;
|
const x = (pageTargetWidth - scaledWidth) / 2;
|
||||||
const y = (targetHeight - scaledHeight) / 2;
|
const y = (pageTargetHeight - scaledHeight) / 2;
|
||||||
|
|
||||||
outputPage.drawPage(embeddedPage, {
|
outputPage.drawPage(embeddedPage, {
|
||||||
x,
|
x,
|
||||||
|
|||||||
250
src/tests/fix-page-size.test.ts
Normal file
250
src/tests/fix-page-size.test.ts
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { fixPageSize, FixPageSizeOptions } from '../js/utils/pdf-operations';
|
||||||
|
import { PDFDocument, PageSizes, rgb } from 'pdf-lib';
|
||||||
|
|
||||||
|
const WHITE = { r: 1, g: 1, b: 1 };
|
||||||
|
|
||||||
|
function opts(overrides: Partial<FixPageSizeOptions> = {}): FixPageSizeOptions {
|
||||||
|
return {
|
||||||
|
targetSize: 'A4',
|
||||||
|
orientation: 'auto',
|
||||||
|
scalingMode: 'fit',
|
||||||
|
backgroundColor: WHITE,
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createPdf(pages: [number, number][]): Promise<Uint8Array> {
|
||||||
|
const doc = await PDFDocument.create();
|
||||||
|
for (const [w, h] of pages) {
|
||||||
|
const page = doc.addPage([w, h]);
|
||||||
|
page.drawRectangle({
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: w,
|
||||||
|
height: h,
|
||||||
|
color: rgb(1, 1, 1),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return new Uint8Array(await doc.save());
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getPageSizes(pdfBytes: Uint8Array): Promise<[number, number][]> {
|
||||||
|
const doc = await PDFDocument.load(pdfBytes);
|
||||||
|
return doc.getPages().map((p) => {
|
||||||
|
const { width, height } = p.getSize();
|
||||||
|
return [Math.round(width), Math.round(height)];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const A4_W = Math.round(PageSizes.A4[0]);
|
||||||
|
const A4_H = Math.round(PageSizes.A4[1]);
|
||||||
|
|
||||||
|
describe('fixPageSize', () => {
|
||||||
|
describe('orientation: auto', () => {
|
||||||
|
it('keeps portrait target for portrait source', async () => {
|
||||||
|
const pdf = await createPdf([[400, 600]]);
|
||||||
|
const result = await fixPageSize(pdf, opts({ orientation: 'auto' }));
|
||||||
|
const sizes = await getPageSizes(result);
|
||||||
|
expect(sizes[0][0]).toBeLessThan(sizes[0][1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('swaps to landscape target for landscape source', async () => {
|
||||||
|
const pdf = await createPdf([[800, 400]]);
|
||||||
|
const result = await fixPageSize(pdf, opts({ orientation: 'auto' }));
|
||||||
|
const sizes = await getPageSizes(result);
|
||||||
|
expect(sizes[0][0]).toBeGreaterThan(sizes[0][1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles mixed portrait and landscape pages', async () => {
|
||||||
|
const pdf = await createPdf([
|
||||||
|
[400, 600],
|
||||||
|
[800, 400],
|
||||||
|
[300, 500],
|
||||||
|
]);
|
||||||
|
const result = await fixPageSize(pdf, opts({ orientation: 'auto' }));
|
||||||
|
const sizes = await getPageSizes(result);
|
||||||
|
expect(sizes[0][0]).toBeLessThan(sizes[0][1]);
|
||||||
|
expect(sizes[1][0]).toBeGreaterThan(sizes[1][1]);
|
||||||
|
expect(sizes[2][0]).toBeLessThan(sizes[2][1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps square pages as portrait target', async () => {
|
||||||
|
const pdf = await createPdf([[500, 500]]);
|
||||||
|
const result = await fixPageSize(pdf, opts({ orientation: 'auto' }));
|
||||||
|
const sizes = await getPageSizes(result);
|
||||||
|
expect(sizes[0][0]).toBe(A4_W);
|
||||||
|
expect(sizes[0][1]).toBe(A4_H);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('orientation: portrait', () => {
|
||||||
|
it('forces portrait regardless of source orientation', async () => {
|
||||||
|
const pdf = await createPdf([[800, 400]]);
|
||||||
|
const result = await fixPageSize(pdf, opts({ orientation: 'portrait' }));
|
||||||
|
const sizes = await getPageSizes(result);
|
||||||
|
expect(sizes[0][0]).toBeLessThan(sizes[0][1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps portrait for portrait source', async () => {
|
||||||
|
const pdf = await createPdf([[400, 600]]);
|
||||||
|
const result = await fixPageSize(pdf, opts({ orientation: 'portrait' }));
|
||||||
|
const sizes = await getPageSizes(result);
|
||||||
|
expect(sizes[0][0]).toBe(A4_W);
|
||||||
|
expect(sizes[0][1]).toBe(A4_H);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('orientation: landscape', () => {
|
||||||
|
it('forces landscape regardless of source orientation', async () => {
|
||||||
|
const pdf = await createPdf([[400, 600]]);
|
||||||
|
const result = await fixPageSize(pdf, opts({ orientation: 'landscape' }));
|
||||||
|
const sizes = await getPageSizes(result);
|
||||||
|
expect(sizes[0][0]).toBeGreaterThan(sizes[0][1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps landscape for landscape source', async () => {
|
||||||
|
const pdf = await createPdf([[800, 400]]);
|
||||||
|
const result = await fixPageSize(pdf, opts({ orientation: 'landscape' }));
|
||||||
|
const sizes = await getPageSizes(result);
|
||||||
|
expect(sizes[0][0]).toBe(A4_H);
|
||||||
|
expect(sizes[0][1]).toBe(A4_W);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('target size: predefined', () => {
|
||||||
|
it('uses A4 dimensions', async () => {
|
||||||
|
const pdf = await createPdf([[400, 600]]);
|
||||||
|
const result = await fixPageSize(pdf, opts({ targetSize: 'A4' }));
|
||||||
|
const sizes = await getPageSizes(result);
|
||||||
|
expect(sizes[0]).toEqual([A4_W, A4_H]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses Letter dimensions', async () => {
|
||||||
|
const pdf = await createPdf([[400, 600]]);
|
||||||
|
const result = await fixPageSize(pdf, opts({ targetSize: 'Letter' }));
|
||||||
|
const sizes = await getPageSizes(result);
|
||||||
|
expect(sizes[0]).toEqual([
|
||||||
|
Math.round(PageSizes.Letter[0]),
|
||||||
|
Math.round(PageSizes.Letter[1]),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('falls back to A4 for unknown size', async () => {
|
||||||
|
const pdf = await createPdf([[400, 600]]);
|
||||||
|
const result = await fixPageSize(pdf, opts({ targetSize: 'Unknown' }));
|
||||||
|
const sizes = await getPageSizes(result);
|
||||||
|
expect(sizes[0]).toEqual([A4_W, A4_H]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('target size: custom', () => {
|
||||||
|
it('uses custom mm dimensions', async () => {
|
||||||
|
const pdf = await createPdf([[400, 600]]);
|
||||||
|
const result = await fixPageSize(
|
||||||
|
pdf,
|
||||||
|
opts({
|
||||||
|
targetSize: 'custom',
|
||||||
|
customWidth: 100,
|
||||||
|
customHeight: 200,
|
||||||
|
customUnits: 'mm',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
const sizes = await getPageSizes(result);
|
||||||
|
const expectedW = Math.round(100 * (72 / 25.4));
|
||||||
|
const expectedH = Math.round(200 * (72 / 25.4));
|
||||||
|
expect(sizes[0]).toEqual([expectedW, expectedH]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses custom inch dimensions', async () => {
|
||||||
|
const pdf = await createPdf([[400, 600]]);
|
||||||
|
const result = await fixPageSize(
|
||||||
|
pdf,
|
||||||
|
opts({
|
||||||
|
targetSize: 'custom',
|
||||||
|
customWidth: 5,
|
||||||
|
customHeight: 8,
|
||||||
|
customUnits: 'in',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
const sizes = await getPageSizes(result);
|
||||||
|
expect(sizes[0]).toEqual([360, 576]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('defaults custom dimensions to 210x297mm when not provided', async () => {
|
||||||
|
const pdf = await createPdf([[400, 600]]);
|
||||||
|
const result = await fixPageSize(pdf, opts({ targetSize: 'custom' }));
|
||||||
|
const sizes = await getPageSizes(result);
|
||||||
|
expect(sizes[0]).toEqual([A4_W, A4_H]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('scaling mode', () => {
|
||||||
|
it('preserves page count', async () => {
|
||||||
|
const pdf = await createPdf([
|
||||||
|
[400, 600],
|
||||||
|
[500, 700],
|
||||||
|
[300, 400],
|
||||||
|
]);
|
||||||
|
const result = await fixPageSize(pdf, opts({ scalingMode: 'fit' }));
|
||||||
|
const sizes = await getPageSizes(result);
|
||||||
|
expect(sizes.length).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fit mode does not exceed target dimensions', async () => {
|
||||||
|
const pdf = await createPdf([[1000, 500]]);
|
||||||
|
const result = await fixPageSize(
|
||||||
|
pdf,
|
||||||
|
opts({ orientation: 'landscape', scalingMode: 'fit' })
|
||||||
|
);
|
||||||
|
const doc = await PDFDocument.load(result);
|
||||||
|
const page = doc.getPages()[0];
|
||||||
|
const { width, height } = page.getSize();
|
||||||
|
expect(width).toBeLessThanOrEqual(A4_H + 1);
|
||||||
|
expect(height).toBeLessThanOrEqual(A4_W + 1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('single page', () => {
|
||||||
|
it('converts single page PDF', async () => {
|
||||||
|
const pdf = await createPdf([[612, 792]]);
|
||||||
|
const result = await fixPageSize(pdf, opts());
|
||||||
|
const sizes = await getPageSizes(result);
|
||||||
|
expect(sizes.length).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('auto orientation with custom size', () => {
|
||||||
|
it('swaps custom dimensions for landscape source', async () => {
|
||||||
|
const pdf = await createPdf([[800, 400]]);
|
||||||
|
const result = await fixPageSize(
|
||||||
|
pdf,
|
||||||
|
opts({
|
||||||
|
targetSize: 'custom',
|
||||||
|
customWidth: 100,
|
||||||
|
customHeight: 200,
|
||||||
|
customUnits: 'mm',
|
||||||
|
orientation: 'auto',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
const sizes = await getPageSizes(result);
|
||||||
|
expect(sizes[0][0]).toBeGreaterThan(sizes[0][1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps custom dimensions for portrait source', async () => {
|
||||||
|
const pdf = await createPdf([[400, 800]]);
|
||||||
|
const result = await fixPageSize(
|
||||||
|
pdf,
|
||||||
|
opts({
|
||||||
|
targetSize: 'custom',
|
||||||
|
customWidth: 100,
|
||||||
|
customHeight: 200,
|
||||||
|
customUnits: 'mm',
|
||||||
|
orientation: 'auto',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
const sizes = await getPageSizes(result);
|
||||||
|
expect(sizes[0][0]).toBeLessThan(sizes[0][1]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user