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.
Files changed (2) hide show
  1. package/package.json +18 -0
  2. 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
+