pdfbooklet 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +18 -0
- package/pdfbooklet.js +200 -0
package/package.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name":"pdfbooklet",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Create booklet from a pdf file suitable for compact printing",
|
|
5
|
+
"main": "pdfbooklet.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"pdfbooklet": "pdfbooklet.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"pdfbooklet.js"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"start": "node pdfbooklet.js"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"pdf-lib": "^1.17.1"
|
|
17
|
+
}
|
|
18
|
+
}
|
package/pdfbooklet.js
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const {PDFDocument, degrees, grayscale} = require('pdf-lib');
|
|
5
|
+
|
|
6
|
+
const INCH = 72;
|
|
7
|
+
const marginInsets = {
|
|
8
|
+
left: 0,
|
|
9
|
+
right: 0,
|
|
10
|
+
top: 0,
|
|
11
|
+
bottom: 0,
|
|
12
|
+
}
|
|
13
|
+
const pageSize = {
|
|
14
|
+
width: 11 * INCH,
|
|
15
|
+
height: 8.5 * INCH,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const err = (msg, exitCode) => {
|
|
19
|
+
console.error(msg);
|
|
20
|
+
process.exit(exitCode ?? 1);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const usage = (msg) => {
|
|
24
|
+
if (msg) console.error(msg);
|
|
25
|
+
console.log(`
|
|
26
|
+
pdfbooklet <input.pdf>
|
|
27
|
+
|
|
28
|
+
OPTIONS:
|
|
29
|
+
--paper-size (a4|letter), default is letter
|
|
30
|
+
--margin-insets X reduce margin of the original pdf on all sides by X (in inches)
|
|
31
|
+
--margin-insets X,Y reduce margin X on top and bottom, Y on left and right
|
|
32
|
+
--margin-insets X,Y,Z,A reduce margin-top by X, right by Y, bottom by Z, left by A
|
|
33
|
+
`)
|
|
34
|
+
process.exit(msg ? 1 : 0)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/*
|
|
38
|
+
* What is a booklet?
|
|
39
|
+
* Suppose we have 4 pages,
|
|
40
|
+
* A B C D
|
|
41
|
+
* Put two page on a sheet,
|
|
42
|
+
*
|
|
43
|
+
* +--------
|
|
44
|
+
* | D | A |
|
|
45
|
+
* | | |
|
|
46
|
+
* +-------+
|
|
47
|
+
*
|
|
48
|
+
* |------+
|
|
49
|
+
* | B C | (rotate 180 degrees)
|
|
50
|
+
* | |
|
|
51
|
+
* +------|
|
|
52
|
+
*
|
|
53
|
+
* So when printed in landscape mode on a duplex printer, one paper will have 4 pages,
|
|
54
|
+
* and when you fold it, it is in the natural order of A B C D.
|
|
55
|
+
*/
|
|
56
|
+
async function createBooklet(inputPdfPath, outputPdfPath) {
|
|
57
|
+
// Read the input PDF
|
|
58
|
+
const existingPdfBytes = fs.readFileSync(inputPdfPath);
|
|
59
|
+
const pdfDoc = await PDFDocument.load(existingPdfBytes);
|
|
60
|
+
const totalPages = pdfDoc.getPageCount();
|
|
61
|
+
|
|
62
|
+
// Ensure the number of pages is a multiple of 4 (for booklet printing)
|
|
63
|
+
let numPagesToAdd = (4 - (totalPages % 4)) % 4; // Calculate how many blank pages are needed
|
|
64
|
+
const adjustedTotalPages = totalPages + numPagesToAdd;
|
|
65
|
+
|
|
66
|
+
console.log(`Total pages in the input PDF: ${totalPages}`);
|
|
67
|
+
if (numPagesToAdd > 0) {
|
|
68
|
+
console.log(`Adding ${numPagesToAdd} blank pages to make it a multiple of 4.`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Create a new PDF for the booklet
|
|
72
|
+
const bookletPdf = await PDFDocument.create();
|
|
73
|
+
|
|
74
|
+
const getEmbedPage = async (i) => {
|
|
75
|
+
if (i < totalPages) {
|
|
76
|
+
const [a] = await bookletPdf.embedPdf(pdfDoc, [i]);
|
|
77
|
+
return a;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
const leftBox = {
|
|
83
|
+
x: 0,
|
|
84
|
+
y: 0, // Draw the first page on the left half
|
|
85
|
+
width: pageSize.width / 2,
|
|
86
|
+
height: pageSize.height,
|
|
87
|
+
}
|
|
88
|
+
const rightBox = {
|
|
89
|
+
x: pageSize.width / 2,
|
|
90
|
+
y: 0, // Draw the first page on the left half
|
|
91
|
+
width: pageSize.width / 2,
|
|
92
|
+
height: pageSize.height,
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const drawPageInBox = async (newPage, box, originPageIndex, flip) => {
|
|
96
|
+
const a = await getEmbedPage(originPageIndex)
|
|
97
|
+
if (a) {
|
|
98
|
+
const {width, height} = a.size()
|
|
99
|
+
const w = width - marginInsets.left - marginInsets.right;
|
|
100
|
+
const h = height - marginInsets.top - marginInsets.bottom;
|
|
101
|
+
const xscale = box.width / w;
|
|
102
|
+
const yscale = box.height / h;
|
|
103
|
+
const scale = xscale < yscale ? xscale : yscale;
|
|
104
|
+
const w2 = w * scale;
|
|
105
|
+
const h2 = h * scale;
|
|
106
|
+
|
|
107
|
+
const dx = -marginInsets.left * scale;
|
|
108
|
+
const dy = -marginInsets.top * scale;
|
|
109
|
+
newPage.drawPage(a, {
|
|
110
|
+
x: box.x + (box.width - w2) / 2 + dx,
|
|
111
|
+
y: box.y + (box.height - h2) / 2 + dy,
|
|
112
|
+
xScale: scale,
|
|
113
|
+
yScale: scale,
|
|
114
|
+
})
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Booklet page order calculation
|
|
119
|
+
for (let i = 0; i < adjustedTotalPages; i += 4) {
|
|
120
|
+
const newPage1 = bookletPdf.addPage([pageSize.width, pageSize.height])
|
|
121
|
+
await drawPageInBox(newPage1, leftBox, i + 3)
|
|
122
|
+
await drawPageInBox(newPage1, rightBox, i)
|
|
123
|
+
const newPage2 = bookletPdf.addPage([pageSize.width, pageSize.height])
|
|
124
|
+
await drawPageInBox(newPage2, rightBox, i + 2,)
|
|
125
|
+
await drawPageInBox(newPage2, leftBox, i + 1,)
|
|
126
|
+
newPage2.setRotation(degrees(180))
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Write the final booklet PDF to a file
|
|
130
|
+
const pdfBytes = await bookletPdf.save();
|
|
131
|
+
fs.writeFileSync(outputPdfPath, pdfBytes);
|
|
132
|
+
console.log(`Booklet PDF created: ${outputPdfPath} pages=${totalPages}`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
let inputPdfPath;
|
|
136
|
+
|
|
137
|
+
for (let i = 2; i < process.argv.length; i++) {
|
|
138
|
+
switch (process.argv[i]) {
|
|
139
|
+
case '--paper-size':
|
|
140
|
+
switch (process.argv[++i]) {
|
|
141
|
+
case 'a4':
|
|
142
|
+
pageSize.width = 11.7 * INCH;
|
|
143
|
+
pageSize.height = 8.3 * INCH;
|
|
144
|
+
break;
|
|
145
|
+
case 'letter':
|
|
146
|
+
pageSize.width = 11 * INCH;
|
|
147
|
+
pageSize.height = 8.5 * INCH;
|
|
148
|
+
break;
|
|
149
|
+
|
|
150
|
+
default:
|
|
151
|
+
err("Unknown paper size")
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
break;
|
|
155
|
+
case '--margin-inset':
|
|
156
|
+
// follow css convention
|
|
157
|
+
const insets = process.argv[++i].split(',').map(x => parseFloat(x) * INCH);
|
|
158
|
+
if (insets.length === 1) {
|
|
159
|
+
marginInsets.left = insets[0];
|
|
160
|
+
marginInsets.right = insets[0];
|
|
161
|
+
marginInsets.bottom = insets[0];
|
|
162
|
+
marginInsets.top = insets[0];
|
|
163
|
+
} else if (insets.length === 2) {
|
|
164
|
+
marginInsets.bottom = insets[0];
|
|
165
|
+
marginInsets.top = insets[0];
|
|
166
|
+
marginInsets.left = insets[1];
|
|
167
|
+
marginInsets.right = insets[1];
|
|
168
|
+
} else if (insets.length === 4) {
|
|
169
|
+
marginInsets.top = insets[0];
|
|
170
|
+
marginInsets.right = insets[1];
|
|
171
|
+
marginInsets.bottom = insets[2];
|
|
172
|
+
marginInsets.left = insets[3];
|
|
173
|
+
} else {
|
|
174
|
+
err("Invalid insets");
|
|
175
|
+
}
|
|
176
|
+
break;
|
|
177
|
+
case '--help':
|
|
178
|
+
usage();
|
|
179
|
+
break;
|
|
180
|
+
default:
|
|
181
|
+
inputPdfPath = process.argv[i];
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (!inputPdfPath) {
|
|
187
|
+
usage("Missing input file");
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (!fs.existsSync(inputPdfPath)) {
|
|
191
|
+
err(`File not found: ${inputPdfPath}`)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Automatically generate the output filename by appending '-booklet' before '.pdf'
|
|
195
|
+
const outputPdfPath = inputPdfPath.replace(/\.pdf$/i, '-booklet.pdf');
|
|
196
|
+
// Usage example
|
|
197
|
+
createBooklet(inputPdfPath, outputPdfPath).catch(err => {
|
|
198
|
+
console.error('Error creating booklet:', err);
|
|
199
|
+
});
|
|
200
|
+
|