modern-pdf-lib 0.15.0 → 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.
Files changed (95) hide show
  1. package/README.md +106 -7
  2. package/dist/batchOptimize-7U_kD3_j.mjs +392 -0
  3. package/dist/batchOptimize-xo6BXbGZ.cjs +427 -0
  4. package/dist/{bridge-C7U4E7St.mjs → bridge-DTH5LMAK.mjs} +3 -3
  5. package/dist/{bridge-DUcJFVsk.cjs → bridge-DYCQzxF7.cjs} +2 -2
  6. package/dist/browser.cjs +621 -0
  7. package/dist/browser.d.cts +190 -0
  8. package/dist/browser.d.cts.map +1 -0
  9. package/dist/browser.d.mts +190 -0
  10. package/dist/browser.d.mts.map +1 -0
  11. package/dist/browser.mjs +212 -0
  12. package/dist/cli/index.cjs +247 -0
  13. package/dist/cli/index.d.cts +1 -0
  14. package/dist/cli/index.d.mts +1 -0
  15. package/dist/cli/index.mjs +248 -0
  16. package/dist/compressionAnalysis-BBv4BkQP.d.mts +261 -0
  17. package/dist/compressionAnalysis-BBv4BkQP.d.mts.map +1 -0
  18. package/dist/compressionAnalysis-Bw2alOxt.mjs +1490 -0
  19. package/dist/compressionAnalysis-CtJ2X9l2.d.cts +261 -0
  20. package/dist/compressionAnalysis-CtJ2X9l2.d.cts.map +1 -0
  21. package/dist/compressionAnalysis-eXYyDsrh.cjs +1525 -0
  22. package/dist/create.cjs +35 -0
  23. package/dist/create.d.cts +3 -0
  24. package/dist/create.d.mts +3 -0
  25. package/dist/create.mjs +5 -0
  26. package/dist/deduplicateImages-B5lmzL9j.cjs +113 -0
  27. package/dist/deduplicateImages-BX3Zg8Qp.mjs +102 -0
  28. package/dist/{fflateAdapter-DX0VqT5k.mjs → fflateAdapter-CBQpGTlx.mjs} +2 -2
  29. package/dist/{fflateAdapter-AHC_S3cb.cjs → fflateAdapter-LTAeAhaD.cjs} +1 -1
  30. package/dist/fieldAppearance-C8PoLFSc.d.mts +136 -0
  31. package/dist/fieldAppearance-C8PoLFSc.d.mts.map +1 -0
  32. package/dist/fieldAppearance-CdiGFG5e.d.cts +136 -0
  33. package/dist/fieldAppearance-CdiGFG5e.d.cts.map +1 -0
  34. package/dist/fontEmbed-Dsu9fo4U.d.mts +636 -0
  35. package/dist/fontEmbed-Dsu9fo4U.d.mts.map +1 -0
  36. package/dist/fontEmbed-LID6yG6g.d.cts +636 -0
  37. package/dist/fontEmbed-LID6yG6g.d.cts.map +1 -0
  38. package/dist/{fontSubset-pFc8Dueu.cjs → fontSubset-5SLWMmEw.cjs} +1 -1
  39. package/dist/{fontSubset-ZpLoOZ2e.mjs → fontSubset-DWpduoY2.mjs} +2 -2
  40. package/dist/forms.cjs +13 -0
  41. package/dist/forms.d.cts +3 -0
  42. package/dist/forms.d.mts +3 -0
  43. package/dist/forms.mjs +3 -0
  44. package/dist/grayscaleDetect-C2m-eEXR.cjs +96 -0
  45. package/dist/grayscaleDetect-C6kFF3dk.mjs +84 -0
  46. package/dist/imageExtract-B6OvUEp-.mjs +155 -0
  47. package/dist/imageExtract-PxdBvpHj.cjs +166 -0
  48. package/dist/index-BtYOx5wh.d.mts +4904 -0
  49. package/dist/index-BtYOx5wh.d.mts.map +1 -0
  50. package/dist/index-bpktKzCA.d.cts +4904 -0
  51. package/dist/index-bpktKzCA.d.cts.map +1 -0
  52. package/dist/index.cjs +288 -25851
  53. package/dist/index.d.cts +7 -9151
  54. package/dist/index.d.mts +7 -9151
  55. package/dist/index.mjs +17 -25665
  56. package/dist/layout-BZ8tTeAk.mjs +438 -0
  57. package/dist/layout-Inbqegsk.cjs +563 -0
  58. package/dist/{libdeflateWasm-Enus0G1k.cjs → libdeflateWasm-BdiDEJOj.cjs} +2 -2
  59. package/dist/{libdeflateWasm-82loOtIV.mjs → libdeflateWasm-rLppXytE.mjs} +3 -3
  60. package/dist/loader-3u6Tw5T-.mjs +328 -0
  61. package/dist/loader-I4zdkoWc.cjs +393 -0
  62. package/dist/parse.cjs +24 -0
  63. package/dist/parse.d.cts +4 -0
  64. package/dist/parse.d.mts +4 -0
  65. package/dist/parse.mjs +7 -0
  66. package/dist/pdfCatalog-CYy4NXEY.cjs +173 -0
  67. package/dist/pdfCatalog-IImGcMbR.mjs +138 -0
  68. package/dist/pdfDocument-BSiQdNZq.d.cts +4640 -0
  69. package/dist/pdfDocument-BSiQdNZq.d.cts.map +1 -0
  70. package/dist/pdfDocument-DOg240g9.mjs +13685 -0
  71. package/dist/pdfDocument-Duf9LelM.cjs +14110 -0
  72. package/dist/pdfDocument-i6U5fQ91.d.mts +4640 -0
  73. package/dist/pdfDocument-i6U5fQ91.d.mts.map +1 -0
  74. package/dist/pdfForm-9gd40uz9.cjs +1796 -0
  75. package/dist/pdfForm-BiyNtYem.d.mts +905 -0
  76. package/dist/pdfForm-BiyNtYem.d.mts.map +1 -0
  77. package/dist/pdfForm-Cn-cVicP.mjs +1695 -0
  78. package/dist/pdfForm-SOXJ72LW.d.cts +905 -0
  79. package/dist/pdfForm-SOXJ72LW.d.cts.map +1 -0
  80. package/dist/{pdfCatalog-COKoYQ8C.cjs → pdfObjects-1veop1_d.cjs} +2 -172
  81. package/dist/{pdfCatalog-BB2Wnmud.mjs → pdfObjects-uEsWlfzU.mjs} +3 -138
  82. package/dist/{pdfPage-N1K2U3jI.mjs → pdfPage-BacMkrLe.mjs} +3024 -4
  83. package/dist/{pdfPage-DBfdinTR.cjs → pdfPage-CirlQRzJ.cjs} +3148 -104
  84. package/dist/{pngEmbed-gaJ9S2Dk.mjs → pngEmbed-BLj2zi-5.mjs} +3 -3
  85. package/dist/{pngEmbed-10m4CfBU.cjs → pngEmbed-D4X4ZN-3.cjs} +2 -2
  86. package/dist/src-BLWEEbd7.cjs +11852 -0
  87. package/dist/src-x0g7wiRq.mjs +11103 -0
  88. package/dist/streamDecode-Bs0_MT_Q.cjs +4607 -0
  89. package/dist/streamDecode-CWN-nfPJ.mjs +4596 -0
  90. package/package.json +33 -1
  91. package/dist/index.d.cts.map +0 -1
  92. package/dist/index.d.mts.map +0 -1
  93. package/dist/loader-1VJXLlMZ.mjs +0 -164
  94. package/dist/loader-CKlBOHma.cjs +0 -166
  95. 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
  [![npm version](https://img.shields.io/npm/v/modern-pdf-lib?style=flat-square&color=cb3837)](https://www.npmjs.com/package/modern-pdf-lib)
17
17
  [![bundle size](https://img.shields.io/badge/gzip-36kb_core-blue?style=flat-square)](https://bundlephobia.com/package/modern-pdf-lib)
18
- [![tests](https://img.shields.io/badge/tests-2%2C323_passing-brightgreen?style=flat-square)](#)
18
+ [![tests](https://img.shields.io/badge/tests-3%2C260_passing-brightgreen?style=flat-square)](#)
19
19
  [![TypeScript](https://img.shields.io/badge/TypeScript-6.0-3178c6?style=flat-square&logo=typescript&logoColor=white)](#)
20
20
  [![License: MIT](https://img.shields.io/badge/license-MIT-yellow?style=flat-square)](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
- - 60+ low-level PDF operators
110
- - Custom appearance providers
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> &mdash; 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> &mdash; 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/ 2,323 tests across 110 suites
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 # 2,323 tests
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