mirror of
https://github.com/FranP-code/pdf-pages-reducer.git
synced 2025-10-12 23:52:37 +00:00
Enhance PDF manipulation by adding "grid" orientation option and rotation feature for grid pages
This commit is contained in:
201
src/index.ts
201
src/index.ts
@@ -3,8 +3,8 @@ import { PDFDocument, degrees } from "pdf-lib";
|
|||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
|
|
||||||
// Extend the orientation type to include "stacked"
|
// Extend the orientation type to include "stacked" and "grid"
|
||||||
type OrientationType = "horizontal" | "vertical" | "stacked";
|
type OrientationType = "horizontal" | "vertical" | "stacked" | "grid";
|
||||||
|
|
||||||
interface Answers {
|
interface Answers {
|
||||||
operation: string;
|
operation: string;
|
||||||
@@ -13,6 +13,33 @@ interface Answers {
|
|||||||
orientation: OrientationType;
|
orientation: OrientationType;
|
||||||
specifyPaperSize: boolean;
|
specifyPaperSize: boolean;
|
||||||
selectedSize: [number, number];
|
selectedSize: [number, number];
|
||||||
|
rotateGridPages?: boolean; // New property to track if grid pages should be rotated
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------- Utility function to ensure unique filenames --------------
|
||||||
|
function getUniqueFilePath(filePath: string): string {
|
||||||
|
if (!fs.existsSync(filePath)) {
|
||||||
|
return filePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dir = path.dirname(filePath);
|
||||||
|
const ext = path.extname(filePath);
|
||||||
|
const baseName = path.basename(filePath, ext);
|
||||||
|
|
||||||
|
// Check if the filename already ends with a pattern like (1), (2), etc.
|
||||||
|
const match = baseName.match(/^(.*?)(\(\d+\))?$/);
|
||||||
|
const nameWithoutCounter = match ? match[1].trim() : baseName;
|
||||||
|
|
||||||
|
let counter = 1;
|
||||||
|
let newPath = filePath;
|
||||||
|
|
||||||
|
// Keep incrementing counter until we find a filename that doesn't exist
|
||||||
|
while (fs.existsSync(newPath)) {
|
||||||
|
newPath = path.join(dir, `${nameWithoutCounter}(${counter})${ext}`);
|
||||||
|
counter++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return newPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------- Existing duplicate function --------------
|
// -------------- Existing duplicate function --------------
|
||||||
@@ -39,7 +66,8 @@ async function duplicatePages(
|
|||||||
async function combinePages2In1(
|
async function combinePages2In1(
|
||||||
inputPath: string,
|
inputPath: string,
|
||||||
orientation: OrientationType,
|
orientation: OrientationType,
|
||||||
paperSize?: [number, number]
|
paperSize?: [number, number],
|
||||||
|
rotateGridPages?: boolean // New parameter to track if grid pages should be rotated
|
||||||
): Promise<Uint8Array> {
|
): Promise<Uint8Array> {
|
||||||
// 1) Load the input PDF
|
// 1) Load the input PDF
|
||||||
const pdfBytes = fs.readFileSync(inputPath);
|
const pdfBytes = fs.readFileSync(inputPath);
|
||||||
@@ -65,11 +93,11 @@ async function combinePages2In1(
|
|||||||
// Add a blank page in the new PDF
|
// Add a blank page in the new PDF
|
||||||
const newPage = newPdf.addPage([finalWidth, finalHeight]);
|
const newPage = newPdf.addPage([finalWidth, finalHeight]);
|
||||||
|
|
||||||
// Copy the same page twice
|
// Copy the same page twice (or four times for grid)
|
||||||
const [origPage1] = await newPdf.copyPages(originalPdf, [i]);
|
const [origPage1] = await newPdf.copyPages(originalPdf, [i]);
|
||||||
const [origPage2] = await newPdf.copyPages(originalPdf, [i]);
|
const [origPage2] = await newPdf.copyPages(originalPdf, [i]);
|
||||||
|
|
||||||
// Embed them
|
// Embed the pages
|
||||||
const embedded1 = await newPdf.embedPage(origPage1);
|
const embedded1 = await newPdf.embedPage(origPage1);
|
||||||
const embedded2 = await newPdf.embedPage(origPage2);
|
const embedded2 = await newPdf.embedPage(origPage2);
|
||||||
|
|
||||||
@@ -162,6 +190,133 @@ async function combinePages2In1(
|
|||||||
yScale: scale,
|
yScale: scale,
|
||||||
rotate: degrees(90),
|
rotate: degrees(90),
|
||||||
});
|
});
|
||||||
|
} else if (orientation === "grid") {
|
||||||
|
// ---------------------------------------------------
|
||||||
|
// "GRID" = 2x2 grid with 4 copies
|
||||||
|
// ---------------------------------------------------
|
||||||
|
//
|
||||||
|
// ┌─────────┬─────────┐
|
||||||
|
// │ Page1 │ Page2 │
|
||||||
|
// ├─────────┼─────────┤
|
||||||
|
// │ Page3 │ Page4 │
|
||||||
|
// └─────────┴─────────┘
|
||||||
|
//
|
||||||
|
// Each cell is finalWidth/2 wide, finalHeight/2 tall.
|
||||||
|
|
||||||
|
// We need two more copies of the page for the grid layout
|
||||||
|
const [origPage3] = await newPdf.copyPages(originalPdf, [i]);
|
||||||
|
const [origPage4] = await newPdf.copyPages(originalPdf, [i]);
|
||||||
|
|
||||||
|
const embedded3 = await newPdf.embedPage(origPage3);
|
||||||
|
const embedded4 = await newPdf.embedPage(origPage4);
|
||||||
|
|
||||||
|
const slotWidth = finalWidth / 2;
|
||||||
|
const slotHeight = finalHeight / 2;
|
||||||
|
|
||||||
|
if (rotateGridPages) {
|
||||||
|
// When rotated 90 degrees, width and height are swapped for scaling calculation
|
||||||
|
const scale = Math.min(slotWidth / h, slotHeight / w);
|
||||||
|
|
||||||
|
// Calculate offsets with rotated dimensions
|
||||||
|
// Top-left cell (Page1)
|
||||||
|
const offsetX1 = (slotWidth - h * scale) / 2;
|
||||||
|
const offsetY1 = slotHeight + (slotHeight - w * scale) / 2;
|
||||||
|
|
||||||
|
// Top-right cell (Page2)
|
||||||
|
const offsetX2 = slotWidth + (slotWidth - h * scale) / 2;
|
||||||
|
const offsetY2 = offsetY1;
|
||||||
|
|
||||||
|
// Bottom-left cell (Page3)
|
||||||
|
const offsetX3 = offsetX1;
|
||||||
|
const offsetY3 = (slotHeight - w * scale) / 2;
|
||||||
|
|
||||||
|
// Bottom-right cell (Page4)
|
||||||
|
const offsetX4 = offsetX2;
|
||||||
|
const offsetY4 = offsetY3;
|
||||||
|
|
||||||
|
// Draw all four copies with rotation
|
||||||
|
newPage.drawPage(embedded1, {
|
||||||
|
x: offsetX1 + h * scale,
|
||||||
|
y: offsetY1,
|
||||||
|
xScale: scale,
|
||||||
|
yScale: scale,
|
||||||
|
rotate: degrees(90),
|
||||||
|
});
|
||||||
|
|
||||||
|
newPage.drawPage(embedded2, {
|
||||||
|
x: offsetX2 + h * scale,
|
||||||
|
y: offsetY2,
|
||||||
|
xScale: scale,
|
||||||
|
yScale: scale,
|
||||||
|
rotate: degrees(90),
|
||||||
|
});
|
||||||
|
|
||||||
|
newPage.drawPage(embedded3, {
|
||||||
|
x: offsetX3 + h * scale,
|
||||||
|
y: offsetY3,
|
||||||
|
xScale: scale,
|
||||||
|
yScale: scale,
|
||||||
|
rotate: degrees(90),
|
||||||
|
});
|
||||||
|
|
||||||
|
newPage.drawPage(embedded4, {
|
||||||
|
x: offsetX4 + h * scale,
|
||||||
|
y: offsetY4,
|
||||||
|
xScale: scale,
|
||||||
|
yScale: scale,
|
||||||
|
rotate: degrees(90),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// No rotation, standard grid placement
|
||||||
|
const scale = Math.min(slotWidth / w, slotHeight / h);
|
||||||
|
const scaledW = w * scale;
|
||||||
|
const scaledH = h * scale;
|
||||||
|
|
||||||
|
// Calculate offsets for top-left cell (Page1)
|
||||||
|
const offsetX1 = (slotWidth - scaledW) / 2;
|
||||||
|
const offsetY1 = slotHeight + (slotHeight - scaledH) / 2;
|
||||||
|
|
||||||
|
// Calculate offsets for top-right cell (Page2)
|
||||||
|
const offsetX2 = slotWidth + (slotWidth - scaledW) / 2;
|
||||||
|
const offsetY2 = offsetY1;
|
||||||
|
|
||||||
|
// Calculate offsets for bottom-left cell (Page3)
|
||||||
|
const offsetX3 = offsetX1;
|
||||||
|
const offsetY3 = (slotHeight - scaledH) / 2;
|
||||||
|
|
||||||
|
// Calculate offsets for bottom-right cell (Page4)
|
||||||
|
const offsetX4 = offsetX2;
|
||||||
|
const offsetY4 = offsetY3;
|
||||||
|
|
||||||
|
// Draw all four copies without rotation
|
||||||
|
newPage.drawPage(embedded1, {
|
||||||
|
x: offsetX1,
|
||||||
|
y: offsetY1,
|
||||||
|
xScale: scale,
|
||||||
|
yScale: scale,
|
||||||
|
});
|
||||||
|
|
||||||
|
newPage.drawPage(embedded2, {
|
||||||
|
x: offsetX2,
|
||||||
|
y: offsetY2,
|
||||||
|
xScale: scale,
|
||||||
|
yScale: scale,
|
||||||
|
});
|
||||||
|
|
||||||
|
newPage.drawPage(embedded3, {
|
||||||
|
x: offsetX3,
|
||||||
|
y: offsetY3,
|
||||||
|
xScale: scale,
|
||||||
|
yScale: scale,
|
||||||
|
});
|
||||||
|
|
||||||
|
newPage.drawPage(embedded4, {
|
||||||
|
x: offsetX4,
|
||||||
|
y: offsetY4,
|
||||||
|
xScale: scale,
|
||||||
|
yScale: scale,
|
||||||
|
});
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// ---------------------------------------------------
|
// ---------------------------------------------------
|
||||||
// "STACKED" = top & bottom WITHOUT rotation
|
// "STACKED" = top & bottom WITHOUT rotation
|
||||||
@@ -268,9 +423,11 @@ async function main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Output path for duplicated pages
|
// Output path for duplicated pages
|
||||||
const outputPath = path.join(
|
const outputPath = getUniqueFilePath(
|
||||||
path.dirname(pdfPath),
|
path.join(
|
||||||
`${path.basename(pdfPath, ".pdf")}_duplicated.pdf`
|
path.dirname(pdfPath),
|
||||||
|
`${path.basename(pdfPath, ".pdf")}_duplicated.pdf`
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Duplicate
|
// Duplicate
|
||||||
@@ -285,6 +442,7 @@ async function main() {
|
|||||||
// - "vertical" => side by side
|
// - "vertical" => side by side
|
||||||
// - "horizontal" => top/bottom with rotation
|
// - "horizontal" => top/bottom with rotation
|
||||||
// - "stacked" => top/bottom no rotation
|
// - "stacked" => top/bottom no rotation
|
||||||
|
// - "grid" => 2x2 grid with 4 copies
|
||||||
const { orientation } = await inquirer.prompt<
|
const { orientation } = await inquirer.prompt<
|
||||||
Pick<Answers, "orientation">
|
Pick<Answers, "orientation">
|
||||||
>({
|
>({
|
||||||
@@ -295,9 +453,25 @@ async function main() {
|
|||||||
{ name: "Vertical (side-by-side)", value: "vertical" },
|
{ name: "Vertical (side-by-side)", value: "vertical" },
|
||||||
{ name: "Horizontal (top/bottom, rotated)", value: "horizontal" },
|
{ name: "Horizontal (top/bottom, rotated)", value: "horizontal" },
|
||||||
{ name: "Stacked (top/bottom, no rotation)", value: "stacked" },
|
{ name: "Stacked (top/bottom, no rotation)", value: "stacked" },
|
||||||
|
{ name: "Grid (2x2)", value: "grid" },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// If grid layout is selected, ask if user wants to rotate pages 90 degrees
|
||||||
|
let rotateGridPages = false;
|
||||||
|
if (orientation === "grid") {
|
||||||
|
const { shouldRotate } = await inquirer.prompt<{
|
||||||
|
shouldRotate: boolean;
|
||||||
|
}>({
|
||||||
|
type: "confirm",
|
||||||
|
name: "shouldRotate",
|
||||||
|
message:
|
||||||
|
"Would you like to rotate each copy by 90 degrees (horizontally) in the grid?",
|
||||||
|
default: false,
|
||||||
|
});
|
||||||
|
rotateGridPages = shouldRotate;
|
||||||
|
}
|
||||||
|
|
||||||
// 4.2) Ask if user wants to specify a paper size
|
// 4.2) Ask if user wants to specify a paper size
|
||||||
const { specifyPaperSize } = await inquirer.prompt<
|
const { specifyPaperSize } = await inquirer.prompt<
|
||||||
Pick<Answers, "specifyPaperSize">
|
Pick<Answers, "specifyPaperSize">
|
||||||
@@ -328,16 +502,19 @@ async function main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 4.3) Output path
|
// 4.3) Output path
|
||||||
const outputPath = path.join(
|
const outputPath = getUniqueFilePath(
|
||||||
path.dirname(pdfPath),
|
path.join(
|
||||||
`${path.basename(pdfPath, ".pdf")}_2up.pdf`
|
path.dirname(pdfPath),
|
||||||
|
`${path.basename(pdfPath, ".pdf")}_2up.pdf`
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
// 4.4) Generate the 2-up PDF
|
// 4.4) Generate the 2-up PDF
|
||||||
const pdfBytes = await combinePages2In1(
|
const pdfBytes = await combinePages2In1(
|
||||||
pdfPath,
|
pdfPath,
|
||||||
orientation,
|
orientation,
|
||||||
chosenPaperSize
|
chosenPaperSize,
|
||||||
|
rotateGridPages // Pass the rotation preference to the function
|
||||||
);
|
);
|
||||||
fs.writeFileSync(outputPath, pdfBytes);
|
fs.writeFileSync(outputPath, pdfBytes);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user