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.
Files changed (3) hide show
  1. package/README.md +12 -5
  2. package/package.json +1 -1
  3. 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 <input.pdf>
14
+ pdfbooklet [options] <INPUT-FILE.pdf>
11
15
 
12
16
  OPTIONS:
13
- --paper-size (a4|letter), default is letter
14
- --margin-inset X reduce margin of the original pdf on all sides by X (in inches)
15
- --margin-inset X,Y reduce margin X on top and bottom, Y on left and right
16
- --margin-inset X,Y,Z,A reduce margin-top by X, right by Y, bottom by Z, left by A
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pdfbooklet",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "Create booklet from a pdf file suitable for compact printing",
5
5
  "main": "pdfbooklet.js",
6
6
  "bin": {
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
- left: 0,
9
- right: 0,
10
- top: 0,
11
- bottom: 0,
8
+ left: 0,
9
+ right: 0,
10
+ top: 0,
11
+ bottom: 0,
12
12
  }
13
13
  const pageSize = {
14
- width: 11 * INCH,
15
- height: 8.5 * INCH,
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
- console.error(msg);
20
- process.exit(exitCode ?? 1);
25
+ console.error(msg);
26
+ process.exit(exitCode ?? 1);
21
27
  }
22
28
 
23
29
  const usage = (msg) => {
24
- if (msg) console.error(msg);
25
- console.log(`
26
- pdfbooklet <input.pdf>
30
+ if (msg) console.error(msg);
31
+ console.log(`
32
+ pdfbooklet [options] <INPUT-FILE.pdf>
27
33
 
28
34
  OPTIONS:
29
- --paper-size (a4|letter), default is letter
30
- --margin-inset X reduce margin of the original pdf on all sides by X (in inches)
31
- --margin-inset X,Y reduce margin X on top and bottom, Y on left and right
32
- --margin-inset X,Y,Z,A reduce margin-top by X, right by Y, bottom by Z, left by A
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
- process.exit(msg ? 1 : 0)
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
- // 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 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
- // Booklet page order calculation
115
- for (let i = 0; i < adjustedTotalPages; i += 4) {
116
- const newPage1 = bookletPdf.addPage([pageSize.width, pageSize.height])
117
- await drawPageInBox(newPage1, leftBox, i + 3)
118
- await drawPageInBox(newPage1, rightBox, i)
119
- const newPage2 = bookletPdf.addPage([pageSize.width, pageSize.height])
120
- await drawPageInBox(newPage2, rightBox, i + 2,)
121
- await drawPageInBox(newPage2, leftBox, i + 1,)
122
- newPage2.setRotation(degrees(180))
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
- // Write the final booklet PDF to a file
126
- const pdfBytes = await bookletPdf.save();
127
- fs.writeFileSync(outputPdfPath, pdfBytes);
128
- console.log(`Booklet PDF created: ${outputPdfPath} pages=${totalPages}`);
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
- switch (process.argv[i]) {
135
- case '--paper-size':
136
- switch (process.argv[++i]) {
137
- case 'a4':
138
- pageSize.width = 11.7 * INCH;
139
- pageSize.height = 8.3 * INCH;
140
- break;
141
- case 'letter':
142
- pageSize.width = 11 * INCH;
143
- pageSize.height = 8.5 * INCH;
144
- break;
145
-
146
- default:
147
- err("Unknown paper size")
148
- break;
149
- }
150
- break;
151
- case '--margin-inset':
152
- // follow css convention
153
- const insets = process.argv[++i].split(',').map(x => parseFloat(x) * INCH);
154
- if (insets.length === 1) {
155
- marginInsets.left = insets[0];
156
- marginInsets.right = insets[0];
157
- marginInsets.bottom = insets[0];
158
- marginInsets.top = insets[0];
159
- } else if (insets.length === 2) {
160
- marginInsets.top = insets[0];
161
- marginInsets.bottom = insets[0];
162
- marginInsets.left = insets[1];
163
- marginInsets.right = insets[1];
164
- } else if (insets.length === 4) {
165
- marginInsets.top = insets[0];
166
- marginInsets.right = insets[1];
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
- inputPdfPath = process.argv[i];
178
- break;
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
- usage("Missing input file");
214
+ usage("Missing input file");
184
215
  }
185
216
 
186
217
  if (!fs.existsSync(inputPdfPath)) {
187
- err(`File not found: ${inputPdfPath}`)
218
+ err(`File not found: ${inputPdfPath}`)
188
219
  }
189
220
 
190
221
  // Automatically generate the output filename by appending '-booklet' before '.pdf'
191
- const outputPdfPath = inputPdfPath.replace(/\.pdf$/i, '-booklet.pdf');
222
+ outputPdfPath ||= inputPdfPath.replace(/\.pdf$/i, '-booklet.pdf');
223
+
192
224
  // Usage example
193
225
  createBooklet(inputPdfPath, outputPdfPath).catch(err => {
194
- console.error('Error creating booklet:', err);
226
+ console.error('Error creating booklet:', err);
195
227
  });
196
228