modern-pdf-lib 0.15.1 → 0.22.9
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 +119 -9
- package/dist/batchOptimize-Ba_pWw71.cjs +427 -0
- package/dist/batchOptimize-CxyY4fZe.mjs +392 -0
- package/dist/{bridge-DpzMOnHd.mjs → bridge-DTH5LMAK.mjs} +3 -3
- package/dist/{bridge-DN7BOHRW.cjs → bridge-DYCQzxF7.cjs} +2 -2
- package/dist/browser.cjs +621 -0
- package/dist/browser.d.cts +190 -0
- package/dist/browser.d.cts.map +1 -0
- package/dist/browser.d.mts +190 -0
- package/dist/browser.d.mts.map +1 -0
- package/dist/browser.mjs +212 -0
- package/dist/cli/index.cjs +40 -18
- package/dist/cli/index.mjs +40 -18
- package/dist/compressionAnalysis-B84FPXaQ.cjs +1525 -0
- package/dist/compressionAnalysis-BBv4BkQP.d.mts +261 -0
- package/dist/compressionAnalysis-BBv4BkQP.d.mts.map +1 -0
- package/dist/compressionAnalysis-ChkscEa1.mjs +1490 -0
- package/dist/compressionAnalysis-CtJ2X9l2.d.cts +261 -0
- package/dist/compressionAnalysis-CtJ2X9l2.d.cts.map +1 -0
- package/dist/create.cjs +35 -0
- package/dist/create.d.cts +3 -0
- package/dist/create.d.mts +3 -0
- package/dist/create.mjs +5 -0
- package/dist/{deduplicateImages-BfpjHY9b.mjs → deduplicateImages-CmTeo6Tx.mjs} +3 -3
- package/dist/{deduplicateImages-BtJ5tlrr.cjs → deduplicateImages-cKsnD6Ep.cjs} +2 -2
- package/dist/{fflateAdapter-D2mv_ttM.mjs → fflateAdapter-CBQpGTlx.mjs} +2 -2
- package/dist/{fflateAdapter-cT4YeY_h.cjs → fflateAdapter-LTAeAhaD.cjs} +1 -1
- package/dist/fieldAppearance-C8PoLFSc.d.mts +136 -0
- package/dist/fieldAppearance-C8PoLFSc.d.mts.map +1 -0
- package/dist/fieldAppearance-CdiGFG5e.d.cts +136 -0
- package/dist/fieldAppearance-CdiGFG5e.d.cts.map +1 -0
- package/dist/fontEmbed-Dsu9fo4U.d.mts +636 -0
- package/dist/fontEmbed-Dsu9fo4U.d.mts.map +1 -0
- package/dist/fontEmbed-LID6yG6g.d.cts +636 -0
- package/dist/fontEmbed-LID6yG6g.d.cts.map +1 -0
- package/dist/{fontSubset-BxsF9Tu5.cjs → fontSubset-5SLWMmEw.cjs} +1 -1
- package/dist/{fontSubset-ClyTXlhY.mjs → fontSubset-DWpduoY2.mjs} +2 -2
- package/dist/forms.cjs +13 -0
- package/dist/forms.d.cts +3 -0
- package/dist/forms.d.mts +3 -0
- package/dist/forms.mjs +3 -0
- package/dist/grayscaleDetect-C2m-eEXR.cjs +96 -0
- package/dist/grayscaleDetect-C6kFF3dk.mjs +84 -0
- package/dist/imageExtract-Dnk_Ssv7.mjs +155 -0
- package/dist/imageExtract-zEb1gnkb.cjs +166 -0
- package/dist/index-BtYOx5wh.d.mts +4904 -0
- package/dist/index-BtYOx5wh.d.mts.map +1 -0
- package/dist/index-bpktKzCA.d.cts +4904 -0
- package/dist/index-bpktKzCA.d.cts.map +1 -0
- package/dist/index.cjs +274 -20704
- package/dist/index.d.cts +7 -9151
- package/dist/index.d.mts +7 -9151
- package/dist/index.mjs +17 -20532
- package/dist/layout-CuAVk_Or.cjs +563 -0
- package/dist/layout-DgX_0jfK.mjs +438 -0
- package/dist/{libdeflateWasm-Cg7cWHOq.cjs → libdeflateWasm-BdiDEJOj.cjs} +2 -2
- package/dist/{libdeflateWasm-Cmxa-yiS.mjs → libdeflateWasm-rLppXytE.mjs} +3 -3
- package/dist/loader-3u6Tw5T-.mjs +328 -0
- package/dist/loader-I4zdkoWc.cjs +393 -0
- package/dist/parse.cjs +24 -0
- package/dist/parse.d.cts +4 -0
- package/dist/parse.d.mts +4 -0
- package/dist/parse.mjs +7 -0
- package/dist/{pdfCatalog-BcOL6QF-.cjs → pdfCatalog-CYy4NXEY.cjs} +2 -2
- package/dist/{pdfCatalog-CnJRovvm.mjs → pdfCatalog-IImGcMbR.mjs} +3 -3
- package/dist/pdfDocument-BFxHD_2u.mjs +13755 -0
- package/dist/pdfDocument-BSiQdNZq.d.cts +4640 -0
- package/dist/pdfDocument-BSiQdNZq.d.cts.map +1 -0
- package/dist/pdfDocument-i6U5fQ91.d.mts +4640 -0
- package/dist/pdfDocument-i6U5fQ91.d.mts.map +1 -0
- package/dist/pdfDocument-pmRXryVI.cjs +14180 -0
- package/dist/pdfForm-9gd40uz9.cjs +1796 -0
- package/dist/pdfForm-BiyNtYem.d.mts +905 -0
- package/dist/pdfForm-BiyNtYem.d.mts.map +1 -0
- package/dist/pdfForm-Cn-cVicP.mjs +1695 -0
- package/dist/pdfForm-SOXJ72LW.d.cts +905 -0
- package/dist/pdfForm-SOXJ72LW.d.cts.map +1 -0
- package/dist/{pdfObjects-BrU4Xd0V.cjs → pdfObjects-1veop1_d.cjs} +2 -2
- package/dist/{pdfObjects-DZZ2GPRW.mjs → pdfObjects-uEsWlfzU.mjs} +2 -2
- package/dist/{pdfPage-Dm5XC_g_.mjs → pdfPage-Cd8e7flb.mjs} +3024 -5
- package/dist/{pdfPage-Dz_SVKUS.cjs → pdfPage-Cd8jOJp6.cjs} +3046 -3
- package/dist/{pngEmbed-I1hU3Y6m.mjs → pngEmbed-BLj2zi-5.mjs} +3 -3
- package/dist/{pngEmbed-C6M1eX6b.cjs → pngEmbed-D4X4ZN-3.cjs} +2 -2
- package/dist/src-6L07EQsi.cjs +11852 -0
- package/dist/src-Dm4aaZ8q.mjs +11103 -0
- package/dist/{imageExtract-vjyQyFcT.mjs → streamDecode-Bj568Nc9.mjs} +1646 -188
- package/dist/{imageExtract-BC7TMY98.cjs → streamDecode-CvgErkFu.cjs} +1645 -199
- package/package.json +33 -1
- package/dist/batchOptimize-ClXizv19.mjs +0 -306
- package/dist/batchOptimize-DYQOX1-7.cjs +0 -329
- package/dist/index.d.cts.map +0 -1
- package/dist/index.d.mts.map +0 -1
- package/dist/loader-B6VIrZOJ.mjs +0 -164
- package/dist/loader-DdB5Xo5D.cjs +0 -166
- package/dist/rolldown-runtime-95iHPtFO.mjs +0 -18
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ Create, parse, fill, merge, sign, and manipulate PDF documents<br />in Node, Den
|
|
|
15
15
|
|
|
16
16
|
[](https://www.npmjs.com/package/modern-pdf-lib)
|
|
17
17
|
[](https://bundlephobia.com/package/modern-pdf-lib)
|
|
18
|
-
[](#)
|
|
19
19
|
[](#)
|
|
20
20
|
[](LICENSE)
|
|
21
21
|
|
|
@@ -55,6 +55,44 @@ const stream = doc.saveAsStream(); // ReadableStream
|
|
|
55
55
|
const blob = await doc.saveAsBlob(); // Blob (browsers)
|
|
56
56
|
```
|
|
57
57
|
|
|
58
|
+
### CDN (no install)
|
|
59
|
+
|
|
60
|
+
Use directly in the browser via CDN — no bundler required:
|
|
61
|
+
|
|
62
|
+
```html
|
|
63
|
+
<script type="module">
|
|
64
|
+
import { createPdf, PageSizes, rgb } from 'https://cdn.jsdelivr.net/npm/modern-pdf-lib/dist/browser.mjs';
|
|
65
|
+
|
|
66
|
+
const doc = createPdf();
|
|
67
|
+
const page = doc.addPage(PageSizes.A4);
|
|
68
|
+
page.drawText('Hello from CDN!', { x: 50, y: 750, size: 24, color: rgb(0, 0, 0) });
|
|
69
|
+
const bytes = await doc.save();
|
|
70
|
+
console.log('PDF size:', bytes.length, 'bytes');
|
|
71
|
+
</script>
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Also available via:
|
|
75
|
+
- **unpkg:** `https://unpkg.com/modern-pdf-lib/dist/browser.mjs`
|
|
76
|
+
- **esm.sh:** `https://esm.sh/modern-pdf-lib`
|
|
77
|
+
|
|
78
|
+
### Script Tag (no modules)
|
|
79
|
+
|
|
80
|
+
For environments without ES module support, use the IIFE bundle which exposes a `window.ModernPdf` global:
|
|
81
|
+
|
|
82
|
+
```html
|
|
83
|
+
<script src="https://cdn.jsdelivr.net/npm/modern-pdf-lib/dist/modern-pdf-lib.iife.js"></script>
|
|
84
|
+
<script>
|
|
85
|
+
const { createPdf, PageSizes, rgb } = ModernPdf;
|
|
86
|
+
|
|
87
|
+
const doc = createPdf();
|
|
88
|
+
const page = doc.addPage(PageSizes.A4);
|
|
89
|
+
page.drawText('Hello from script tag!', { x: 50, y: 750, size: 24, color: rgb(0, 0, 0) });
|
|
90
|
+
doc.save().then(function (bytes) {
|
|
91
|
+
console.log('PDF size:', bytes.length, 'bytes');
|
|
92
|
+
});
|
|
93
|
+
</script>
|
|
94
|
+
```
|
|
95
|
+
|
|
58
96
|
<br />
|
|
59
97
|
|
|
60
98
|
## Features
|
|
@@ -92,6 +130,7 @@ const blob = await doc.saveAsBlob(); // Blob (browsers)
|
|
|
92
130
|
**Secure & Compliant**
|
|
93
131
|
- AES-256 / RC4 encryption & decryption
|
|
94
132
|
- Digital signatures (PKCS#7, visible/invisible, timestamps)
|
|
133
|
+
- CRL/OCSP revocation checking & certificate chain validation
|
|
95
134
|
- PDF/A-1b through PDF/A-3u validation
|
|
96
135
|
- Tagged PDF / PDF/UA accessibility
|
|
97
136
|
- Structure tree & marked content
|
|
@@ -101,13 +140,16 @@ const blob = await doc.saveAsBlob(); // Blob (browsers)
|
|
|
101
140
|
<td width="50%" valign="top">
|
|
102
141
|
|
|
103
142
|
**Advanced**
|
|
143
|
+
- QR codes & barcodes (9 formats)
|
|
144
|
+
- Table layout engine with pagination
|
|
145
|
+
- JPEG2000 (JPXDecode) image support
|
|
146
|
+
- Form field JavaScript evaluation & sandboxing
|
|
104
147
|
- Outlines / bookmarks
|
|
105
148
|
- Optional content layers (OCGs)
|
|
106
|
-
- File attachments
|
|
107
|
-
- Watermarks
|
|
149
|
+
- File attachments & watermarks
|
|
108
150
|
- Linearization (fast web view)
|
|
109
|
-
-
|
|
110
|
-
-
|
|
151
|
+
- Browser helpers (download, blob, data URL)
|
|
152
|
+
- Service Worker & Web Worker support
|
|
111
153
|
- CLI: `npx modern-pdf optimize`
|
|
112
154
|
|
|
113
155
|
</td>
|
|
@@ -150,7 +192,15 @@ const blob = await doc.saveAsBlob(); // Blob (browsers)
|
|
|
150
192
|
<td align="center">No</td></tr>
|
|
151
193
|
|
|
152
194
|
<tr><td><strong>Digital signatures</strong></td>
|
|
153
|
-
<td align="center">PKCS#7, timestamps</td>
|
|
195
|
+
<td align="center">PKCS#7, timestamps, CRL/OCSP</td>
|
|
196
|
+
<td align="center">No</td></tr>
|
|
197
|
+
|
|
198
|
+
<tr><td><strong>JPEG2000 decoding</strong></td>
|
|
199
|
+
<td align="center">Full JPXDecode (JP2 + J2K)</td>
|
|
200
|
+
<td align="center">No</td></tr>
|
|
201
|
+
|
|
202
|
+
<tr><td><strong>Form JavaScript</strong></td>
|
|
203
|
+
<td align="center">Sandboxed evaluation</td>
|
|
154
204
|
<td align="center">No</td></tr>
|
|
155
205
|
|
|
156
206
|
<tr><td><strong>Forms</strong></td>
|
|
@@ -197,6 +247,18 @@ const blob = await doc.saveAsBlob(); // Blob (browsers)
|
|
|
197
247
|
<td align="center">Yes</td>
|
|
198
248
|
<td align="center">No</td></tr>
|
|
199
249
|
|
|
250
|
+
<tr><td><strong>QR codes & barcodes</strong></td>
|
|
251
|
+
<td align="center">9 formats (QR, EAN, Code 128, PDF417…)</td>
|
|
252
|
+
<td align="center">No</td></tr>
|
|
253
|
+
|
|
254
|
+
<tr><td><strong>Table layout</strong></td>
|
|
255
|
+
<td align="center">Spanning, pagination, presets, overflow</td>
|
|
256
|
+
<td align="center">No</td></tr>
|
|
257
|
+
|
|
258
|
+
<tr><td><strong>Browser utilities</strong></td>
|
|
259
|
+
<td align="center">Download, blob, Web Worker, Service Worker</td>
|
|
260
|
+
<td align="center">No</td></tr>
|
|
261
|
+
|
|
200
262
|
<tr><td><strong>Image optimization</strong></td>
|
|
201
263
|
<td align="center">JPEG recompress, dedup, grayscale</td>
|
|
202
264
|
<td align="center">No</td></tr>
|
|
@@ -390,6 +452,51 @@ npx modern-pdf optimize report.pdf report-opt.pdf --quality 60 --grayscale --ded
|
|
|
390
452
|
```
|
|
391
453
|
</details>
|
|
392
454
|
|
|
455
|
+
<details>
|
|
456
|
+
<summary><strong>Tables</strong> — layout engine with spanning, pagination, presets</summary>
|
|
457
|
+
|
|
458
|
+
```ts
|
|
459
|
+
import { createPdf, PageSizes, professionalPreset, applyPreset } from 'modern-pdf-lib';
|
|
460
|
+
|
|
461
|
+
const doc = createPdf();
|
|
462
|
+
const page = doc.addPage(PageSizes.A4);
|
|
463
|
+
|
|
464
|
+
page.drawTable(applyPreset(professionalPreset(), {
|
|
465
|
+
x: 50,
|
|
466
|
+
y: 750,
|
|
467
|
+
width: 495,
|
|
468
|
+
headerRows: 1,
|
|
469
|
+
rows: [
|
|
470
|
+
{ cells: ['Product', 'Qty', 'Price', 'Total'] },
|
|
471
|
+
{ cells: ['Widget A', '10', '$5.00', '$50.00'] },
|
|
472
|
+
{ cells: ['Widget B', '25', '$3.50', '$87.50'] },
|
|
473
|
+
{ cells: [{ content: 'Grand Total', colSpan: 3, align: 'right' }, '$137.50'] },
|
|
474
|
+
],
|
|
475
|
+
columns: [{ flex: 2 }, { width: 60 }, { width: 80 }, { width: 80, align: 'right' }],
|
|
476
|
+
}));
|
|
477
|
+
```
|
|
478
|
+
</details>
|
|
479
|
+
|
|
480
|
+
<details>
|
|
481
|
+
<summary><strong>QR Codes & Barcodes</strong> — 9 formats</summary>
|
|
482
|
+
|
|
483
|
+
```ts
|
|
484
|
+
import { createPdf, PageSizes } from 'modern-pdf-lib';
|
|
485
|
+
|
|
486
|
+
const doc = createPdf();
|
|
487
|
+
const page = doc.addPage(PageSizes.A4);
|
|
488
|
+
|
|
489
|
+
// QR code
|
|
490
|
+
page.drawQrCode('https://example.com', { x: 50, y: 700, size: 120 });
|
|
491
|
+
|
|
492
|
+
// Barcodes (Code 128, EAN-13, UPC-A, Code 39, ITF, PDF417, Data Matrix)
|
|
493
|
+
import { encodeCode128, encodeEan13, renderStyledBarcode } from 'modern-pdf-lib';
|
|
494
|
+
|
|
495
|
+
const barcode = encodeCode128('ABC-12345');
|
|
496
|
+
const ops = renderStyledBarcode(barcode, { x: 50, y: 500, height: 60 });
|
|
497
|
+
```
|
|
498
|
+
</details>
|
|
499
|
+
|
|
393
500
|
<details>
|
|
394
501
|
<summary><strong>PDF/A & Accessibility</strong></summary>
|
|
395
502
|
|
|
@@ -467,16 +574,19 @@ modern-pdf-lib/
|
|
|
467
574
|
annotation/ 18 annotation types + appearance generators
|
|
468
575
|
accessibility/ Structure tree, marked content, PDF/UA checker
|
|
469
576
|
compliance/ PDF/A validation & enforcement
|
|
470
|
-
signature/ PKCS#7 signatures, timestamps, verification
|
|
577
|
+
signature/ PKCS#7 signatures, timestamps, verification, CRL/OCSP
|
|
471
578
|
crypto/ AES-256, RC4, MD5, SHA-256/384/512
|
|
472
579
|
compression/ Deflate (fflate + optional WASM)
|
|
473
580
|
assets/ Font metrics/embed/subset, image embed, SVG
|
|
581
|
+
barcode/ QR, Code 128, EAN, UPC, Code 39, ITF, PDF417, Data Matrix
|
|
582
|
+
layout/ Table engine (spanning, pagination, presets, overflow)
|
|
583
|
+
browser/ Download helpers, Service Worker, Web Worker
|
|
474
584
|
layers/ Optional content groups (OCG)
|
|
475
585
|
outline/ Bookmarks / document outline
|
|
476
586
|
metadata/ XMP metadata, viewer preferences
|
|
477
587
|
wasm/ Rust crate sources (6 modules)
|
|
478
588
|
cli/ CLI tool (modern-pdf optimize)
|
|
479
|
-
tests/
|
|
589
|
+
tests/ 3,997 tests across 184 suites
|
|
480
590
|
docs/ VitePress documentation
|
|
481
591
|
```
|
|
482
592
|
|
|
@@ -488,7 +598,7 @@ modern-pdf-lib/
|
|
|
488
598
|
git clone https://github.com/ABCrimson/modern-pdf-lib.git
|
|
489
599
|
cd modern-pdf-lib
|
|
490
600
|
npm install
|
|
491
|
-
npm test #
|
|
601
|
+
npm test # 3,997 tests
|
|
492
602
|
npm run typecheck # TypeScript 6.0 strict
|
|
493
603
|
npm run build # ESM + CJS + declarations
|
|
494
604
|
```
|
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
const require_rolldown_runtime = require('./rolldown-runtime-CKhH4XqG.cjs');
|
|
2
|
+
const require_pdfObjects = require('./pdfObjects-1veop1_d.cjs');
|
|
3
|
+
const require_imageExtract = require('./imageExtract-zEb1gnkb.cjs');
|
|
4
|
+
const require_grayscaleDetect = require('./grayscaleDetect-C2m-eEXR.cjs');
|
|
5
|
+
|
|
6
|
+
//#region src/assets/image/iccProfile.ts
|
|
7
|
+
/**
|
|
8
|
+
* @module assets/image/iccProfile
|
|
9
|
+
*
|
|
10
|
+
* ICC color profile extraction and preservation for PDF image XObjects.
|
|
11
|
+
*
|
|
12
|
+
* When images are recompressed during optimization, their ICC color
|
|
13
|
+
* profiles can be stripped, causing color shifts in print workflows.
|
|
14
|
+
* This module extracts ICC profiles from the original image's color
|
|
15
|
+
* space entry and re-embeds them after recompression, preserving
|
|
16
|
+
* color fidelity.
|
|
17
|
+
*
|
|
18
|
+
* No Buffer — uses Uint8Array exclusively.
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* Known ICC color space signatures (4 bytes at offset 16 in the profile header).
|
|
22
|
+
* @internal
|
|
23
|
+
*/
|
|
24
|
+
const ICC_COLOR_SPACE_MAP = {
|
|
25
|
+
"RGB ": "RGB",
|
|
26
|
+
"CMYK": "CMYK",
|
|
27
|
+
"GRAY": "GRAY",
|
|
28
|
+
"Lab ": "Lab",
|
|
29
|
+
"XYZ ": "XYZ",
|
|
30
|
+
"Luv ": "Luv",
|
|
31
|
+
"YCbr": "YCbCr",
|
|
32
|
+
"Yxy ": "Yxy",
|
|
33
|
+
"HSV ": "HSV",
|
|
34
|
+
"HLS ": "HLS"
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Read the color space signature from raw ICC profile data.
|
|
38
|
+
*
|
|
39
|
+
* The ICC profile header stores a 4-byte color space of data field
|
|
40
|
+
* at byte offset 16. This function reads and decodes that signature
|
|
41
|
+
* into a human-readable string.
|
|
42
|
+
*
|
|
43
|
+
* @param data - Raw ICC profile bytes.
|
|
44
|
+
* @returns The color space name (e.g. `'RGB'`, `'CMYK'`, `'GRAY'`),
|
|
45
|
+
* or `'Unknown'` if the data is too short or the signature
|
|
46
|
+
* is not recognized.
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* ```ts
|
|
50
|
+
* import { parseIccColorSpace } from 'modern-pdf-lib';
|
|
51
|
+
*
|
|
52
|
+
* const colorSpace = parseIccColorSpace(iccProfileBytes);
|
|
53
|
+
* console.log(colorSpace); // 'RGB'
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
function parseIccColorSpace(data) {
|
|
57
|
+
if (data.length < 20) return "Unknown";
|
|
58
|
+
return ICC_COLOR_SPACE_MAP[String.fromCharCode(data[16], data[17], data[18], data[19])] ?? "Unknown";
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Parse the human-readable description from an ICC profile's 'desc' tag.
|
|
62
|
+
*
|
|
63
|
+
* Searches the ICC tag table for a tag with signature `'desc'`
|
|
64
|
+
* (0x64657363) and reads the ASCII description string from it.
|
|
65
|
+
*
|
|
66
|
+
* The 'desc' tag (ICC v2) has the structure:
|
|
67
|
+
* - Bytes 0–3: type signature ('desc')
|
|
68
|
+
* - Bytes 4–7: reserved (0)
|
|
69
|
+
* - Bytes 8–11: ASCII description length (uint32 BE)
|
|
70
|
+
* - Bytes 12+: ASCII description string
|
|
71
|
+
*
|
|
72
|
+
* @param data - Raw ICC profile bytes.
|
|
73
|
+
* @returns The description string, or `undefined` if the tag is not
|
|
74
|
+
* found or cannot be parsed.
|
|
75
|
+
*/
|
|
76
|
+
function parseIccDescription(data) {
|
|
77
|
+
if (data.length < 132) return void 0;
|
|
78
|
+
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
79
|
+
const tagCount = view.getUint32(128, false);
|
|
80
|
+
const tagTableStart = 132;
|
|
81
|
+
if (data.length < tagTableStart + tagCount * 12) return void 0;
|
|
82
|
+
for (let i = 0; i < tagCount; i++) {
|
|
83
|
+
const entryOffset = tagTableStart + i * 12;
|
|
84
|
+
if (view.getUint32(entryOffset, false) !== 1684370275) continue;
|
|
85
|
+
const tagOffset = view.getUint32(entryOffset + 4, false);
|
|
86
|
+
const tagSize = view.getUint32(entryOffset + 8, false);
|
|
87
|
+
if (tagOffset + tagSize > data.length) return void 0;
|
|
88
|
+
if (tagSize < 12) return void 0;
|
|
89
|
+
const descLen = view.getUint32(tagOffset + 8, false);
|
|
90
|
+
if (descLen === 0) return void 0;
|
|
91
|
+
const strEnd = Math.min(tagOffset + 12 + descLen - 1, tagOffset + tagSize);
|
|
92
|
+
if (strEnd <= tagOffset + 12) return void 0;
|
|
93
|
+
const chars = [];
|
|
94
|
+
for (let j = tagOffset + 12; j < strEnd; j++) {
|
|
95
|
+
const c = data[j];
|
|
96
|
+
if (c === 0) break;
|
|
97
|
+
chars.push(String.fromCharCode(c));
|
|
98
|
+
}
|
|
99
|
+
return chars.length > 0 ? chars.join("") : void 0;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Extract the ICC color profile from a PDF image XObject's `/ColorSpace`.
|
|
104
|
+
*
|
|
105
|
+
* Checks whether the image's `/ColorSpace` entry is an ICCBased array
|
|
106
|
+
* (i.e. `[/ICCBased <stream ref>]`), and if so, extracts the raw ICC
|
|
107
|
+
* profile bytes and metadata from the referenced stream.
|
|
108
|
+
*
|
|
109
|
+
* @param stream - The `PdfStream` for the image XObject.
|
|
110
|
+
* @param registry - The document's `PdfObjectRegistry` for resolving
|
|
111
|
+
* indirect references.
|
|
112
|
+
* @returns An `IccProfile` if the image uses an ICCBased color space,
|
|
113
|
+
* or `undefined` if no ICC profile is attached.
|
|
114
|
+
*
|
|
115
|
+
* @example
|
|
116
|
+
* ```ts
|
|
117
|
+
* import { extractIccProfile, extractImages, loadPdf } from 'modern-pdf-lib';
|
|
118
|
+
*
|
|
119
|
+
* const doc = await loadPdf(pdfBytes);
|
|
120
|
+
* const images = extractImages(doc);
|
|
121
|
+
*
|
|
122
|
+
* for (const img of images) {
|
|
123
|
+
* const profile = extractIccProfile(img.stream, doc.getRegistry());
|
|
124
|
+
* if (profile) {
|
|
125
|
+
* console.log(`ICC: ${profile.colorSpace}, ${profile.components} channels`);
|
|
126
|
+
* console.log(`Description: ${profile.description ?? 'none'}`);
|
|
127
|
+
* }
|
|
128
|
+
* }
|
|
129
|
+
* ```
|
|
130
|
+
*/
|
|
131
|
+
function extractIccProfile(stream, registry) {
|
|
132
|
+
const csEntry = stream.dict.get("/ColorSpace");
|
|
133
|
+
if (!csEntry) return void 0;
|
|
134
|
+
let cs = csEntry;
|
|
135
|
+
if (cs.kind === "ref") {
|
|
136
|
+
const resolved = registry.resolve(cs);
|
|
137
|
+
if (!resolved) return void 0;
|
|
138
|
+
cs = resolved;
|
|
139
|
+
}
|
|
140
|
+
if (cs.kind !== "array") return void 0;
|
|
141
|
+
const arr = cs;
|
|
142
|
+
if (arr.items.length < 2) return void 0;
|
|
143
|
+
const first = arr.items[0];
|
|
144
|
+
if (first.kind !== "name") return void 0;
|
|
145
|
+
if (first.value !== "/ICCBased") return void 0;
|
|
146
|
+
let profileObj = arr.items[1];
|
|
147
|
+
if (profileObj.kind === "ref") {
|
|
148
|
+
const resolved = registry.resolve(profileObj);
|
|
149
|
+
if (!resolved) return void 0;
|
|
150
|
+
profileObj = resolved;
|
|
151
|
+
}
|
|
152
|
+
if (profileObj.kind !== "stream") return void 0;
|
|
153
|
+
const profileStream = profileObj;
|
|
154
|
+
const profileData = profileStream.data;
|
|
155
|
+
const nEntry = profileStream.dict.get("/N");
|
|
156
|
+
const components = nEntry && nEntry.kind === "number" ? nEntry.value : 0;
|
|
157
|
+
if (components < 1 || components > 4) return void 0;
|
|
158
|
+
return {
|
|
159
|
+
data: profileData,
|
|
160
|
+
components,
|
|
161
|
+
colorSpace: parseIccColorSpace(profileData),
|
|
162
|
+
description: parseIccDescription(profileData)
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Embed an ICC color profile into the PDF object registry and return
|
|
167
|
+
* a reference that can be used as a `/ColorSpace` entry.
|
|
168
|
+
*
|
|
169
|
+
* Creates a new `PdfStream` for the ICC profile data with the required
|
|
170
|
+
* `/N` (number of components) entry, registers it, and returns a
|
|
171
|
+
* `PdfRef` to the stream. The caller should then set the image's
|
|
172
|
+
* `/ColorSpace` to `[/ICCBased <returned ref>]`.
|
|
173
|
+
*
|
|
174
|
+
* @param profile - The `IccProfile` to embed.
|
|
175
|
+
* @param registry - The document's `PdfObjectRegistry`.
|
|
176
|
+
* @returns A `PdfRef` pointing to the newly created ICC profile stream.
|
|
177
|
+
*
|
|
178
|
+
* @example
|
|
179
|
+
* ```ts
|
|
180
|
+
* import { embedIccProfile, extractIccProfile } from 'modern-pdf-lib';
|
|
181
|
+
*
|
|
182
|
+
* const profile = extractIccProfile(imageStream, registry);
|
|
183
|
+
* if (profile) {
|
|
184
|
+
* const profileRef = embedIccProfile(profile, registry);
|
|
185
|
+
* const colorSpace = PdfArray.of([PdfName.of('/ICCBased'), profileRef]);
|
|
186
|
+
* imageStream.dict.set('/ColorSpace', colorSpace);
|
|
187
|
+
* }
|
|
188
|
+
* ```
|
|
189
|
+
*/
|
|
190
|
+
function embedIccProfile(profile, registry) {
|
|
191
|
+
const dict = new require_pdfObjects.PdfDict();
|
|
192
|
+
dict.set("/N", require_pdfObjects.PdfNumber.of(profile.components));
|
|
193
|
+
dict.set("/Length", require_pdfObjects.PdfNumber.of(profile.data.length));
|
|
194
|
+
if (profile.components === 1) dict.set("/Alternate", require_pdfObjects.PdfName.of("/DeviceGray"));
|
|
195
|
+
else if (profile.components === 3) dict.set("/Alternate", require_pdfObjects.PdfName.of("/DeviceRGB"));
|
|
196
|
+
else if (profile.components === 4) dict.set("/Alternate", require_pdfObjects.PdfName.of("/DeviceCMYK"));
|
|
197
|
+
const stream = new require_pdfObjects.PdfStream(dict, profile.data);
|
|
198
|
+
return registry.register(stream);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
//#endregion
|
|
202
|
+
//#region src/assets/image/batchOptimize.ts
|
|
203
|
+
var batchOptimize_exports = /* @__PURE__ */ require_rolldown_runtime.__exportAll({ optimizeAllImages: () => optimizeAllImages });
|
|
204
|
+
/** Minimum image size to bother optimizing (10 KB). */
|
|
205
|
+
const SMALL_IMAGE_THRESHOLD = 10240;
|
|
206
|
+
/**
|
|
207
|
+
* Process items concurrently with a maximum parallelism limit.
|
|
208
|
+
* Workers pull from a shared index — no item is processed twice.
|
|
209
|
+
*/
|
|
210
|
+
async function processWithConcurrency(items, concurrency, processor) {
|
|
211
|
+
let nextIndex = 0;
|
|
212
|
+
const workers = Array.from({ length: Math.min(concurrency, items.length) }, async () => {
|
|
213
|
+
while (nextIndex < items.length) {
|
|
214
|
+
const index = nextIndex++;
|
|
215
|
+
await processor(items[index], index);
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
await Promise.all(workers);
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Optimize all images in a PDF document by recompressing them as JPEG.
|
|
222
|
+
*
|
|
223
|
+
* Walks every image XObject in the document, decodes its pixel data,
|
|
224
|
+
* recompresses it as JPEG using the WASM encoder (if available), and
|
|
225
|
+
* replaces the stream data in-place when the result is smaller.
|
|
226
|
+
*
|
|
227
|
+
* **Requires the JPEG WASM module to be initialized** via
|
|
228
|
+
* `initJpegWasm()` or `initWasm({ jpeg: true })`. Without it,
|
|
229
|
+
* no images will be optimized (all will be skipped).
|
|
230
|
+
*
|
|
231
|
+
* @param doc - A parsed `PdfDocument` (from `loadPdf()`).
|
|
232
|
+
* @param options - Optimization settings.
|
|
233
|
+
* @returns A report summarizing the optimization results.
|
|
234
|
+
*
|
|
235
|
+
* @example
|
|
236
|
+
* ```ts
|
|
237
|
+
* import { loadPdf, initWasm, optimizeAllImages } from 'modern-pdf-lib';
|
|
238
|
+
*
|
|
239
|
+
* await initWasm({ jpeg: true });
|
|
240
|
+
*
|
|
241
|
+
* const doc = await loadPdf(pdfBytes);
|
|
242
|
+
* const report = await optimizeAllImages(doc);
|
|
243
|
+
*
|
|
244
|
+
* console.log(`Optimized ${report.optimizedImages} of ${report.totalImages} images`);
|
|
245
|
+
* console.log(`Savings: ${report.savings.toFixed(1)}%`);
|
|
246
|
+
*
|
|
247
|
+
* const optimizedBytes = await doc.save();
|
|
248
|
+
* ```
|
|
249
|
+
*/
|
|
250
|
+
async function optimizeAllImages(doc, options = {}) {
|
|
251
|
+
const quality = options.quality ?? 80;
|
|
252
|
+
const minSavingsPercent = options.minSavingsPercent ?? 10;
|
|
253
|
+
const skipSmall = options.skipSmallImages ?? false;
|
|
254
|
+
const progressive = options.progressive ?? false;
|
|
255
|
+
const chromaSubsampling = options.chromaSubsampling ?? "4:2:0";
|
|
256
|
+
const concurrency = Math.max(1, options.concurrency ?? 1);
|
|
257
|
+
const { pageRange, minImageSize, colorSpaces, namePattern } = options;
|
|
258
|
+
const { encodeJpegWasm, isJpegWasmReady } = await Promise.resolve().then(() => require("./bridge-DYCQzxF7.cjs")).then((n) => n.bridge_exports);
|
|
259
|
+
const { decodeJpegWasm } = await Promise.resolve().then(() => require("./bridge-DYCQzxF7.cjs")).then((n) => n.bridge_exports);
|
|
260
|
+
const images = require_imageExtract.extractImages(doc);
|
|
261
|
+
const registry = doc.getRegistry();
|
|
262
|
+
const results = new Array(images.length);
|
|
263
|
+
/** Process a single image and write the result into `results[index]`. */
|
|
264
|
+
const processImage = async (img, index) => {
|
|
265
|
+
const skip = (skippedByFilter, reason) => {
|
|
266
|
+
results[index] = {
|
|
267
|
+
name: img.name,
|
|
268
|
+
pageIndex: img.pageIndex,
|
|
269
|
+
originalSize: img.compressedSize,
|
|
270
|
+
newSize: img.compressedSize,
|
|
271
|
+
skipped: true,
|
|
272
|
+
skippedByFilter,
|
|
273
|
+
reason
|
|
274
|
+
};
|
|
275
|
+
};
|
|
276
|
+
if (pageRange && (img.pageIndex < pageRange.start || img.pageIndex > pageRange.end)) return skip(true, `Page ${img.pageIndex} outside range [${pageRange.start}, ${pageRange.end}]`);
|
|
277
|
+
if (minImageSize && img.compressedSize < minImageSize) return skip(true, `Compressed size ${img.compressedSize} below minimum ${minImageSize} bytes`);
|
|
278
|
+
if (colorSpaces && !colorSpaces.includes(img.colorSpace)) return skip(true, `Color space '${img.colorSpace}' not in allowed list`);
|
|
279
|
+
if (namePattern && !namePattern.test(img.name)) return skip(true, `Name '${img.name}' does not match pattern ${namePattern}`);
|
|
280
|
+
if (!isJpegWasmReady()) return skip(false, "JPEG WASM encoder not initialized");
|
|
281
|
+
if (skipSmall && img.compressedSize < SMALL_IMAGE_THRESHOLD) return skip(false, `Below size threshold (${SMALL_IMAGE_THRESHOLD} bytes)`);
|
|
282
|
+
if (img.bitsPerComponent !== 8) return skip(false, `Unsupported bits per component: ${img.bitsPerComponent}`);
|
|
283
|
+
if (img.colorSpace === "Indexed") return skip(false, "Indexed color space not suitable for JPEG");
|
|
284
|
+
let iccProfile;
|
|
285
|
+
try {
|
|
286
|
+
iccProfile = extractIccProfile(img.stream, registry);
|
|
287
|
+
} catch {
|
|
288
|
+
iccProfile = void 0;
|
|
289
|
+
}
|
|
290
|
+
let pixels;
|
|
291
|
+
let channels = img.channels;
|
|
292
|
+
try {
|
|
293
|
+
if (img.filters[0] === "DCTDecode") {
|
|
294
|
+
const decoded = decodeJpegWasm(img.stream.data);
|
|
295
|
+
if (!decoded) return skip(false, "Failed to decode existing JPEG");
|
|
296
|
+
pixels = decoded.pixels;
|
|
297
|
+
channels = decoded.channels;
|
|
298
|
+
} else pixels = require_imageExtract.decodeImageStream(img);
|
|
299
|
+
} catch {
|
|
300
|
+
return skip(false, "Failed to decode image stream");
|
|
301
|
+
}
|
|
302
|
+
const expectedLen = img.width * img.height * channels;
|
|
303
|
+
if (pixels.length !== expectedLen) return skip(false, `Pixel data length mismatch: got ${pixels.length}, expected ${expectedLen}`);
|
|
304
|
+
if (channels === 4 && img.colorSpace === "DeviceCMYK") {
|
|
305
|
+
const rgb = new Uint8Array(img.width * img.height * 3);
|
|
306
|
+
for (let i = 0; i < img.width * img.height; i++) {
|
|
307
|
+
const c = pixels[i * 4] / 255;
|
|
308
|
+
const m = pixels[i * 4 + 1] / 255;
|
|
309
|
+
const y = pixels[i * 4 + 2] / 255;
|
|
310
|
+
const k = pixels[i * 4 + 3] / 255;
|
|
311
|
+
rgb[i * 3] = Math.round(255 * (1 - c) * (1 - k));
|
|
312
|
+
rgb[i * 3 + 1] = Math.round(255 * (1 - m) * (1 - k));
|
|
313
|
+
rgb[i * 3 + 2] = Math.round(255 * (1 - y) * (1 - k));
|
|
314
|
+
}
|
|
315
|
+
pixels = rgb;
|
|
316
|
+
channels = 3;
|
|
317
|
+
}
|
|
318
|
+
let convertedToGrayscale = false;
|
|
319
|
+
if (options.autoGrayscale && (channels === 3 || channels === 4)) {
|
|
320
|
+
if (require_grayscaleDetect.isGrayscaleImage(pixels, img.width, img.height, channels)) {
|
|
321
|
+
pixels = require_grayscaleDetect.convertToGrayscale(pixels, img.width, img.height, channels);
|
|
322
|
+
channels = 1;
|
|
323
|
+
convertedToGrayscale = true;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
const jpegBytes = encodeJpegWasm(pixels, img.width, img.height, channels, quality, progressive, chromaSubsampling);
|
|
327
|
+
if (!jpegBytes) return skip(false, "JPEG encoding failed");
|
|
328
|
+
const savingsPercent = (img.compressedSize - jpegBytes.length) / img.compressedSize * 100;
|
|
329
|
+
if (savingsPercent < minSavingsPercent) return skip(false, `Savings ${savingsPercent.toFixed(1)}% below threshold ${minSavingsPercent}%`);
|
|
330
|
+
img.stream.data = jpegBytes;
|
|
331
|
+
img.stream.syncLength();
|
|
332
|
+
const dict = img.stream.dict;
|
|
333
|
+
dict.set("/Filter", require_pdfObjects.PdfName.of("/DCTDecode"));
|
|
334
|
+
if (channels === 1) dict.set("/ColorSpace", require_pdfObjects.PdfName.of("/DeviceGray"));
|
|
335
|
+
else if (iccProfile && !convertedToGrayscale && iccProfile.components === channels) {
|
|
336
|
+
const profileRef = embedIccProfile(iccProfile, registry);
|
|
337
|
+
dict.set("/ColorSpace", require_pdfObjects.PdfArray.of([require_pdfObjects.PdfName.of("/ICCBased"), profileRef]));
|
|
338
|
+
} else if (img.colorSpace === "DeviceCMYK" && channels === 3) dict.set("/ColorSpace", require_pdfObjects.PdfName.of("/DeviceRGB"));
|
|
339
|
+
else if (channels === 3) dict.set("/ColorSpace", require_pdfObjects.PdfName.of("/DeviceRGB"));
|
|
340
|
+
dict.delete("/DecodeParms");
|
|
341
|
+
if (img.colorSpace === "DeviceCMYK") dict.delete("/Decode");
|
|
342
|
+
results[index] = {
|
|
343
|
+
name: img.name,
|
|
344
|
+
pageIndex: img.pageIndex,
|
|
345
|
+
originalSize: img.compressedSize,
|
|
346
|
+
newSize: jpegBytes.length,
|
|
347
|
+
skipped: false,
|
|
348
|
+
skippedByFilter: false
|
|
349
|
+
};
|
|
350
|
+
};
|
|
351
|
+
await processWithConcurrency(images, concurrency, processImage);
|
|
352
|
+
let totalOriginal = 0;
|
|
353
|
+
let totalNew = 0;
|
|
354
|
+
let optimizedCount = 0;
|
|
355
|
+
let skippedByFilterCount = 0;
|
|
356
|
+
let cumulativeSavedBytes = 0;
|
|
357
|
+
const { onProgress } = options;
|
|
358
|
+
for (let i = 0; i < results.length; i++) {
|
|
359
|
+
const entry = results[i];
|
|
360
|
+
totalOriginal += entry.originalSize;
|
|
361
|
+
totalNew += entry.newSize;
|
|
362
|
+
if (!entry.skipped) optimizedCount++;
|
|
363
|
+
if (entry.skippedByFilter) skippedByFilterCount++;
|
|
364
|
+
if (onProgress) {
|
|
365
|
+
const saved = entry.originalSize - entry.newSize;
|
|
366
|
+
cumulativeSavedBytes += saved;
|
|
367
|
+
onProgress({
|
|
368
|
+
current: i + 1,
|
|
369
|
+
total: images.length,
|
|
370
|
+
imageName: entry.name,
|
|
371
|
+
pageIndex: entry.pageIndex,
|
|
372
|
+
savedBytes: saved,
|
|
373
|
+
totalSavedBytes: cumulativeSavedBytes,
|
|
374
|
+
skipped: entry.skipped
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
const overallSavings = totalOriginal > 0 ? (totalOriginal - totalNew) / totalOriginal * 100 : 0;
|
|
379
|
+
return {
|
|
380
|
+
totalImages: images.length,
|
|
381
|
+
optimizedImages: optimizedCount,
|
|
382
|
+
skippedByFilter: skippedByFilterCount,
|
|
383
|
+
originalTotalBytes: totalOriginal,
|
|
384
|
+
optimizedTotalBytes: totalNew,
|
|
385
|
+
savings: overallSavings,
|
|
386
|
+
perImage: results
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
//#endregion
|
|
391
|
+
Object.defineProperty(exports, 'batchOptimize_exports', {
|
|
392
|
+
enumerable: true,
|
|
393
|
+
get: function () {
|
|
394
|
+
return batchOptimize_exports;
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
Object.defineProperty(exports, 'embedIccProfile', {
|
|
398
|
+
enumerable: true,
|
|
399
|
+
get: function () {
|
|
400
|
+
return embedIccProfile;
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
Object.defineProperty(exports, 'extractIccProfile', {
|
|
404
|
+
enumerable: true,
|
|
405
|
+
get: function () {
|
|
406
|
+
return extractIccProfile;
|
|
407
|
+
}
|
|
408
|
+
});
|
|
409
|
+
Object.defineProperty(exports, 'optimizeAllImages', {
|
|
410
|
+
enumerable: true,
|
|
411
|
+
get: function () {
|
|
412
|
+
return optimizeAllImages;
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
Object.defineProperty(exports, 'parseIccColorSpace', {
|
|
416
|
+
enumerable: true,
|
|
417
|
+
get: function () {
|
|
418
|
+
return parseIccColorSpace;
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
Object.defineProperty(exports, 'parseIccDescription', {
|
|
422
|
+
enumerable: true,
|
|
423
|
+
get: function () {
|
|
424
|
+
return parseIccDescription;
|
|
425
|
+
}
|
|
426
|
+
});
|
|
427
|
+
//# sourceMappingURL=batchOptimize-Ba_pWw71.cjs.map
|