modern-pdf-lib 0.15.1 → 0.19.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 +106 -7
- package/dist/batchOptimize-7U_kD3_j.mjs +392 -0
- package/dist/batchOptimize-xo6BXbGZ.cjs +427 -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-BBv4BkQP.d.mts +261 -0
- package/dist/compressionAnalysis-BBv4BkQP.d.mts.map +1 -0
- package/dist/compressionAnalysis-Bw2alOxt.mjs +1490 -0
- package/dist/compressionAnalysis-CtJ2X9l2.d.cts +261 -0
- package/dist/compressionAnalysis-CtJ2X9l2.d.cts.map +1 -0
- package/dist/compressionAnalysis-eXYyDsrh.cjs +1525 -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-BtJ5tlrr.cjs → deduplicateImages-B5lmzL9j.cjs} +2 -2
- package/dist/{deduplicateImages-BfpjHY9b.mjs → deduplicateImages-BX3Zg8Qp.mjs} +3 -3
- 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-B6OvUEp-.mjs +155 -0
- package/dist/imageExtract-PxdBvpHj.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-BZ8tTeAk.mjs +438 -0
- package/dist/layout-Inbqegsk.cjs +563 -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-BSiQdNZq.d.cts +4640 -0
- package/dist/pdfDocument-BSiQdNZq.d.cts.map +1 -0
- package/dist/pdfDocument-DOg240g9.mjs +13685 -0
- package/dist/pdfDocument-Duf9LelM.cjs +14110 -0
- package/dist/pdfDocument-i6U5fQ91.d.mts +4640 -0
- package/dist/pdfDocument-i6U5fQ91.d.mts.map +1 -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-BacMkrLe.mjs} +3024 -5
- package/dist/{pdfPage-Dz_SVKUS.cjs → pdfPage-CirlQRzJ.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-BLWEEbd7.cjs +11852 -0
- package/dist/src-x0g7wiRq.mjs +11103 -0
- package/dist/{imageExtract-BC7TMY98.cjs → streamDecode-Bs0_MT_Q.cjs} +2 -165
- package/dist/{imageExtract-vjyQyFcT.mjs → streamDecode-CWN-nfPJ.mjs} +3 -154
- 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
|
|
@@ -101,13 +139,14 @@ const blob = await doc.saveAsBlob(); // Blob (browsers)
|
|
|
101
139
|
<td width="50%" valign="top">
|
|
102
140
|
|
|
103
141
|
**Advanced**
|
|
142
|
+
- QR codes & barcodes (9 formats)
|
|
143
|
+
- Table layout engine with pagination
|
|
104
144
|
- Outlines / bookmarks
|
|
105
145
|
- Optional content layers (OCGs)
|
|
106
|
-
- File attachments
|
|
107
|
-
- Watermarks
|
|
146
|
+
- File attachments & watermarks
|
|
108
147
|
- Linearization (fast web view)
|
|
109
|
-
-
|
|
110
|
-
-
|
|
148
|
+
- Browser helpers (download, blob, data URL)
|
|
149
|
+
- Service Worker & Web Worker support
|
|
111
150
|
- CLI: `npx modern-pdf optimize`
|
|
112
151
|
|
|
113
152
|
</td>
|
|
@@ -197,6 +236,18 @@ const blob = await doc.saveAsBlob(); // Blob (browsers)
|
|
|
197
236
|
<td align="center">Yes</td>
|
|
198
237
|
<td align="center">No</td></tr>
|
|
199
238
|
|
|
239
|
+
<tr><td><strong>QR codes & barcodes</strong></td>
|
|
240
|
+
<td align="center">9 formats (QR, EAN, Code 128, PDF417…)</td>
|
|
241
|
+
<td align="center">No</td></tr>
|
|
242
|
+
|
|
243
|
+
<tr><td><strong>Table layout</strong></td>
|
|
244
|
+
<td align="center">Spanning, pagination, presets, overflow</td>
|
|
245
|
+
<td align="center">No</td></tr>
|
|
246
|
+
|
|
247
|
+
<tr><td><strong>Browser utilities</strong></td>
|
|
248
|
+
<td align="center">Download, blob, Web Worker, Service Worker</td>
|
|
249
|
+
<td align="center">No</td></tr>
|
|
250
|
+
|
|
200
251
|
<tr><td><strong>Image optimization</strong></td>
|
|
201
252
|
<td align="center">JPEG recompress, dedup, grayscale</td>
|
|
202
253
|
<td align="center">No</td></tr>
|
|
@@ -390,6 +441,51 @@ npx modern-pdf optimize report.pdf report-opt.pdf --quality 60 --grayscale --ded
|
|
|
390
441
|
```
|
|
391
442
|
</details>
|
|
392
443
|
|
|
444
|
+
<details>
|
|
445
|
+
<summary><strong>Tables</strong> — layout engine with spanning, pagination, presets</summary>
|
|
446
|
+
|
|
447
|
+
```ts
|
|
448
|
+
import { createPdf, PageSizes, professionalPreset, applyPreset } from 'modern-pdf-lib';
|
|
449
|
+
|
|
450
|
+
const doc = createPdf();
|
|
451
|
+
const page = doc.addPage(PageSizes.A4);
|
|
452
|
+
|
|
453
|
+
page.drawTable(applyPreset(professionalPreset(), {
|
|
454
|
+
x: 50,
|
|
455
|
+
y: 750,
|
|
456
|
+
width: 495,
|
|
457
|
+
headerRows: 1,
|
|
458
|
+
rows: [
|
|
459
|
+
{ cells: ['Product', 'Qty', 'Price', 'Total'] },
|
|
460
|
+
{ cells: ['Widget A', '10', '$5.00', '$50.00'] },
|
|
461
|
+
{ cells: ['Widget B', '25', '$3.50', '$87.50'] },
|
|
462
|
+
{ cells: [{ content: 'Grand Total', colSpan: 3, align: 'right' }, '$137.50'] },
|
|
463
|
+
],
|
|
464
|
+
columns: [{ flex: 2 }, { width: 60 }, { width: 80 }, { width: 80, align: 'right' }],
|
|
465
|
+
}));
|
|
466
|
+
```
|
|
467
|
+
</details>
|
|
468
|
+
|
|
469
|
+
<details>
|
|
470
|
+
<summary><strong>QR Codes & Barcodes</strong> — 9 formats</summary>
|
|
471
|
+
|
|
472
|
+
```ts
|
|
473
|
+
import { createPdf, PageSizes } from 'modern-pdf-lib';
|
|
474
|
+
|
|
475
|
+
const doc = createPdf();
|
|
476
|
+
const page = doc.addPage(PageSizes.A4);
|
|
477
|
+
|
|
478
|
+
// QR code
|
|
479
|
+
page.drawQrCode('https://example.com', { x: 50, y: 700, size: 120 });
|
|
480
|
+
|
|
481
|
+
// Barcodes (Code 128, EAN-13, UPC-A, Code 39, ITF, PDF417, Data Matrix)
|
|
482
|
+
import { encodeCode128, encodeEan13, renderStyledBarcode } from 'modern-pdf-lib';
|
|
483
|
+
|
|
484
|
+
const barcode = encodeCode128('ABC-12345');
|
|
485
|
+
const ops = renderStyledBarcode(barcode, { x: 50, y: 500, height: 60 });
|
|
486
|
+
```
|
|
487
|
+
</details>
|
|
488
|
+
|
|
393
489
|
<details>
|
|
394
490
|
<summary><strong>PDF/A & Accessibility</strong></summary>
|
|
395
491
|
|
|
@@ -471,12 +567,15 @@ modern-pdf-lib/
|
|
|
471
567
|
crypto/ AES-256, RC4, MD5, SHA-256/384/512
|
|
472
568
|
compression/ Deflate (fflate + optional WASM)
|
|
473
569
|
assets/ Font metrics/embed/subset, image embed, SVG
|
|
570
|
+
barcode/ QR, Code 128, EAN, UPC, Code 39, ITF, PDF417, Data Matrix
|
|
571
|
+
layout/ Table engine (spanning, pagination, presets, overflow)
|
|
572
|
+
browser/ Download helpers, Service Worker, Web Worker
|
|
474
573
|
layers/ Optional content groups (OCG)
|
|
475
574
|
outline/ Bookmarks / document outline
|
|
476
575
|
metadata/ XMP metadata, viewer preferences
|
|
477
576
|
wasm/ Rust crate sources (6 modules)
|
|
478
577
|
cli/ CLI tool (modern-pdf optimize)
|
|
479
|
-
tests/
|
|
578
|
+
tests/ 3,260 tests across 158 suites
|
|
480
579
|
docs/ VitePress documentation
|
|
481
580
|
```
|
|
482
581
|
|
|
@@ -488,7 +587,7 @@ modern-pdf-lib/
|
|
|
488
587
|
git clone https://github.com/ABCrimson/modern-pdf-lib.git
|
|
489
588
|
cd modern-pdf-lib
|
|
490
589
|
npm install
|
|
491
|
-
npm test #
|
|
590
|
+
npm test # 3,260 tests
|
|
492
591
|
npm run typecheck # TypeScript 6.0 strict
|
|
493
592
|
npm run build # ESM + CJS + declarations
|
|
494
593
|
```
|
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
import { Ka as __exportAll } from "./index-BtYOx5wh.d.mts";
|
|
2
|
+
import { i as PdfName, l as PdfStream, o as PdfNumber, r as PdfDict, t as PdfArray } from "./pdfObjects-uEsWlfzU.mjs";
|
|
3
|
+
import { n as extractImages, t as decodeImageStream } from "./imageExtract-B6OvUEp-.mjs";
|
|
4
|
+
import { n as isGrayscaleImage, t as convertToGrayscale } from "./grayscaleDetect-C6kFF3dk.mjs";
|
|
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 PdfDict();
|
|
192
|
+
dict.set("/N", PdfNumber.of(profile.components));
|
|
193
|
+
dict.set("/Length", PdfNumber.of(profile.data.length));
|
|
194
|
+
if (profile.components === 1) dict.set("/Alternate", PdfName.of("/DeviceGray"));
|
|
195
|
+
else if (profile.components === 3) dict.set("/Alternate", PdfName.of("/DeviceRGB"));
|
|
196
|
+
else if (profile.components === 4) dict.set("/Alternate", PdfName.of("/DeviceCMYK"));
|
|
197
|
+
const stream = new 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__ */ __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 import("./bridge-DTH5LMAK.mjs").then((n) => n.t);
|
|
259
|
+
const { decodeJpegWasm } = await import("./bridge-DTH5LMAK.mjs").then((n) => n.t);
|
|
260
|
+
const images = 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 = 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 (isGrayscaleImage(pixels, img.width, img.height, channels)) {
|
|
321
|
+
pixels = 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", PdfName.of("/DCTDecode"));
|
|
334
|
+
if (channels === 1) dict.set("/ColorSpace", PdfName.of("/DeviceGray"));
|
|
335
|
+
else if (iccProfile && !convertedToGrayscale && iccProfile.components === channels) {
|
|
336
|
+
const profileRef = embedIccProfile(iccProfile, registry);
|
|
337
|
+
dict.set("/ColorSpace", PdfArray.of([PdfName.of("/ICCBased"), profileRef]));
|
|
338
|
+
} else if (img.colorSpace === "DeviceCMYK" && channels === 3) dict.set("/ColorSpace", PdfName.of("/DeviceRGB"));
|
|
339
|
+
else if (channels === 3) dict.set("/ColorSpace", 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
|
+
export { parseIccColorSpace as a, extractIccProfile as i, optimizeAllImages as n, parseIccDescription as o, embedIccProfile as r, batchOptimize_exports as t };
|
|
392
|
+
//# sourceMappingURL=batchOptimize-7U_kD3_j.mjs.map
|