Skip to content

Commit 7cde7e6

Browse files
committed
docs(examples): add resize pages to target size example
Demonstrates resizing pages from mixed-size PDFs to a uniform target size (A4) while preserving annotations via flatten + embedPage + drawPage.
1 parent 2aaeb14 commit 7cde7e6

File tree

1 file changed

+262
-0
lines changed

1 file changed

+262
-0
lines changed
Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
/**
2+
* Example: Resize Pages to Target Size
3+
*
4+
* This example demonstrates how to resize pages from a PDF with mixed page
5+
* sizes (like scanned documents with varying formats) to a uniform target
6+
* size while preserving annotations.
7+
*
8+
* Use case: You have a merged PDF with pages in different sizes (A4, A3, A2,
9+
* letter, etc.) and want to normalize them all to a standard size for printing.
10+
*
11+
* Run: npx tsx examples/02-pages/resize-pages-to-target-size.ts
12+
*/
13+
14+
import { black, PDF, rgb } from "../../src/index";
15+
import { formatBytes, saveOutput } from "../utils";
16+
17+
// Standard page sizes in points (1 point = 1/72 inch)
18+
const PageSizes = {
19+
A4: { width: 595.28, height: 841.89 },
20+
A3: { width: 841.89, height: 1190.55 },
21+
A2: { width: 1190.55, height: 1683.78 },
22+
Letter: { width: 612, height: 792 },
23+
} as const;
24+
25+
async function main() {
26+
console.log("Resizing pages with annotations to target size...\n");
27+
28+
// ============================================================
29+
// Step 1: Create a source PDF with mixed page sizes and annotations
30+
// ============================================================
31+
console.log("=== Creating Source PDF with Mixed Sizes ===\n");
32+
33+
const sourcePdf = PDF.create();
34+
35+
// Page 1: Small page (like a scanned receipt)
36+
const smallPage = sourcePdf.addPage({ width: 300, height: 400 });
37+
38+
smallPage.drawText("Small Page (300x400)", { x: 20, y: 350, size: 16, color: black });
39+
smallPage.drawText("Receipt or small scan", { x: 20, y: 320, size: 12, color: black });
40+
smallPage.drawRectangle({
41+
x: 20,
42+
y: 100,
43+
width: 260,
44+
height: 150,
45+
borderColor: rgb(0.8, 0, 0),
46+
borderWidth: 2,
47+
});
48+
49+
// Add a highlight annotation to page 1
50+
smallPage.addHighlightAnnotation({
51+
rect: { x: 20, y: 345, width: 200, height: 20 },
52+
color: rgb(1, 1, 0),
53+
opacity: 0.5,
54+
});
55+
56+
// Page 2: A4-ish page
57+
const a4Page = sourcePdf.addPage({ width: PageSizes.A4.width, height: PageSizes.A4.height });
58+
59+
a4Page.drawText("A4 Page", { x: 50, y: 780, size: 24, color: black });
60+
a4Page.drawText("Standard document size", { x: 50, y: 740, size: 14, color: black });
61+
a4Page.drawCircle({ x: 297, y: 421, radius: 100, color: rgb(0.9, 0.9, 1) });
62+
63+
// Add a sticky note annotation to page 2
64+
a4Page.addTextAnnotation({
65+
rect: { x: 400, y: 700, width: 24, height: 24 },
66+
contents: "This is a note on the A4 page",
67+
color: rgb(1, 0.9, 0.5),
68+
});
69+
70+
// Page 3: Large page (like A2 poster)
71+
const largePage = sourcePdf.addPage({ width: PageSizes.A2.width, height: PageSizes.A2.height });
72+
73+
largePage.drawText("Large Page (A2 Size)", { x: 100, y: 1600, size: 48, color: black });
74+
largePage.drawText("Poster or technical drawing", { x: 100, y: 1520, size: 24, color: black });
75+
largePage.drawRectangle({
76+
x: 100,
77+
y: 200,
78+
width: 990,
79+
height: 1200,
80+
borderColor: rgb(0, 0, 0.8),
81+
borderWidth: 4,
82+
});
83+
84+
// Add underline annotation to page 3
85+
largePage.addUnderlineAnnotation({
86+
rect: { x: 100, y: 1590, width: 450, height: 30 },
87+
color: rgb(0, 0, 1),
88+
});
89+
90+
// Page 4: Wide landscape page
91+
const widePage = sourcePdf.addPage({ width: 1000, height: 400 });
92+
93+
widePage.drawText("Wide Landscape Page", { x: 50, y: 350, size: 20, color: black });
94+
widePage.drawText("Like a panoramic scan", { x: 50, y: 310, size: 14, color: black });
95+
96+
console.log("Source PDF pages:");
97+
98+
for (let i = 0; i < sourcePdf.getPageCount(); i++) {
99+
const page = sourcePdf.getPage(i);
100+
101+
if (page) {
102+
const annotations = page.getAnnotations();
103+
console.log(
104+
` Page ${i + 1}: ${page.width.toFixed(0)} x ${page.height.toFixed(0)} pts, ${annotations.length} annotation(s)`,
105+
);
106+
}
107+
}
108+
109+
// Save the source PDF before flattening (so users can see the original)
110+
const sourceBytes = await sourcePdf.save();
111+
const sourcePath = await saveOutput("02-pages/mixed-sizes-source.pdf", sourceBytes);
112+
113+
console.log(`\nSaved source: ${sourcePath} (${formatBytes(sourceBytes.length)})`);
114+
115+
// ============================================================
116+
// Step 2: Flatten annotations (bake them into page content)
117+
// ============================================================
118+
console.log("\n=== Flattening Annotations ===\n");
119+
120+
// Flatten annotations so they become part of the page content.
121+
// This is necessary because embedPage/drawPage creates a Form XObject
122+
// which only captures the content stream, not annotations.
123+
//
124+
// Note: Some annotation types like sticky notes (Text) may not flatten
125+
// if they don't have a visual appearance. Text markup annotations
126+
// (Highlight, Underline, etc.) flatten into colored rectangles/lines.
127+
const flattenedCount = sourcePdf.flattenAnnotations();
128+
129+
console.log(`Flattened ${flattenedCount} annotation(s) into page content`);
130+
131+
// ============================================================
132+
// Step 3: Create destination PDF and resize pages to A4
133+
// ============================================================
134+
console.log("\n=== Resizing All Pages to A4 ===\n");
135+
136+
const targetSize = PageSizes.A4;
137+
const destPdf = PDF.create();
138+
139+
for (let i = 0; i < sourcePdf.getPageCount(); i++) {
140+
const srcPage = sourcePdf.getPage(i);
141+
142+
if (!srcPage) {
143+
continue;
144+
}
145+
146+
// Create a new A4 page
147+
destPdf.addPage({ width: targetSize.width, height: targetSize.height });
148+
const destPage = destPdf.getPage(i);
149+
150+
if (!destPage) {
151+
continue;
152+
}
153+
154+
// Embed the source page as a Form XObject
155+
const embedded = await destPdf.embedPage(sourcePdf, i);
156+
157+
// Calculate scaling to fit within A4 while maintaining aspect ratio
158+
const scaleX = targetSize.width / embedded.width;
159+
const scaleY = targetSize.height / embedded.height;
160+
161+
const scale = Math.min(scaleX, scaleY); // Fit within bounds
162+
163+
// Calculate position to center the page
164+
const scaledWidth = embedded.width * scale;
165+
const scaledHeight = embedded.height * scale;
166+
167+
const x = (targetSize.width - scaledWidth) / 2;
168+
const y = (targetSize.height - scaledHeight) / 2;
169+
170+
// Draw the embedded page scaled and centered
171+
destPage.drawPage(embedded, { x, y, scale });
172+
173+
// Add a subtle border to show the original page bounds
174+
destPage.drawRectangle({
175+
x,
176+
y,
177+
width: scaledWidth,
178+
height: scaledHeight,
179+
borderColor: rgb(0.8, 0.8, 0.8),
180+
borderWidth: 0.5,
181+
});
182+
183+
console.log(
184+
` Page ${i + 1}: ${srcPage.width.toFixed(0)}x${srcPage.height.toFixed(0)} -> A4 (scale: ${(scale * 100).toFixed(1)}%)`,
185+
);
186+
}
187+
188+
// ============================================================
189+
// Step 4: Save the resized PDF
190+
// ============================================================
191+
console.log("\n=== Saving Result ===\n");
192+
193+
const outputBytes = await destPdf.save();
194+
const outputPath = await saveOutput("02-pages/resized-to-a4.pdf", outputBytes);
195+
196+
console.log(`Output: ${outputPath}`);
197+
console.log(`Size: ${formatBytes(outputBytes.length)}`);
198+
console.log(`Pages: ${destPdf.getPageCount()} (all A4)`);
199+
200+
// ============================================================
201+
// Bonus: Demonstrate "fit to width" vs "fit to page"
202+
// ============================================================
203+
console.log("\n=== Bonus: Fit to Width (Crop Height) ===\n");
204+
205+
const fitWidthPdf = PDF.create();
206+
207+
for (let i = 0; i < sourcePdf.getPageCount(); i++) {
208+
const srcPage = sourcePdf.getPage(i);
209+
210+
if (!srcPage) {
211+
continue;
212+
}
213+
214+
fitWidthPdf.addPage({ width: targetSize.width, height: targetSize.height });
215+
const destPage = fitWidthPdf.getPage(i);
216+
217+
if (!destPage) {
218+
continue;
219+
}
220+
221+
const embedded = await fitWidthPdf.embedPage(sourcePdf, i);
222+
223+
// Scale to fit width exactly (may crop top/bottom or leave blank)
224+
const scale = targetSize.width / embedded.width;
225+
const scaledHeight = embedded.height * scale;
226+
227+
// Position at bottom, content may extend above page
228+
const y = 0;
229+
230+
destPage.drawPage(embedded, {
231+
x: 0,
232+
y,
233+
width: targetSize.width, // Fit to width exactly
234+
});
235+
236+
const status = scaledHeight > targetSize.height ? "(cropped)" : "(with margins)";
237+
238+
console.log(` Page ${i + 1}: scaled to ${(scale * 100).toFixed(1)}% ${status}`);
239+
}
240+
241+
const fitWidthBytes = await fitWidthPdf.save();
242+
const fitWidthPath = await saveOutput("02-pages/resized-fit-width.pdf", fitWidthBytes);
243+
244+
console.log(`\nOutput: ${fitWidthPath}`);
245+
console.log(`Size: ${formatBytes(fitWidthBytes.length)}`);
246+
247+
// ============================================================
248+
// Summary
249+
// ============================================================
250+
console.log("\n=== Summary ===\n");
251+
console.log("To resize pages with annotations:");
252+
console.log("1. Flatten annotations first (pdf.flattenAnnotations())");
253+
console.log("2. Embed each page as XObject (pdf.embedPage())");
254+
console.log("3. Draw onto new page with scaling (page.drawPage())");
255+
console.log();
256+
console.log("Scaling strategies:");
257+
console.log("- Fit to page: scale = Math.min(targetW/srcW, targetH/srcH)");
258+
console.log("- Fit to width: scale = targetWidth / srcWidth");
259+
console.log("- Fit to height: scale = targetHeight / srcHeight");
260+
}
261+
262+
main().catch(console.error);

0 commit comments

Comments
 (0)