pdf-lib-extended 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/README.md ADDED
@@ -0,0 +1,123 @@
1
+ # PDF-lib-Extended
2
+
3
+ PDF-lib-Extended is a JavaScript library that extends the functionality of the popular [pdf-lib](https://github.com/Hopding/pdf-lib) library. This extension adds several useful methods for working with PDF documents, making it easier to manipulate text, images, tables, and other elements within your PDFs.
4
+
5
+ ## Installation
6
+
7
+ To install PDF-lib-Extended, use npm:
8
+
9
+ ```bash
10
+ npm install pdf-lib-extended
11
+ ```
12
+
13
+ ## Usage
14
+ Here’s a basic example of how to use PDF-lib-Extended in your project:
15
+
16
+ ```javascript
17
+ import PDFExtended from 'pdf-lib-extended';
18
+
19
+ async function createPDF() {
20
+ const pdf = new PDFExtended();
21
+ await pdf.init();
22
+
23
+ // Add a new page
24
+ pdf.addNewPage();
25
+
26
+ // Draw some text
27
+ pdf.drawText('Hello, World!', {
28
+ align: 'center',
29
+ size: 24,
30
+ color: pdf.getColor(),
31
+ });
32
+
33
+ // Draw a circle with text inside
34
+ pdf.drawCircleText(100, 100, 'A', {
35
+ size: 12,
36
+ color: pdf.getColor(),
37
+ });
38
+
39
+ // Generate and download the PDF
40
+ const pdfUrl = await pdf.generatePDFURL();
41
+ window.open(pdfUrl);
42
+ }
43
+
44
+ createPDF();
45
+ ```
46
+
47
+ ## Features
48
+ PDF-lib-Extended includes several custom methods to enhance your PDF creation experience:
49
+
50
+ * **Text Handling:**
51
+
52
+ * **`drawText(text, options)`:** Draws text on the current page with alignment, color, and size options.
53
+ * **`drawParagraph(text, options)`:** Automatically wraps text to the next line within specified bounds.
54
+
55
+ * **Shape Drawing:**
56
+
57
+ * **`drawCircleText(x, y, text, options)`:** Draws a circle with text centered inside.
58
+
59
+ * **Tables:**
60
+ * **`drawTable(header, data, options)`:** Draws a table with customizable borders, alignment, and size.
61
+
62
+ * **Images:**
63
+
64
+ * **`addImage(x, y, base64Image, imageScale)`:** Adds an image to the PDF document at specified coordinates with optional scaling.
65
+ * **Watermarks:**
66
+
67
+ * **`drawWatermark(text, size)`:** Draws a watermark on each page of the document.
68
+
69
+ * **Document Management:**
70
+
71
+ * **`addNewPage(dimensions)`:** Adds a new page to the PDF document.
72
+ * **`fetchPDF(url)`:** Fetches an existing PDF from a URL and loads it into the document.
73
+ * **`mergePDF(urls)`:** Merges multiple PDFs from URLs into the current document.
74
+
75
+ * **HTML Parsing:**
76
+
77
+ * **`htmlParser(text, parser, options)`:** Parses HTML and draws it on the PDF.
78
+
79
+ ## API Reference
80
+ **`init()`**
81
+ Initializes the PDF document and embeds fonts.
82
+
83
+ **`setTextSize(number)`**
84
+ Sets the text size for the document.
85
+
86
+ **`setCircleScale(number)`**
87
+ Sets the scale for circles drawn on the document.
88
+
89
+ **`setMargin(arr)`**
90
+ Sets the margins for the document.
91
+
92
+ **`setCurrentPage(page)`**
93
+ Sets the current page of the document.
94
+
95
+ **`setColor(color)`**
96
+ Sets the color for text and shapes.
97
+
98
+ **`getPDF()`**
99
+ Returns the current PDF document instance.
100
+
101
+ **`generatePDFURL()`**
102
+ Generates a URL for downloading the PDF document.
103
+
104
+ **`drawText(text, options)`**
105
+ Draws text on the current page.
106
+
107
+ **`drawCircleText(x, y, text, options)`**
108
+ Draws a circle with text centered inside.
109
+
110
+ **`drawTable(header, data, options)`**
111
+ Draws a table with the specified header and data.
112
+
113
+ **`addImage(x, y, base64Image, imageScale)`**
114
+ Adds an image to the PDF document.
115
+
116
+ **`drawWatermark(text, size)`**
117
+ Draws a watermark on each page of the document.
118
+
119
+ ## Contributing
120
+ If you find a bug or have a feature request, feel free to open an issue or submit a pull request. Contributions are welcome!
121
+
122
+ ## License
123
+ This project is licensed under the MIT License.
package/index.js ADDED
@@ -0,0 +1,3 @@
1
+ import PDFLibExtended from "./src/PDFLibExtended";
2
+
3
+ export default PDFLibExtended;
package/package.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "pdf-lib-extended",
3
+ "version": "1.0.0",
4
+ "description": "This project extends the capabilities of the pdf-lib JavaScript library by providing a set of helper functions that simplify common PDF manipulation tasks. It includes utilities for drawing and formatting text, images, and shapes within PDF documents, allowing for more advanced customization and automation. The class-based architecture, designed as a toolkit, ensures that developers can easily integrate these enhanced features into their existing workflows. With this extension, users can streamline the creation of dynamic and complex PDFs with minimal effort.",
5
+ "main": "index.js",
6
+ "type": "module",
7
+ "scripts": {
8
+ "test": "echo \"Error: no test specified\" && exit 1"
9
+ },
10
+ "author": "JalenLT",
11
+ "license": "ISC",
12
+ "dependencies": {
13
+ "pdf-lib": "^1.17.1"
14
+ }
15
+ }
@@ -0,0 +1,932 @@
1
+ import { degrees, PDFDocument, rgb, StandardFonts } from 'pdf-lib';
2
+
3
+ class PDFLibExtended {
4
+ // Private properties for the PDF document, font, current page, text size, circle scale, and margins
5
+ #pdf;
6
+ #currentFont;
7
+ #font;
8
+ #boldFont;
9
+ #italicFont;
10
+ #italicBoldFont;
11
+ #currentPage;
12
+ #textSize;
13
+ #circleScale;
14
+ #margin;
15
+ #color;
16
+ #currentNodes;
17
+
18
+ constructor() {
19
+ // Initialize the class properties with default values
20
+ this.#pdf = null;
21
+ this.#currentFont = null;
22
+ this.#font = null;
23
+ this.#boldFont = null;
24
+ this.#italicFont = null;
25
+ this.#italicBoldFont = null;
26
+ this.#currentPage = null;
27
+ this.#textSize = 12;
28
+ this.#circleScale = 15;
29
+ this.#color = rgb(0, 0, 0);
30
+ this.#currentNodes = [];
31
+ this.#margin = {
32
+ left: 25,
33
+ right: 25,
34
+ top: 25,
35
+ bottom: 25
36
+ };
37
+ }
38
+
39
+ async init() {
40
+ // Asynchronously create the PDF document and embed the font
41
+ this.#pdf = await PDFDocument.create();
42
+ this.#font = await this.#pdf.embedFont(StandardFonts.Helvetica);
43
+ this.#boldFont = await this.#pdf.embedFont(StandardFonts.HelveticaBold);
44
+ this.#italicFont = await this.#pdf.embedFont(StandardFonts.HelveticaOblique);
45
+ this.#italicBoldFont = await this.#pdf.embedFont(StandardFonts.HelveticaBoldOblique);
46
+ this.#currentFont = this.#font;
47
+ }
48
+
49
+ /**
50
+ *### Adds the provided node to the current nodes
51
+ *
52
+ * @param {string} node
53
+ */
54
+ addNode(node){
55
+ try {
56
+ if(!node) throw new Error("Please enter a valid value");
57
+ this.#currentNodes.push(node);
58
+ } catch (error) {
59
+ console.error("Error in addNodes: ", error);
60
+ }
61
+ }
62
+
63
+ /**
64
+ *### Removes the provided node from the current nodes
65
+ *
66
+ * @param {string} node
67
+ */
68
+ removeNode(node) {
69
+ try {
70
+ if (!node) throw new Error("Please enter a valid value");
71
+
72
+ const lastIndex = this.#currentNodes.lastIndexOf(node);
73
+
74
+ if (lastIndex !== -1) {
75
+ this.#currentNodes.splice(lastIndex, 1);
76
+ }
77
+ } catch (error) {
78
+ console.error("Error in removeNode:", error);
79
+ }
80
+ }
81
+
82
+ /**
83
+ *### Method to set the text size, ensuring it's a valid number greater than 0
84
+ * @param {number} number
85
+ * @returns {boolean}
86
+ */
87
+ setTextSize(number) {
88
+ try {
89
+ if (isNaN(Number(number))) throw new Error("The value supplied is not a number: ", number);
90
+ number = Number(number);
91
+ if (number <= 0) throw new Error("The value provided must be larger than 0");
92
+ this.#textSize = number;
93
+ return true;
94
+ } catch (error) {
95
+ console.error("Error in setTextSize: ", error);
96
+ return false;
97
+ }
98
+ }
99
+
100
+ /**
101
+ *### Method to set the scale of circles, ensuring it's a valid number greater than 0
102
+ *
103
+ * @param {number} number
104
+ * @returns {boolean}
105
+ */
106
+ setCircleScale(number) {
107
+ try {
108
+ if (isNaN(Number(number))) throw new Error("The value supplied is not a number: ", number);
109
+ number = Number(number);
110
+ if (number <= 0) throw new Error("The value provided must be larger than 0");
111
+ this.#circleScale = number;
112
+ return true;
113
+ } catch (error) {
114
+ console.error("Error in setCircleScale: ", error);
115
+ return false;
116
+ }
117
+ }
118
+
119
+ /**
120
+ *### Method to set the margins, ensuring the input is an array
121
+ *
122
+ * @param {object} arr
123
+ * @returns {boolean}
124
+ */
125
+ setMargin(arr) {
126
+ try {
127
+ if (typeof arr != 'object' || arr === null || !Array.isArray(arr)) throw new Error("The value supplied is not a JSON/Array: ", arr);
128
+ this.#margin = {
129
+ ...this.#margin,
130
+ ...arr
131
+ };
132
+ return true;
133
+ } catch (error) {
134
+ console.error("Error in setMargin: ", error);
135
+ return false;
136
+ }
137
+ }
138
+
139
+ /**
140
+ *### Method to set the current page for the PDF document
141
+ *
142
+ * @param {page} page
143
+ * @returns {boolean}
144
+ */
145
+ setCurrentPage(page) {
146
+ try {
147
+ if (!page) throw new Error("Please supply a valid value");
148
+ this.#currentPage = page;
149
+ return true;
150
+ } catch (error) {
151
+ console.error("Error in setCurrentPage: ", error);
152
+ return false;
153
+ }
154
+ }
155
+
156
+ /**
157
+ *### Setter for color
158
+ *
159
+ * @param {import('pdf-lib').RGB} color
160
+ * @returns {boolean}
161
+ */
162
+ setColor(color){
163
+ try {
164
+ if(!color) throw new Error("Please supply a valid color");
165
+ this.#color = color;
166
+ return true;
167
+ } catch (error) {
168
+ console.error("Error in setColor: ", error);
169
+ return false;
170
+ }
171
+ }
172
+
173
+ /**
174
+ *### Getter for the color
175
+ *
176
+ * @returns {import('pdf-lib').RGB}
177
+ */
178
+ getColor(){
179
+ return this.#color;
180
+ }
181
+
182
+ /**
183
+ *### Getter for the current nodes being processed by the HTMLParser
184
+ *
185
+ * @returns {array}
186
+ */
187
+ getCurrentNodes(){
188
+ return this.#currentNodes;
189
+ }
190
+
191
+ /**
192
+ *### Getter for the PDF
193
+ *
194
+ * @returns {PDFDocument}
195
+ */
196
+ getPDF(){
197
+ return this.#pdf;
198
+ }
199
+
200
+ /**
201
+ *### Setter for the font
202
+ *
203
+ * @param {font} font
204
+ */
205
+ setFont(font){
206
+ try {
207
+ if(!font) throw new Error("Please provide a font");
208
+ this.getCurrentPage().setFont(font);
209
+ this.#currentFont = font;
210
+ } catch (error) {
211
+ console.error("Error in setFont: ", error);
212
+ }
213
+ }
214
+
215
+ /**
216
+ *### Getter for the current font
217
+ *
218
+ * @returns {font}
219
+ */
220
+ getCurrentFont(){
221
+ return this.#currentFont;
222
+ }
223
+
224
+ /**
225
+ *### Getter for the font
226
+ *
227
+ * @returns {StandardFonts.Helvetica}
228
+ */
229
+ getFont() {
230
+ return this.#font;
231
+ }
232
+
233
+ /**
234
+ *### Getter for the Bold font
235
+ *
236
+ * @returns {StandardFonts.HelveticaBold}
237
+ */
238
+ getBoldFont(){
239
+ return this.#boldFont;
240
+ }
241
+
242
+ /**
243
+ *### Getter for the Italic font
244
+ *
245
+ * @returns {StandardFonts.HelveticaOblique}
246
+ */
247
+ getItalicFont(){
248
+ return this.#italicFont;
249
+ }
250
+
251
+ /**
252
+ *### Getter for the ItalicBold font
253
+ *
254
+ * @returns {StandardFonts.HelveticaBoldOblique}
255
+ */
256
+ getItalicBoldFont(){
257
+ return this.#italicBoldFont;
258
+ }
259
+
260
+ /**
261
+ *### Getter for the current text size
262
+ * @returns {number}
263
+ */
264
+ getTextSize() {
265
+ return this.#textSize;
266
+ }
267
+
268
+ /**
269
+ *### Getter for the current circle scale
270
+ * @returns {number}
271
+ */
272
+ getCircleScale() {
273
+ return this.#circleScale;
274
+ }
275
+
276
+ /**
277
+ *### Getter for the current page of the PDF document
278
+ * @returns {PDFDocument.page}
279
+ */
280
+ getCurrentPage() {
281
+ return this.#currentPage;
282
+ }
283
+
284
+ /**
285
+ *### Getter for the document margins
286
+ *
287
+ * @returns {array}
288
+ * @example margin = {
289
+ * left: number,
290
+ * right: number,
291
+ * top: number,
292
+ * bottom: number
293
+ * }
294
+ */
295
+ getMargin() {
296
+ return this.#margin;
297
+ }
298
+
299
+ /**
300
+ *### Method to create a new page in the PDF document with optional dimensions
301
+ *
302
+ * @param {array} dimensions
303
+ */
304
+ addNewPage(dimensions = null) {
305
+ if (!dimensions) this.setCurrentPage(this.#pdf.addPage());
306
+ else this.setCurrentPage(this.#pdf.addPage(dimensions));
307
+
308
+ this.getCurrentPage().moveTo(this.getMargin().left, this.getCurrentPage().getHeight() - this.getMargin().top);
309
+ }
310
+
311
+ /**
312
+ *### Method to fetch an existing PDF from a URL and load it into the document
313
+ *
314
+ * @param {string} url
315
+ * @returns {Promise<PDFDocument>|boolean}
316
+ */
317
+ async fetchPDF(url) {
318
+ try {
319
+ if(!url) throw new Error("A URL must be provided");
320
+ const response = await fetch(url);
321
+ const arrayBuffer = await response.arrayBuffer();
322
+ return PDFDocument.load(arrayBuffer);
323
+ } catch (error) {
324
+ console.error("Error in fetchPDF: ", error);
325
+ return false;
326
+ }
327
+ }
328
+
329
+ /**
330
+ *### Fetches PDFs from the provided URLs and appends them to the end of the current document
331
+ * @param {string|array} urls
332
+ * @returns
333
+ */
334
+ async mergePDF(urls) {
335
+ try {
336
+ if (!urls) throw new Error("A valid URL must be provided");
337
+
338
+ const pdfDoc = this.getPDF();
339
+
340
+ const urlArray = typeof urls === "string" ? [urls] : urls;
341
+
342
+ for (const url of urlArray) {
343
+ const fetchedPdf = await this.fetchPDF(url);
344
+ const copiedPages = await pdfDoc.copyPages(fetchedPdf, fetchedPdf.getPageIndices());
345
+
346
+ copiedPages.forEach(page => pdfDoc.addPage(page));
347
+ }
348
+
349
+ const pdfBytes = await pdfDoc.save();
350
+
351
+ return pdfBytes;
352
+
353
+ } catch (error) {
354
+ console.error("Error in mergePDF: ", error);
355
+ }
356
+ }
357
+
358
+ /**
359
+ *### Method to generate a URL for the PDF document
360
+ *
361
+ * @returns {string|boolean}
362
+ */
363
+ async generatePDFURL() {
364
+ try {
365
+ const pdfBytes = await this.getPDF().save();
366
+ const blob = new Blob([pdfBytes], { type: 'application/pdf' });
367
+ return URL.createObjectURL(blob);
368
+ } catch (error) {
369
+ console.error("Error in generatePDFURL: ", error);
370
+ return false;
371
+ }
372
+ }
373
+
374
+ /**
375
+ *### Moves the pointer to the next line of the current page
376
+ */
377
+ nextLine() {
378
+ this.getCurrentPage().moveTo(this.getMargin().left, this.getCurrentPage().getY() - this.getTextSize());
379
+ }
380
+
381
+ /**
382
+ *### Draws text on the current page
383
+ *
384
+ * @param {string} text
385
+ * @param {object} options
386
+ * @param {"left"|"center"|"right"} options.align
387
+ * @param {{left: number, right: number}} options.range - The bounds in which the text is written
388
+ * @param {number} options.size - The size of the text
389
+ * @param {import('pdf-lib').RGB} options.color - The color of the text
390
+ * @param {number} options.opacity - The opacity of the text
391
+ */
392
+ drawText(text, options = {}) {
393
+ let defaultOptions = {
394
+ align: "left",
395
+ range: {
396
+ left: this.getMargin().left,
397
+ right: this.getCurrentPage().getWidth() - this.getMargin().right
398
+ },
399
+ size: this.getTextSize(),
400
+ color: this.getColor(),
401
+ opacity: 1,
402
+ ...options,
403
+ };
404
+
405
+ switch (defaultOptions.align) {
406
+ case "left":
407
+ this.getCurrentPage().drawText(text, {
408
+ size: defaultOptions.size,
409
+ color: defaultOptions.color,
410
+ opacity: defaultOptions.opacity
411
+ });
412
+ break;
413
+
414
+ case "center":
415
+ let xCenterPosition = defaultOptions.range.left +
416
+ ((defaultOptions.range.right - defaultOptions.range.left) / 2) -
417
+ (this.getCurrentFont().widthOfTextAtSize(text, defaultOptions.size) / 2);
418
+
419
+ let currentPositionCenter = {
420
+ x: this.getCurrentPage().getX(),
421
+ y: this.getCurrentPage().getY()
422
+ };
423
+
424
+ this.getCurrentPage().moveTo(xCenterPosition, this.getCurrentPage().getY());
425
+ this.getCurrentPage().drawText(text, {
426
+ size: defaultOptions.size,
427
+ color: defaultOptions.color,
428
+ opacity: defaultOptions.opacity
429
+ });
430
+ this.getCurrentPage().moveTo(currentPositionCenter.x, currentPositionCenter.y);
431
+ break;
432
+
433
+ case "right":
434
+ let textWidth = this.getCurrentFont().widthOfTextAtSize(text, defaultOptions.size);
435
+ let xRightPosition = defaultOptions.range.right - textWidth;
436
+
437
+ let currentPositionRight = {
438
+ x: this.getCurrentPage().getX(),
439
+ y: this.getCurrentPage().getY()
440
+ };
441
+
442
+ this.getCurrentPage().moveTo(xRightPosition, this.getCurrentPage().getY());
443
+ this.getCurrentPage().drawText(text, {
444
+ size: defaultOptions.size,
445
+ color: defaultOptions.color,
446
+ opacity: defaultOptions.opacity
447
+ });
448
+ this.getCurrentPage().moveTo(currentPositionRight.x, currentPositionRight.y);
449
+ break;
450
+
451
+ default:
452
+ break;
453
+ }
454
+ }
455
+
456
+ /**
457
+ *### Draws text that automatically wraps to the next line
458
+ *
459
+ * @param {string} text
460
+ * @param {object} options
461
+ * @param {"left"|"center"|"right"} options.align
462
+ * @param {{left: number, right: number}} options.range - The bounds in which the paragraph is written
463
+ * @param {number} options.size - The size of the text
464
+ * @param {import('pdf-lib').RGB} options.color - The color of the text
465
+ * @param {number} options.opacity - The opacity of the text
466
+ */
467
+ drawParagraph(text, options = {}) {
468
+ let defaultOptions = {
469
+ align: "left",
470
+ range: {
471
+ left: this.getMargin().left,
472
+ right: this.getCurrentPage().getWidth() - this.getMargin().right
473
+ },
474
+ size: this.getTextSize(),
475
+ color: this.getColor(),
476
+ opacity: 1,
477
+ ...options
478
+ };
479
+ if(options.range) this.getCurrentPage().moveTo(defaultOptions.range.left, this.getCurrentPage().getY());
480
+
481
+ let maxWidth = defaultOptions.range.right - defaultOptions.range.left;
482
+ let currentWidth = 0;
483
+ let currentLine = "";
484
+
485
+ text = text.split(" ");
486
+
487
+ text.forEach((string, i) => {
488
+ let wordWidth = this.getCurrentFont().widthOfTextAtSize(string, defaultOptions.size);
489
+
490
+ // Check if adding this word would overflow
491
+ if (currentWidth + wordWidth > maxWidth) {
492
+ this.drawText(currentLine.trim(), {
493
+ size: defaultOptions.size,
494
+ color: defaultOptions.color,
495
+ opacity: defaultOptions.opacity,
496
+ align: defaultOptions.align,
497
+ range: defaultOptions.range
498
+ });
499
+
500
+ // Move to the next line
501
+ this.nextLine();
502
+ this.getCurrentPage().moveTo(defaultOptions.range.left, this.getCurrentPage().getY());
503
+ currentLine = "";
504
+ currentWidth = 0;
505
+ }
506
+
507
+ currentLine += string + " ";
508
+ currentWidth += wordWidth;
509
+
510
+ // If it's the last word, draw the remaining line
511
+ if (i === text.length - 1) {
512
+ this.drawText(currentLine.trim(), {
513
+ size: defaultOptions.size,
514
+ color: defaultOptions.color,
515
+ opacity: defaultOptions.opacity,
516
+ align: defaultOptions.align,
517
+ range: defaultOptions.range
518
+ });
519
+ }
520
+ });
521
+ }
522
+
523
+ /**
524
+ *### Draws a cell on the current page
525
+ *
526
+ * @param {string} text
527
+ * @param {number} x
528
+ * @param {number} y
529
+ * @param {number} width
530
+ * @param {object} options
531
+ * @param {number} options.height - The height of the cell
532
+ * @param {boolean|string} options.border - The border of the cell.
533
+ * Can be:
534
+ * - `true`/`false` to toggle all borders
535
+ * - A string with specific borders: `"t"` (top), `"r"` (right), `"b"` (bottom), `"l"` (left)
536
+ * - A combination of these letters to apply multiple borders, e.g., `"tb"` for top and bottom or `"tbl"` for top, bottom, and left.
537
+ * @param {"left"|"center"|"right"} options.align
538
+ * @param {boolean} options.newLine
539
+ * @param {number} options.size - The size of the text
540
+ * @param {import('pdf-lib').RGB} options.color - The color of the border and text
541
+ * @param {number} options.lineThickness - The thickness of the border
542
+ * @param {number} options.padding
543
+ * @param {number} options.borderOpacity
544
+ */
545
+ drawCell(text, x, y, width, options = {}){
546
+ let defaultOptions = {
547
+ height: null,
548
+ border: false,
549
+ align: "left",
550
+ newLine: true,
551
+ size: this.getTextSize(),
552
+ color: this.getColor(),
553
+ lineThickness: 1,
554
+ padding: 4,
555
+ borderOpacity: 0.3,
556
+ ...options
557
+ };
558
+ let page = this.getCurrentPage();
559
+ page.moveTo(x, y);
560
+ let change = page.getY();
561
+ this.drawParagraph(text, {range: {left: x, right: x + width}, align: defaultOptions.align, size: defaultOptions.size, color: defaultOptions.color});
562
+ change -= page.getY() - defaultOptions.size;
563
+
564
+ y += defaultOptions.size;
565
+
566
+ /*** BORDERS ***/
567
+ if(defaultOptions.border){
568
+ // TOP
569
+ if(defaultOptions.border === true || defaultOptions.border.includes("t") || defaultOptions.border.includes("T")){
570
+ page.drawLine({
571
+ start: {x: x - defaultOptions.padding, y: y},
572
+ end: {x: x + width, y: y},
573
+ thickness: defaultOptions.lineThickness,
574
+ color: defaultOptions.color,
575
+ opacity: defaultOptions.borderOpacity
576
+ });
577
+ }
578
+ // BOTTOM
579
+ if(defaultOptions.border === true || defaultOptions.border.includes("b") || defaultOptions.border.includes("B")){
580
+ page.drawLine({
581
+ start: {x: x - defaultOptions.padding, y: ((defaultOptions.height) ? y - defaultOptions.height : y - change - defaultOptions.padding)},
582
+ end: {x: x + width, y: ((defaultOptions.height) ? y - defaultOptions.height : y - change -defaultOptions.padding)},
583
+ thickness: defaultOptions.lineThickness,
584
+ color: defaultOptions.color,
585
+ opacity: defaultOptions.borderOpacity
586
+ });
587
+ }
588
+ // LEFT
589
+ if(defaultOptions.border === true || defaultOptions.border.includes("l") || defaultOptions.border.includes("L")){
590
+ page.drawLine({
591
+ start: {x: x - defaultOptions.padding, y: y + (defaultOptions.lineThickness / 2)},
592
+ end: {x: x - defaultOptions.padding, y: ((defaultOptions.height) ? y - defaultOptions.height - (defaultOptions.lineThickness / 2) : y - change - defaultOptions.padding - (defaultOptions.lineThickness / 2))},
593
+ thickness: defaultOptions.lineThickness,
594
+ color: defaultOptions.color,
595
+ opacity: defaultOptions.borderOpacity
596
+ });
597
+ }
598
+ // RIGHT
599
+ if(defaultOptions.border === true || defaultOptions.border.includes("r") || defaultOptions.border.includes("R")){
600
+ page.drawLine({
601
+ start: {x: x + width, y: y + (defaultOptions.lineThickness / 2)},
602
+ end: {x: x + width, y: ((defaultOptions.height) ? y - defaultOptions.height - (defaultOptions.lineThickness / 2) : y - change - defaultOptions.padding - (defaultOptions.lineThickness / 2))},
603
+ thickness: defaultOptions.lineThickness,
604
+ color: defaultOptions.color,
605
+ opacity: defaultOptions.borderOpacity
606
+ });
607
+ }
608
+ }
609
+
610
+ if(defaultOptions.newLine){
611
+ this.nextLine();
612
+ page.moveTo(page.getX(), y - Number(defaultOptions.height) - change - defaultOptions.size - defaultOptions.padding);
613
+ }
614
+ else page.moveTo(x + width + defaultOptions.padding, y - defaultOptions.size);
615
+ }
616
+
617
+ /**
618
+ *### Draws a table
619
+ *
620
+ * @param {array} header
621
+ * @param {array} data
622
+ * @param {object} options
623
+ * @param {boolean|string} options.border - The border of the cell.
624
+ * Can be:
625
+ * - `true`/`false` to toggle all borders
626
+ * - A string with specific borders: `"t"` (top), `"r"` (right), `"b"` (bottom), `"l"` (left)
627
+ * - A combination of these letters to apply multiple borders, e.g., `"tb"` for top and bottom or `"tbl"` for top, bottom, and left.
628
+ * @param {{left: number, right: number}} options.range - The bounds in which the paragraph is written
629
+ * @param {"left"|"center"|"right"} options.align
630
+ * @param {number} options.size - The size of the text
631
+ * @param {import('pdf-lib').RGB} options.color - The color of the border and text
632
+ * @param {number} options.headerDifference - The difference in size of the header text from the regular text size
633
+ */
634
+ drawTable(header = null, data = null, options = {}){
635
+ let defaultOptions = {
636
+ range: {
637
+ left: this.getMargin().left,
638
+ right: this.getCurrentPage().getWidth() - this.getMargin().right
639
+ },
640
+ headerDifference: 0,
641
+ align: "center",
642
+ border: true,
643
+ size: this.getTextSize(),
644
+ color: this.getColor(),
645
+ ...options
646
+ };
647
+ if (options.range) this.getCurrentPage().moveTo(defaultOptions.range.left, this.getCurrentPage().getY());
648
+
649
+ let cellWidth = (defaultOptions.range.right - defaultOptions.range.left) / 3;
650
+ let pointer = {
651
+ x: defaultOptions.range.left,
652
+ y: this.getCurrentPage().getY()
653
+ }
654
+ this.getCurrentPage().moveTo(pointer.x, pointer.y);
655
+ if(header){
656
+ this.setFont(this.getBoldFont());
657
+ header.forEach((head, i) => {
658
+ this.drawCell(head, this.getCurrentPage().getX(), this.getCurrentPage().getY(), cellWidth, {
659
+ border: defaultOptions.border,
660
+ align: defaultOptions.align,
661
+ size: defaultOptions.size + defaultOptions.headerDifference,
662
+ color: defaultOptions.color,
663
+ newLine: (i === header.length - 1) ? true : false
664
+ });
665
+ });
666
+ }
667
+ if(data){
668
+ this.setFont(this.getFont());
669
+ this.getCurrentPage().moveTo(pointer.x, this.getCurrentPage().getY() + defaultOptions.headerDifference);
670
+ data.forEach((row) => {
671
+ this.getCurrentPage().moveTo(pointer.x, this.getCurrentPage().getY());
672
+ row.forEach((string, i) => {
673
+ this.drawCell(string, this.getCurrentPage().getX(), this.getCurrentPage().getY(), cellWidth, {
674
+ border: defaultOptions.border,
675
+ align: defaultOptions.align,
676
+ size: defaultOptions.size + defaultOptions.headerDifference,
677
+ color: defaultOptions.color,
678
+ newLine: (i === header.length - 1) ? true : false
679
+ });
680
+ });
681
+ });
682
+ }
683
+ }
684
+
685
+ /**
686
+ *
687
+ * @param {string} text - HTML text
688
+ * @param {DomParser} parser
689
+ * @param {object} options
690
+ * @param {number} options.margin
691
+ * @param {{left: number, right: number}} options.range - The bounds in which the paragraph is written
692
+ * @param {"left"|"center"|"right"} options.align
693
+ * @param {number} options.size - The size of the text
694
+ * @param {import('pdf-lib').RGB} options.color - The color of the border and text
695
+ */
696
+ htmlParser(text, parser = null, options = {}){
697
+ let doc;
698
+ if(!parser){
699
+ parser = new DOMParser();
700
+ doc = parser.parseFromString(text, "text/html");
701
+ }else doc = text;
702
+ let nodes = doc.childNodes;
703
+ let defaultOptions = {
704
+ margin: 8,
705
+ range: {
706
+ left: this.getMargin().left,
707
+ right: this.getCurrentPage().getWidth() - this.getMargin().right
708
+ },
709
+ align: "left",
710
+ color: this.getColor(),
711
+ size: this.getTextSize(),
712
+ ...options,
713
+ }
714
+ if (defaultOptions.range && !parser) this.getCurrentPage().moveTo(defaultOptions.range.left, this.getCurrentPage().getY());
715
+ let maxWidth = defaultOptions.range.right;
716
+
717
+ if(nodes.length > 0){
718
+ for(let node of nodes){
719
+ if(node.nodeType === Node.ELEMENT_NODE){
720
+ this.addNode(node.nodeName);
721
+ switch (node.nodeName) {
722
+ case "P":
723
+ this.getCurrentPage().moveTo(defaultOptions.range.left, this.getCurrentPage().getY() - defaultOptions.margin);
724
+ this.nextLine();
725
+ this.getCurrentPage().moveTo(defaultOptions.range.left, this.getCurrentPage().getY());
726
+ break;
727
+ case "STRONG":
728
+ if(this.getCurrentNodes().includes("I")) this.setFont(this.getItalicBoldFont());
729
+ else this.setFont(this.getBoldFont());
730
+ break;
731
+ case "I":
732
+ if(this.getCurrentNodes().includes("STRONG")) this.setFont(this.getItalicBoldFont());
733
+ else this.setFont(this.getItalicFont());
734
+ break;
735
+ case "UL":
736
+ break;
737
+ case "LI":
738
+ this.nextLine();
739
+ this.getCurrentPage().moveTo(defaultOptions.range.left, this.getCurrentPage().getY());
740
+ this.getCurrentPage().moveRight(this.getCurrentNodes().filter(value => value === "UL").length * this.getMargin().left);
741
+ this.drawText("• ", { size: defaultOptions.size, color: defaultOptions.color });
742
+ this.getCurrentPage().moveRight(this.getCurrentFont().widthOfTextAtSize("• ", defaultOptions.size));
743
+ break;
744
+ case "TABLE":
745
+ let tableHead = [];
746
+ let tableData = [];
747
+ node.querySelectorAll("thead th").forEach(element => {
748
+ tableHead.push(element.innerHTML.replace(/<\/?[^>]+(>|$)/g, ""));
749
+ });
750
+ node.querySelectorAll("tbody tr").forEach(row => {
751
+ let tableRow = [];
752
+ row.querySelectorAll("td").forEach(col => {
753
+ tableRow.push(col.innerHTML.replace(/<\/?[^>]+(>|$)/g, ""));
754
+ });
755
+ tableData.push(tableRow);
756
+ });
757
+ this.nextLine();
758
+ this.getCurrentPage().moveTo(defaultOptions.range.left, this.getCurrentPage().getY());
759
+ this.drawTable(tableHead, tableData, {
760
+ range: {left: defaultOptions.range.left, right: defaultOptions.range.right},
761
+ size: defaultOptions.size,
762
+ color: defaultOptions.color,
763
+ });
764
+ break;
765
+ default:
766
+ break;
767
+ }
768
+ }
769
+
770
+ if(node.nodeName === "TABLE") break;
771
+
772
+ this.htmlParser(node, parser, defaultOptions);
773
+ }
774
+ }else{
775
+ if(text.data){
776
+ /*** DRAW TEXT ***/
777
+ let splitText = text.data.split(" ");
778
+ splitText.forEach(string => {
779
+ string += " ";
780
+ let wordWidth = this.getCurrentFont().widthOfTextAtSize(string, defaultOptions.size);
781
+ if(wordWidth + this.getCurrentPage().getX() >= maxWidth){
782
+ this.nextLine();
783
+ this.getCurrentPage().moveTo(defaultOptions.range.left, this.getCurrentPage().getY());
784
+ }
785
+ this.drawText(string, {
786
+ size: defaultOptions.size,
787
+ color: defaultOptions.color
788
+ });
789
+ this.getCurrentPage().moveRight(this.getCurrentFont().widthOfTextAtSize(string, defaultOptions.size));
790
+ });
791
+
792
+ /*** REMOVE STYLINGS ***/
793
+ this.checkParentHTML(text);
794
+ }
795
+ return;
796
+ }
797
+ }
798
+
799
+ /**
800
+ *
801
+ * @param {string} text
802
+ * @returns
803
+ */
804
+ checkParentHTML(text){
805
+ if(!text.parentNode) return;
806
+ switch (text.parentNode.nodeName) {
807
+ case "STRONG":
808
+ if(!text.nextSibling){
809
+ if(this.getCurrentNodes().includes("I")) this.setFont(this.getItalicFont());
810
+ else this.setFont(this.getFont());
811
+ this.removeNode(text.parentNode.nodeName);
812
+ }
813
+ break;
814
+ case "I":
815
+ if(!text.nextSibling){
816
+ if(this.getCurrentNodes().includes("B")) this.setFont(this.getBoldFont());
817
+ else this.setFont(this.getFont());
818
+ this.removeNode(text.parentNode.nodeName);
819
+ }
820
+ break;
821
+ case "LI":
822
+ if(!text.nextSibling){
823
+ this.removeNode(text.parentNode.nodeName);
824
+ }
825
+ break;
826
+ case "UL":
827
+ if(!text.nextSibling){
828
+ this.removeNode(text.parentNode.nodeName);
829
+ }
830
+ break;
831
+ default:
832
+ break;
833
+ }
834
+ if(!text.parentNode.nextSibling){
835
+ this.checkParentHTML(text.parentNode);
836
+ }else{
837
+ return;
838
+ }
839
+ }
840
+
841
+ /**
842
+ *### Draws a cirlce with text in the center
843
+ *
844
+ * @param {number} x
845
+ * @param {number} y
846
+ * @param {string} text
847
+ * @param {object} options
848
+ * @param {number} options.size - The size of the text
849
+ * @param {import('pdf-lib').RGB} options.color - The color of the text and circle
850
+ * @param {number} options.borderWidth
851
+ */
852
+ drawCircleText(x, y, text, options = {}){
853
+ let defaultOptions = {
854
+ size: this.getTextSize(),
855
+ color: this.getColor(),
856
+ borderWidth: 2,
857
+ ...options
858
+ };
859
+ // Calculate common values
860
+ const centerX = x;
861
+ const centerY = y - (this.getCurrentFont().widthOfTextAtSize(text, defaultOptions.size)) - 5;
862
+ const textX = centerX - (this.getCurrentFont().widthOfTextAtSize(text, defaultOptions.size) / 2);
863
+ const textY = centerY - (defaultOptions.size / 3);
864
+
865
+ // Draw the circle
866
+ this.getCurrentPage().drawEllipse({
867
+ x: centerX,
868
+ y: centerY,
869
+ xScale: this.getCurrentFont().widthOfTextAtSize(text, defaultOptions.size),
870
+ yScale: this.getCurrentFont().widthOfTextAtSize(text, defaultOptions.size),
871
+ borderWidth: defaultOptions.borderWidth,
872
+ borderColor: defaultOptions.color,
873
+ });
874
+
875
+ // Draw the text
876
+ this.getCurrentPage().drawText(String(text), {
877
+ x: textX,
878
+ y: textY,
879
+ color: defaultOptions.color,
880
+ size: defaultOptions.size,
881
+ });
882
+ }
883
+
884
+ /**
885
+ *### Method to add an image to the PDF document at specified coordinates with optional scaling
886
+ * @param {number} x
887
+ * @param {number} y
888
+ * @param {base64} base64Image
889
+ * @param {number} imageScale
890
+ */
891
+ async addImage(x, y, base64Image, imageScale = 1) {
892
+ if (base64Image.includes(",")) base64Image = base64Image.split(",")[1];
893
+ let imageBytes = Uint8Array.from(atob(base64Image), c => c.charCodeAt(0));
894
+ let embeddedImage = await this.getPDF().embedPng(imageBytes);
895
+ let { width, height } = embeddedImage.scale(imageScale);
896
+ this.getCurrentPage().drawImage(embeddedImage, {
897
+ x: x,
898
+ y: y,
899
+ width: width,
900
+ height: height,
901
+ });
902
+ }
903
+
904
+ /**
905
+ *### Draws a watermark on each page of the document
906
+ * @param {string} text
907
+ * @param {number} size
908
+ * @returns
909
+ */
910
+ drawWatermark(text, size = 30){
911
+ try {
912
+ if(!text) throw new Error("The text provided must be a valid value");
913
+ this.getPDF().getPages().forEach(page => {
914
+ page.drawText(text, {
915
+ x: (page.getWidth() / 2) - (this.getBoldFont().widthOfTextAtSize(text, size) * 0.36),
916
+ y: (page.getHeight() / 2) + (this.getBoldFont().widthOfTextAtSize(text, size) * 0.36),
917
+ font: this.getBoldFont(),
918
+ size: size,
919
+ color: this.getColor(),
920
+ opacity: 0.25,
921
+ rotate: degrees(-45)
922
+ });
923
+ });
924
+ return true;
925
+ } catch (error) {
926
+ console.error("Error in drawWatermark: ", error);
927
+ return false;
928
+ }
929
+ }
930
+ }
931
+
932
+ export default PDFLibExtended;