pdfbooklet 1.0.3 → 1.0.5
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/README.md +12 -5
- package/package.json +1 -1
- package/pdfbooklet.js +168 -136
package/README.md
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
# Generate booklet format of a PDF file for printing
|
|
2
|
+
|
|
3
|
+
Save paper, save trees and save lives.
|
|
4
|
+
|
|
1
5
|
To install
|
|
2
6
|
|
|
3
7
|
```
|
|
@@ -7,11 +11,14 @@ npm i -g pdfbooklet
|
|
|
7
11
|
To use
|
|
8
12
|
|
|
9
13
|
```
|
|
10
|
-
pdfbooklet <
|
|
14
|
+
pdfbooklet [options] <INPUT-FILE.pdf>
|
|
11
15
|
|
|
12
16
|
OPTIONS:
|
|
13
|
-
--
|
|
14
|
-
--margin-inset X reduce margin
|
|
15
|
-
--margin-inset
|
|
16
|
-
--
|
|
17
|
+
--margin-inset X reduce margin of the original pdf on all sides by X inches
|
|
18
|
+
--margin-inset X,Y reduce margin X on top and bottom, Y on left and right, in inches
|
|
19
|
+
--margin-inset T,R,B,L reduce margin-top by T, right by R, bottom by B, left by L, in inches
|
|
20
|
+
--output FILENAME default is INPUT_FILE-booklet.pdf.
|
|
21
|
+
--paper-size (a4|letter) default is letter
|
|
22
|
+
--range X-Y,+N,A-B,... select page X to Y from input, insert N blank pages,
|
|
23
|
+
then from page A to B
|
|
17
24
|
```
|
package/package.json
CHANGED
package/pdfbooklet.js
CHANGED
|
@@ -5,33 +5,42 @@ const {PDFDocument, degrees, grayscale} = require('pdf-lib');
|
|
|
5
5
|
|
|
6
6
|
const INCH = 72;
|
|
7
7
|
const marginInsets = {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
left: 0,
|
|
9
|
+
right: 0,
|
|
10
|
+
top: 0,
|
|
11
|
+
bottom: 0,
|
|
12
12
|
}
|
|
13
13
|
const pageSize = {
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
width: 11 * INCH,
|
|
15
|
+
height: 8.5 * INCH,
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
// a list of 0-based indexes to the original pdf file,
|
|
19
|
+
// -1 means blank page.
|
|
20
|
+
let inputPageNumbers = []
|
|
21
|
+
let inputPdfPath = undefined
|
|
22
|
+
let outputPdfPath = undefined
|
|
23
|
+
|
|
18
24
|
const err = (msg, exitCode) => {
|
|
19
|
-
|
|
20
|
-
|
|
25
|
+
console.error(msg);
|
|
26
|
+
process.exit(exitCode ?? 1);
|
|
21
27
|
}
|
|
22
28
|
|
|
23
29
|
const usage = (msg) => {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
pdfbooklet <
|
|
30
|
+
if (msg) console.error(msg);
|
|
31
|
+
console.log(`
|
|
32
|
+
pdfbooklet [options] <INPUT-FILE.pdf>
|
|
27
33
|
|
|
28
34
|
OPTIONS:
|
|
29
|
-
--
|
|
30
|
-
--margin-inset X
|
|
31
|
-
--margin-inset
|
|
32
|
-
--
|
|
35
|
+
--margin-inset X reduce margin of the original pdf on all sides by X inches
|
|
36
|
+
--margin-inset X,Y reduce margin X on top and bottom, Y on left and right, in inches
|
|
37
|
+
--margin-inset T,R,B,L reduce margin-top by T, right by R, bottom by B, left by L, in inches
|
|
38
|
+
--output FILENAME default is INPUT_FILE-booklet.pdf.
|
|
39
|
+
--paper-size (a4|letter) default is letter
|
|
40
|
+
--range X-Y,+N,A-B,... select page X to Y from input, insert N blank pages,
|
|
41
|
+
then from page A to B
|
|
33
42
|
`)
|
|
34
|
-
|
|
43
|
+
process.exit(msg ? 1 : 0)
|
|
35
44
|
}
|
|
36
45
|
|
|
37
46
|
/*
|
|
@@ -54,143 +63,166 @@ OPTIONS:
|
|
|
54
63
|
* and when you fold it, it is in the natural order of A B C D.
|
|
55
64
|
*/
|
|
56
65
|
async function createBooklet(inputPdfPath, outputPdfPath) {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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 x = box.width / 2 + box.x - w * scale / 2 - marginInsets.left * scale;
|
|
105
|
-
const y = box.height / 2 + box.y - h * scale / 2 - marginInsets.bottom * scale;
|
|
106
|
-
newPage.drawPage(a, {
|
|
107
|
-
x, y,
|
|
108
|
-
xScale: scale,
|
|
109
|
-
yScale: scale,
|
|
110
|
-
})
|
|
111
|
-
}
|
|
66
|
+
// Read the input PDF
|
|
67
|
+
const existingPdfBytes = fs.readFileSync(inputPdfPath);
|
|
68
|
+
const pdfDoc = await PDFDocument.load(existingPdfBytes);
|
|
69
|
+
const totalPages = pdfDoc.getPageCount();
|
|
70
|
+
|
|
71
|
+
// If no page numbers specified, use the whole range
|
|
72
|
+
const pageNumbers = inputPageNumbers || Array.from({length: totalPages}, (_, i) => i);
|
|
73
|
+
// Align the numbers to a multiple of 4, use blank page for padding
|
|
74
|
+
while (pageNumbers.length % 4 !== 0) {
|
|
75
|
+
pageNumbers.push(-1);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
console.log(`Total pages in the input PDF: ${totalPages}`);
|
|
79
|
+
|
|
80
|
+
// Create a new PDF for the booklet
|
|
81
|
+
const bookletPdf = await PDFDocument.create();
|
|
82
|
+
|
|
83
|
+
const getEmbedPage = async (i) => {
|
|
84
|
+
if (i < totalPages) {
|
|
85
|
+
const [a] = await bookletPdf.embedPdf(pdfDoc, [i]);
|
|
86
|
+
return a;
|
|
112
87
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const leftBox = {
|
|
91
|
+
x: 0,
|
|
92
|
+
y: 0, // Draw the first page on the left half
|
|
93
|
+
width: pageSize.width / 2,
|
|
94
|
+
height: pageSize.height,
|
|
95
|
+
}
|
|
96
|
+
const rightBox = {
|
|
97
|
+
x: pageSize.width / 2,
|
|
98
|
+
y: 0, // Draw the first page on the left half
|
|
99
|
+
width: pageSize.width / 2,
|
|
100
|
+
height: pageSize.height,
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const drawPageInBox = async (newPage, box, originPageIndex, flip) => {
|
|
104
|
+
if (originPageIndex < 0) return;
|
|
105
|
+
const a = await getEmbedPage(originPageIndex)
|
|
106
|
+
if (a) {
|
|
107
|
+
const {width, height} = a.size()
|
|
108
|
+
const w = width - marginInsets.left - marginInsets.right;
|
|
109
|
+
const h = height - marginInsets.top - marginInsets.bottom;
|
|
110
|
+
const xScale = box.width / w;
|
|
111
|
+
const yScale = box.height / h;
|
|
112
|
+
const scale = xScale < yScale ? xScale : yScale;
|
|
113
|
+
const x = box.width / 2 + box.x - w * scale / 2 - marginInsets.left * scale;
|
|
114
|
+
const y = box.height / 2 + box.y - h * scale / 2 - marginInsets.bottom * scale;
|
|
115
|
+
newPage.drawPage(a, {
|
|
116
|
+
x, y,
|
|
117
|
+
xScale: scale,
|
|
118
|
+
yScale: scale,
|
|
119
|
+
})
|
|
123
120
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Booklet page order calculation
|
|
124
|
+
for (let i = 0; i < pageNumbers.length; i += 4) {
|
|
125
|
+
const newPage1 = bookletPdf.addPage([pageSize.width, pageSize.height])
|
|
126
|
+
await drawPageInBox(newPage1, leftBox, pageNumbers[i + 3])
|
|
127
|
+
await drawPageInBox(newPage1, rightBox, pageNumbers[i])
|
|
128
|
+
const newPage2 = bookletPdf.addPage([pageSize.width, pageSize.height])
|
|
129
|
+
await drawPageInBox(newPage2, rightBox, pageNumbers[i + 2])
|
|
130
|
+
await drawPageInBox(newPage2, leftBox, pageNumbers[i + 1])
|
|
131
|
+
newPage2.setRotation(degrees(180))
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Write the final booklet PDF to a file
|
|
135
|
+
const pdfBytes = await bookletPdf.save();
|
|
136
|
+
fs.writeFileSync(outputPdfPath, pdfBytes);
|
|
137
|
+
console.log(`Booklet PDF created: ${outputPdfPath}, ${pageNumbers.length/2} pages`);
|
|
138
|
+
console.log(`Please print it in duplex mode.`);
|
|
129
139
|
}
|
|
130
140
|
|
|
131
|
-
let inputPdfPath;
|
|
132
141
|
|
|
133
142
|
for (let i = 2; i < process.argv.length; i++) {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
marginInsets.bottom = insets[2];
|
|
168
|
-
marginInsets.left = insets[3];
|
|
169
|
-
} else {
|
|
170
|
-
err("Invalid insets");
|
|
171
|
-
}
|
|
172
|
-
break;
|
|
173
|
-
case '--help':
|
|
174
|
-
usage();
|
|
175
|
-
break;
|
|
143
|
+
switch (process.argv[i]) {
|
|
144
|
+
case '--output':
|
|
145
|
+
outputPdfPath = process.argv[++i];
|
|
146
|
+
break;
|
|
147
|
+
case '--range':
|
|
148
|
+
inputPageNumbers = process.argv[++i].split(',').map(x => {
|
|
149
|
+
const m = x.match(/^\+(\d+)$/);
|
|
150
|
+
if (m) {
|
|
151
|
+
return Array(parseInt(m[1])).fill(0)
|
|
152
|
+
}
|
|
153
|
+
if (x.match(/^\d+$/)) {
|
|
154
|
+
return parseInt(x);
|
|
155
|
+
}
|
|
156
|
+
const m2 = x.match(/^(\d+)-(\d+)$/);
|
|
157
|
+
if (!m2) {
|
|
158
|
+
throw new Error(`Bad range: ${x}`)
|
|
159
|
+
}
|
|
160
|
+
const begin = parseInt(m2[1]);
|
|
161
|
+
const end = parseInt(m2[2]);
|
|
162
|
+
return Array.from({length: end - begin + 1}, (v, i) => begin + i)
|
|
163
|
+
}).flat().map(x => x - 1); // convert to 0 based index
|
|
164
|
+
break;
|
|
165
|
+
case '--paper-size':
|
|
166
|
+
switch (process.argv[++i]) {
|
|
167
|
+
case 'a4':
|
|
168
|
+
pageSize.width = 11.7 * INCH;
|
|
169
|
+
pageSize.height = 8.3 * INCH;
|
|
170
|
+
break;
|
|
171
|
+
case 'letter':
|
|
172
|
+
pageSize.width = 11 * INCH;
|
|
173
|
+
pageSize.height = 8.5 * INCH;
|
|
174
|
+
break;
|
|
175
|
+
|
|
176
176
|
default:
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
177
|
+
err("Unknown paper size")
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
break;
|
|
181
|
+
case '--margin-inset':
|
|
182
|
+
// follow css convention
|
|
183
|
+
const insets = process.argv[++i].split(',')
|
|
184
|
+
.map(x => parseFloat(x) * INCH);
|
|
185
|
+
if (insets.length === 1) {
|
|
186
|
+
marginInsets.left = insets[0];
|
|
187
|
+
marginInsets.right = insets[0];
|
|
188
|
+
marginInsets.bottom = insets[0];
|
|
189
|
+
marginInsets.top = insets[0];
|
|
190
|
+
} else if (insets.length === 2) {
|
|
191
|
+
marginInsets.top = insets[0];
|
|
192
|
+
marginInsets.bottom = insets[0];
|
|
193
|
+
marginInsets.left = insets[1];
|
|
194
|
+
marginInsets.right = insets[1];
|
|
195
|
+
} else if (insets.length === 4) {
|
|
196
|
+
marginInsets.top = insets[0];
|
|
197
|
+
marginInsets.right = insets[1];
|
|
198
|
+
marginInsets.bottom = insets[2];
|
|
199
|
+
marginInsets.left = insets[3];
|
|
200
|
+
} else {
|
|
201
|
+
err("Invalid insets");
|
|
202
|
+
}
|
|
203
|
+
break;
|
|
204
|
+
case '--help':
|
|
205
|
+
usage();
|
|
206
|
+
break;
|
|
207
|
+
default:
|
|
208
|
+
inputPdfPath = process.argv[i];
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
180
211
|
}
|
|
181
212
|
|
|
182
213
|
if (!inputPdfPath) {
|
|
183
|
-
|
|
214
|
+
usage("Missing input file");
|
|
184
215
|
}
|
|
185
216
|
|
|
186
217
|
if (!fs.existsSync(inputPdfPath)) {
|
|
187
|
-
|
|
218
|
+
err(`File not found: ${inputPdfPath}`)
|
|
188
219
|
}
|
|
189
220
|
|
|
190
221
|
// Automatically generate the output filename by appending '-booklet' before '.pdf'
|
|
191
|
-
|
|
222
|
+
outputPdfPath ||= inputPdfPath.replace(/\.pdf$/i, '-booklet.pdf');
|
|
223
|
+
|
|
192
224
|
// Usage example
|
|
193
225
|
createBooklet(inputPdfPath, outputPdfPath).catch(err => {
|
|
194
|
-
|
|
226
|
+
console.error('Error creating booklet:', err);
|
|
195
227
|
});
|
|
196
228
|
|