1// resize-pdf-pages.js. Change PDF page dimensions (A4/Letter/etc or custom), 2// scaling content to fit. Rotation-proof approach: each selected page is 3// embedded as a form XObject and redrawn onto a fresh page of the target size, 4// so /Rotate flags, offset media boxes, and crop boxes are all handled. 5import { 6 allPages, 7 clamp, 8 exportPdfDocument, 9 loadPdfSource, 10 parsePageSequence, 11} from "./_shared.js?v=1787810615&runtime=bf3904c1cd16"; 12 13const PRESETS = { 14 a4: [595.28, 841.89], 15 letter: [612, 792], 16 legal: [612, 1008], 17 a5: [419.53, 595.28], 18 a3: [841.89, 1190.55], 19 square: [595.28, 595.28], 20}; 21 22const MIN_DIMENSION = 72; 23const MAX_DIMENSION = 14400; 24 25// cos/sin of -rotation (drawPage's rotate is counter-clockwise positive, while 26// the PDF /Rotate attribute is clockwise). Exact values avoid float fuzz. 27const ROTATION_TRIG = { 28 0: { cos: 1, sin: 0 }, 29 90: { cos: 0, sin: -1 }, 30 180: { cos: -1, sin: 0 }, 31 270: { cos: 0, sin: 1 }, 32}; 33 34function normalizeRotation(angle) { 35 const wrapped = ((Math.round(Number(angle) || 0) % 360) + 360) % 360; 36 // /Rotate must be a multiple of 90; snap defensively for malformed files. 37 return (Math.round(wrapped / 90) * 90) % 360; 38} 39 40function parseDimension(raw, fallback) { 41 const value = Number.parseFloat(raw); 42 return clamp(Number.isFinite(value) ? value : fallback, MIN_DIMENSION, MAX_DIMENSION); 43} 44 45// Visible area of the page: crop box intersected with media box (viewers do 46// the same); falls back to the media box when the intersection is degenerate. 47function visibleBox(page) { 48 const media = page.getMediaBox(); 49 const crop = page.getCropBox(); 50 const left = Math.max(media.x, crop.x); 51 const bottom = Math.max(media.y, crop.y); 52 const right = Math.min(media.x + media.width, crop.x + crop.width); 53 const top = Math.min(media.y + media.height, crop.y + crop.height); 54 if (right - left > 0 && top - bottom > 0) { 55 return { left, bottom, right, top }; 56 } 57 return { left: media.x, bottom: media.y, right: media.x + media.width, top: media.y + media.height }; 58} 59 60export async function processResizePdfPages(sourceUrl, settings = {}) { 61 const { PDFLib, doc, totalPages, nameBase } = await loadPdfSource(sourceUrl, settings); 62 if (!totalPages) { 63 throw new Error("The PDF has no pages to resize."); 64 } 65 66 const presetKey = String(settings.preset || "a4").toLowerCase(); 67 let baseWidth; 68 let baseHeight; 69 if (presetKey === "custom") { 70 baseWidth = parseDimension(settings.customWidth, 595); 71 baseHeight = parseDimension(settings.customHeight, 842); 72 } else { 73 [baseWidth, baseHeight] = PRESETS[presetKey] || PRESETS.a4; 74 } 75 76 const orientation = String(settings.orientation || "auto").toLowerCase(); 77 const fitMode = String(settings.fitMode || "fit").toLowerCase(); 78 const resizeSet = new Set( 79 settings.pageRange 80 ? parsePageSequence(settings.pageRange, totalPages, { preserveOrder: false }) 81 : allPages(totalPages), 82 ); 83 84 const out = await PDFLib.PDFDocument.create(); 85 for (const pageNumber of allPages(totalPages)) { 86 if (!resizeSet.has(pageNumber)) { 87 const [copied] = await out.copyPages(doc, [pageNumber - 1]); 88 out.addPage(copied); 89 continue; 90 } 91 92 const sourcePage = doc.getPage(pageNumber - 1); 93 const box = visibleBox(sourcePage); 94 const sourceWidth = box.right - box.left; 95 const sourceHeight = box.top - box.bottom; 96 if (!(sourceWidth > 0) || !(sourceHeight > 0)) { 97 throw new Error(`Page ${pageNumber} has an invalid size and cannot be resized.`); 98 } 99 100 const rotation = normalizeRotation(sourcePage.getRotation().angle); 101 const swapped = rotation === 90 || rotation === 270; 102 // Size of the page as a viewer displays it (after /Rotate). 103 const displayWidth = swapped ? sourceHeight : sourceWidth; 104 const displayHeight = swapped ? sourceWidth : sourceHeight; 105 106 let targetWidth = baseWidth; 107 let targetHeight = baseHeight; 108 const wantLandscape = 109 orientation === "landscape" || 110 (orientation !== "portrait" && displayWidth > displayHeight); 111 if (targetWidth !== targetHeight) { 112 targetWidth = wantLandscape ? Math.max(baseWidth, baseHeight) : Math.min(baseWidth, baseHeight); 113 targetHeight = wantLandscape ? Math.min(baseWidth, baseHeight) : Math.max(baseWidth, baseHeight); 114 } 115 116 let displayScaleX; 117 let displayScaleY; 118 if (fitMode === "stretch") { 119 displayScaleX = targetWidth / displayWidth; 120 displayScaleY = targetHeight / displayHeight; 121 } else { 122 const pick = fitMode === "fill" ? Math.max : Math.min; 123 displayScaleX = displayScaleY = pick(targetWidth / displayWidth, targetHeight / displayHeight); 124 } 125 // drawPage scales along the embedded page's own axes (pre-rotation); a 126 // 90/270 rotation maps the source x-axis to the display's vertical axis. 127 const xScale = swapped ? displayScaleY : displayScaleX; 128 const yScale = swapped ? displayScaleX : displayScaleY; 129 130 // The transform anchor is drawPage's (x, y): translate, then rotate, then
131 // scale. Find the drawn bounding rect relative to the anchor from the four 132 // transformed corners, then center it on the target page. 133 const { cos, sin } = ROTATION_TRIG[rotation]; 134 let minX = Infinity; 135 let minY = Infinity; 136 for (const [px, py] of [[0, 0], [sourceWidth, 0], [0, sourceHeight], [sourceWidth, sourceHeight]]) { 137 minX = Math.min(minX, xScale * px * cos - yScale * py * sin); 138 minY = Math.min(minY, xScale * px * sin + yScale * py * cos); 139 } 140 141 const embedded = await out.embedPage(sourcePage, box); 142 const target = out.addPage([targetWidth, targetHeight]); 143 target.drawPage(embedded, { 144 x: (targetWidth - displayScaleX * displayWidth) / 2 - minX, 145 y: (targetHeight - displayScaleY * displayHeight) / 2 - minY, 146 xScale, 147 yScale, 148 rotate: PDFLib.degrees(-rotation), 149 }); 150 } 151 152 return exportPdfDocument(out, `${nameBase}-resized.pdf`); 153}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.