modern-pdf-lib 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +455 -0
- package/dist/documentMerge-CNPWlWic.mjs +18351 -0
- package/dist/documentMerge-DnLzOg5P.cjs +18878 -0
- package/dist/fflateAdapter-D2mv_ttM.mjs +196 -0
- package/dist/fflateAdapter-cT4YeY_h.cjs +207 -0
- package/dist/fontSubset-BOGts8y9.mjs +203 -0
- package/dist/fontSubset-C0Rm9ih6.cjs +226 -0
- package/dist/index.cjs +4597 -0
- package/dist/index.d.cts +7898 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +7898 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +4306 -0
- package/dist/libdeflateWasm-QVHmuzw-.mjs +220 -0
- package/dist/libdeflateWasm-to2bG6NG.cjs +237 -0
- package/dist/loader-D9LTYmrX.mjs +162 -0
- package/dist/loader-mwt5wRJz.cjs +164 -0
- package/dist/pdfCatalog-DTXk0tbK.cjs +627 -0
- package/dist/pdfCatalog-Dk4qUVvx.mjs +532 -0
- package/dist/pdfPage-C9vw_D1J.cjs +5203 -0
- package/dist/pdfPage-DZA6XJzR.mjs +4544 -0
- package/dist/pngEmbed-BN-gMJrb.cjs +536 -0
- package/dist/pngEmbed-DgeNWlbS.mjs +525 -0
- package/dist/rolldown-runtime-95iHPtFO.mjs +18 -0
- package/dist/rolldown-runtime-CKhH4XqG.cjs +24 -0
- package/package.json +94 -0
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
import { t as __exportAll } from "./rolldown-runtime-95iHPtFO.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/assets/image/pngEmbed.ts
|
|
4
|
+
var pngEmbed_exports = /* @__PURE__ */ __exportAll({
|
|
5
|
+
embedPng: () => embedPng,
|
|
6
|
+
initPngWasm: () => initPngWasm
|
|
7
|
+
});
|
|
8
|
+
/** PNG file signature (8 bytes). */
|
|
9
|
+
const PNG_SIGNATURE = new Uint8Array([
|
|
10
|
+
137,
|
|
11
|
+
80,
|
|
12
|
+
78,
|
|
13
|
+
71,
|
|
14
|
+
13,
|
|
15
|
+
10,
|
|
16
|
+
26,
|
|
17
|
+
10
|
|
18
|
+
]);
|
|
19
|
+
/** Color type constants from the PNG spec. */
|
|
20
|
+
var PngColorType = /* @__PURE__ */ function(PngColorType) {
|
|
21
|
+
PngColorType[PngColorType["Grayscale"] = 0] = "Grayscale";
|
|
22
|
+
PngColorType[PngColorType["RGB"] = 2] = "RGB";
|
|
23
|
+
PngColorType[PngColorType["Indexed"] = 3] = "Indexed";
|
|
24
|
+
PngColorType[PngColorType["GrayscaleAlpha"] = 4] = "GrayscaleAlpha";
|
|
25
|
+
PngColorType[PngColorType["RGBA"] = 6] = "RGBA";
|
|
26
|
+
return PngColorType;
|
|
27
|
+
}(PngColorType || {});
|
|
28
|
+
/**
|
|
29
|
+
* Validate the PNG signature.
|
|
30
|
+
*
|
|
31
|
+
* @param data - The raw PNG bytes.
|
|
32
|
+
* @throws If the signature does not match.
|
|
33
|
+
*/
|
|
34
|
+
function validatePngSignature(data) {
|
|
35
|
+
if (data.length < 8) throw new Error("PNG data too small — expected at least 8 bytes for the signature");
|
|
36
|
+
for (let i = 0; i < 8; i++) if (data[i] !== PNG_SIGNATURE[i]) throw new Error("Invalid PNG signature");
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Iterate over all chunks in a PNG file.
|
|
40
|
+
*
|
|
41
|
+
* @param data - The raw PNG bytes.
|
|
42
|
+
* @returns An array of parsed chunks.
|
|
43
|
+
*/
|
|
44
|
+
function parseChunks(data) {
|
|
45
|
+
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
46
|
+
const chunks = [];
|
|
47
|
+
let offset = 8;
|
|
48
|
+
while (offset < data.length) {
|
|
49
|
+
if (offset + 8 > data.length) break;
|
|
50
|
+
const length = view.getUint32(offset, false);
|
|
51
|
+
const type = String.fromCharCode(data[offset + 4], data[offset + 5], data[offset + 6], data[offset + 7]);
|
|
52
|
+
const chunkData = data.slice(offset + 8, offset + 8 + length);
|
|
53
|
+
chunks.push({
|
|
54
|
+
type,
|
|
55
|
+
data: chunkData
|
|
56
|
+
});
|
|
57
|
+
offset += 12 + length;
|
|
58
|
+
if (type === "IEND") break;
|
|
59
|
+
}
|
|
60
|
+
return chunks;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Parse the IHDR chunk.
|
|
64
|
+
*/
|
|
65
|
+
function parseIhdr(chunk) {
|
|
66
|
+
if (chunk.type !== "IHDR" || chunk.data.length < 13) throw new Error("Invalid PNG: missing or malformed IHDR chunk");
|
|
67
|
+
const view = new DataView(chunk.data.buffer, chunk.data.byteOffset, chunk.data.byteLength);
|
|
68
|
+
return {
|
|
69
|
+
width: view.getUint32(0, false),
|
|
70
|
+
height: view.getUint32(4, false),
|
|
71
|
+
bitDepth: chunk.data[8],
|
|
72
|
+
colorType: chunk.data[9],
|
|
73
|
+
compressionMethod: chunk.data[10],
|
|
74
|
+
filterMethod: chunk.data[11],
|
|
75
|
+
interlaceMethod: chunk.data[12]
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Concatenate all IDAT chunk data into a single Uint8Array.
|
|
80
|
+
*/
|
|
81
|
+
function concatenateIdatChunks(chunks) {
|
|
82
|
+
const idatChunks = chunks.filter((c) => c.type === "IDAT");
|
|
83
|
+
if (idatChunks.length === 0) throw new Error("Invalid PNG: no IDAT chunks found");
|
|
84
|
+
const totalLength = idatChunks.reduce((sum, c) => sum + c.data.length, 0);
|
|
85
|
+
const result = new Uint8Array(totalLength);
|
|
86
|
+
let pos = 0;
|
|
87
|
+
for (const chunk of idatChunks) {
|
|
88
|
+
result.set(chunk.data, pos);
|
|
89
|
+
pos += chunk.data.length;
|
|
90
|
+
}
|
|
91
|
+
return result;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Find the PLTE (palette) chunk data if present.
|
|
95
|
+
*/
|
|
96
|
+
function findPalette(chunks) {
|
|
97
|
+
return chunks.find((c) => c.type === "PLTE")?.data;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Find the tRNS (transparency) chunk data if present.
|
|
101
|
+
*/
|
|
102
|
+
function findTransparency(chunks) {
|
|
103
|
+
return chunks.find((c) => c.type === "tRNS")?.data;
|
|
104
|
+
}
|
|
105
|
+
let pngWasmInstance;
|
|
106
|
+
/**
|
|
107
|
+
* Initialize the PNG decoding WASM module.
|
|
108
|
+
*
|
|
109
|
+
* @param wasmSource - The WASM binary, URL, or Response.
|
|
110
|
+
*/
|
|
111
|
+
async function initPngWasm(wasmSource) {
|
|
112
|
+
if (pngWasmInstance) return;
|
|
113
|
+
const imports = { env: {} };
|
|
114
|
+
let result;
|
|
115
|
+
try {
|
|
116
|
+
if (wasmSource instanceof Uint8Array) result = await WebAssembly.instantiate(wasmSource.buffer, imports);
|
|
117
|
+
else if (typeof Response !== "undefined" && wasmSource instanceof Response) result = await WebAssembly.instantiateStreaming(wasmSource, imports);
|
|
118
|
+
else if (typeof wasmSource === "string" || wasmSource instanceof URL) {
|
|
119
|
+
const resp = await fetch(String(wasmSource));
|
|
120
|
+
result = await WebAssembly.instantiateStreaming(resp, imports);
|
|
121
|
+
} else {
|
|
122
|
+
const { loadWasmModule } = await import("./loader-D9LTYmrX.mjs");
|
|
123
|
+
const bytes = await loadWasmModule("png");
|
|
124
|
+
result = await WebAssembly.instantiate(bytes.buffer, imports);
|
|
125
|
+
}
|
|
126
|
+
pngWasmInstance = result.instance.exports;
|
|
127
|
+
} catch {
|
|
128
|
+
pngWasmInstance = void 0;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Decompress the zlib data from IDAT chunks.
|
|
133
|
+
*
|
|
134
|
+
* Uses the DecompressionStream API (available in modern browsers,
|
|
135
|
+
* Node 18+, Deno) or falls back to a manual inflate.
|
|
136
|
+
*
|
|
137
|
+
* @internal
|
|
138
|
+
*/
|
|
139
|
+
async function decompressZlib(compressed) {
|
|
140
|
+
try {
|
|
141
|
+
const { unzlibSync } = await import("fflate");
|
|
142
|
+
return unzlibSync(compressed);
|
|
143
|
+
} catch {
|
|
144
|
+
if (typeof DecompressionStream !== "undefined") {
|
|
145
|
+
const ds = new DecompressionStream("deflate");
|
|
146
|
+
const writer = ds.writable.getWriter();
|
|
147
|
+
const reader = ds.readable.getReader();
|
|
148
|
+
writer.write(new Uint8Array(compressed)).catch(() => {});
|
|
149
|
+
writer.close().catch(() => {});
|
|
150
|
+
const chunks = [];
|
|
151
|
+
while (true) {
|
|
152
|
+
const { done, value } = await reader.read();
|
|
153
|
+
if (done) break;
|
|
154
|
+
chunks.push(value);
|
|
155
|
+
}
|
|
156
|
+
const totalLength = chunks.reduce((sum, c) => sum + c.length, 0);
|
|
157
|
+
const result = new Uint8Array(totalLength);
|
|
158
|
+
let pos = 0;
|
|
159
|
+
for (const chunk of chunks) {
|
|
160
|
+
result.set(chunk, pos);
|
|
161
|
+
pos += chunk.length;
|
|
162
|
+
}
|
|
163
|
+
return result;
|
|
164
|
+
}
|
|
165
|
+
throw new Error("PNG decompression requires fflate library or DecompressionStream API");
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Apply PNG filter reconstruction to a decompressed scanline buffer.
|
|
170
|
+
*
|
|
171
|
+
* Each scanline is prefixed with a 1-byte filter type:
|
|
172
|
+
* 0=None, 1=Sub, 2=Up, 3=Average, 4=Paeth
|
|
173
|
+
*
|
|
174
|
+
* @internal
|
|
175
|
+
*/
|
|
176
|
+
function reconstructFilters(decompressed, width, height, bytesPerPixel) {
|
|
177
|
+
const stride = width * bytesPerPixel;
|
|
178
|
+
const result = new Uint8Array(height * stride);
|
|
179
|
+
let srcOffset = 0;
|
|
180
|
+
let dstOffset = 0;
|
|
181
|
+
for (let y = 0; y < height; y++) {
|
|
182
|
+
const filterType = decompressed[srcOffset];
|
|
183
|
+
srcOffset++;
|
|
184
|
+
const scanline = decompressed.subarray(srcOffset, srcOffset + stride);
|
|
185
|
+
srcOffset += stride;
|
|
186
|
+
const prevLine = y > 0 ? result.subarray((y - 1) * stride, y * stride) : new Uint8Array(stride);
|
|
187
|
+
for (let x = 0; x < stride; x++) {
|
|
188
|
+
const raw = scanline[x];
|
|
189
|
+
const a = x >= bytesPerPixel ? result[dstOffset + x - bytesPerPixel] : 0;
|
|
190
|
+
const b = prevLine[x];
|
|
191
|
+
const c = x >= bytesPerPixel && y > 0 ? result[(y - 1) * stride + x - bytesPerPixel] : 0;
|
|
192
|
+
let reconstructed;
|
|
193
|
+
switch (filterType) {
|
|
194
|
+
case 0:
|
|
195
|
+
reconstructed = raw;
|
|
196
|
+
break;
|
|
197
|
+
case 1:
|
|
198
|
+
reconstructed = raw + a & 255;
|
|
199
|
+
break;
|
|
200
|
+
case 2:
|
|
201
|
+
reconstructed = raw + b & 255;
|
|
202
|
+
break;
|
|
203
|
+
case 3:
|
|
204
|
+
reconstructed = raw + Math.floor((a + b) / 2) & 255;
|
|
205
|
+
break;
|
|
206
|
+
case 4:
|
|
207
|
+
reconstructed = raw + paethPredictor(a, b, c) & 255;
|
|
208
|
+
break;
|
|
209
|
+
default: throw new Error(`Unknown PNG filter type: ${filterType}`);
|
|
210
|
+
}
|
|
211
|
+
result[dstOffset + x] = reconstructed;
|
|
212
|
+
}
|
|
213
|
+
dstOffset += stride;
|
|
214
|
+
}
|
|
215
|
+
return result;
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Paeth predictor function.
|
|
219
|
+
* @internal
|
|
220
|
+
*/
|
|
221
|
+
function paethPredictor(a, b, c) {
|
|
222
|
+
const p = a + b - c;
|
|
223
|
+
const pa = Math.abs(p - a);
|
|
224
|
+
const pb = Math.abs(p - b);
|
|
225
|
+
const pc = Math.abs(p - c);
|
|
226
|
+
if (pa <= pb && pa <= pc) return a;
|
|
227
|
+
if (pb <= pc) return b;
|
|
228
|
+
return c;
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Compute the bytes per pixel for a given color type and bit depth.
|
|
232
|
+
* @internal
|
|
233
|
+
*/
|
|
234
|
+
function bytesPerPixel(colorType, bitDepth) {
|
|
235
|
+
const channels = colorType === PngColorType.Grayscale ? 1 : colorType === PngColorType.RGB ? 3 : colorType === PngColorType.Indexed ? 1 : colorType === PngColorType.GrayscaleAlpha ? 2 : colorType === PngColorType.RGBA ? 4 : 1;
|
|
236
|
+
return Math.max(1, Math.ceil(channels * bitDepth / 8));
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Separate RGBA pixels into RGB data + alpha mask.
|
|
240
|
+
* @internal
|
|
241
|
+
*/
|
|
242
|
+
function separateRgba(pixels, width, height) {
|
|
243
|
+
const pixelCount = width * height;
|
|
244
|
+
const rgb = new Uint8Array(pixelCount * 3);
|
|
245
|
+
const alpha = new Uint8Array(pixelCount);
|
|
246
|
+
for (let i = 0; i < pixelCount; i++) {
|
|
247
|
+
const srcIdx = i * 4;
|
|
248
|
+
const dstIdx = i * 3;
|
|
249
|
+
rgb[dstIdx] = pixels[srcIdx];
|
|
250
|
+
rgb[dstIdx + 1] = pixels[srcIdx + 1];
|
|
251
|
+
rgb[dstIdx + 2] = pixels[srcIdx + 2];
|
|
252
|
+
alpha[i] = pixels[srcIdx + 3];
|
|
253
|
+
}
|
|
254
|
+
return {
|
|
255
|
+
rgb,
|
|
256
|
+
alpha
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Separate Grayscale+Alpha pixels into gray data + alpha mask.
|
|
261
|
+
* @internal
|
|
262
|
+
*/
|
|
263
|
+
function separateGrayscaleAlpha(pixels, width, height) {
|
|
264
|
+
const pixelCount = width * height;
|
|
265
|
+
const gray = new Uint8Array(pixelCount);
|
|
266
|
+
const alpha = new Uint8Array(pixelCount);
|
|
267
|
+
for (let i = 0; i < pixelCount; i++) {
|
|
268
|
+
gray[i] = pixels[i * 2];
|
|
269
|
+
alpha[i] = pixels[i * 2 + 1];
|
|
270
|
+
}
|
|
271
|
+
return {
|
|
272
|
+
gray,
|
|
273
|
+
alpha
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Compress data using deflate for FlateDecode.
|
|
278
|
+
* @internal
|
|
279
|
+
*/
|
|
280
|
+
async function compressForPdf(data) {
|
|
281
|
+
const { deflateSync } = await import("fflate");
|
|
282
|
+
return deflateSync(data, { level: 6 });
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Embed a PNG image for use as a PDF image XObject.
|
|
286
|
+
*
|
|
287
|
+
* Parses the PNG file, decodes pixel data, separates alpha channels,
|
|
288
|
+
* and produces all the data needed for the PDF image XObject
|
|
289
|
+
* dictionary.
|
|
290
|
+
*
|
|
291
|
+
* @param pngData - The raw PNG file as a Uint8Array.
|
|
292
|
+
* @returns A promise resolving to the embedding result.
|
|
293
|
+
*
|
|
294
|
+
* @example
|
|
295
|
+
* ```ts
|
|
296
|
+
* const pngBytes = await readFile('photo.png');
|
|
297
|
+
* const result = await embedPng(pngBytes);
|
|
298
|
+
*
|
|
299
|
+
* // Create image XObject with:
|
|
300
|
+
* // /Width result.width
|
|
301
|
+
* // /Height result.height
|
|
302
|
+
* // /ColorSpace result.colorSpace
|
|
303
|
+
* // /BitsPerComponent result.bitsPerComponent
|
|
304
|
+
* // /Filter /FlateDecode
|
|
305
|
+
* // stream: result.imageData
|
|
306
|
+
* //
|
|
307
|
+
* // If result.smaskData is defined, create a second XObject for the SMask.
|
|
308
|
+
* ```
|
|
309
|
+
*/
|
|
310
|
+
async function embedPng(pngData) {
|
|
311
|
+
validatePngSignature(pngData);
|
|
312
|
+
const chunks = parseChunks(pngData);
|
|
313
|
+
if (chunks.length === 0) throw new Error("Invalid PNG: no chunks found");
|
|
314
|
+
const ihdr = parseIhdr(chunks[0]);
|
|
315
|
+
const idatData = concatenateIdatChunks(chunks);
|
|
316
|
+
const palette = findPalette(chunks);
|
|
317
|
+
const transparency = findTransparency(chunks);
|
|
318
|
+
const hasAlpha = ihdr.colorType === PngColorType.GrayscaleAlpha || ihdr.colorType === PngColorType.RGBA;
|
|
319
|
+
const isInterlaced = ihdr.interlaceMethod !== 0;
|
|
320
|
+
if (ihdr.colorType === PngColorType.Indexed && !isInterlaced && ihdr.bitDepth === 8) return embedIndexedPng(ihdr, idatData, palette, transparency);
|
|
321
|
+
if (!hasAlpha && !isInterlaced && ihdr.bitDepth === 8) return embedDirectPng(ihdr, idatData, transparency);
|
|
322
|
+
return embedComplexPng(ihdr, idatData, palette, transparency);
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Embed an indexed (palette-based) PNG.
|
|
326
|
+
* @internal
|
|
327
|
+
*/
|
|
328
|
+
async function embedIndexedPng(ihdr, idatData, palette, transparency) {
|
|
329
|
+
if (!palette) throw new Error("Invalid PNG: indexed color type requires PLTE chunk");
|
|
330
|
+
const decompressed = await decompressZlib(idatData);
|
|
331
|
+
const bpp = bytesPerPixel(ihdr.colorType, ihdr.bitDepth);
|
|
332
|
+
const pixels = reconstructFilters(decompressed, ihdr.width, ihdr.height, bpp);
|
|
333
|
+
const imageData = await compressForPdf(pixels);
|
|
334
|
+
let smaskData;
|
|
335
|
+
if (transparency && transparency.length > 0) {
|
|
336
|
+
const pixelCount = ihdr.width * ihdr.height;
|
|
337
|
+
const alpha = new Uint8Array(pixelCount);
|
|
338
|
+
for (let i = 0; i < pixelCount; i++) {
|
|
339
|
+
const paletteIndex = pixels[i];
|
|
340
|
+
alpha[i] = paletteIndex < transparency.length ? transparency[paletteIndex] : 255;
|
|
341
|
+
}
|
|
342
|
+
smaskData = await compressForPdf(alpha);
|
|
343
|
+
}
|
|
344
|
+
return {
|
|
345
|
+
width: ihdr.width,
|
|
346
|
+
height: ihdr.height,
|
|
347
|
+
bitsPerComponent: 8,
|
|
348
|
+
colorSpace: "Indexed",
|
|
349
|
+
palette,
|
|
350
|
+
imageData,
|
|
351
|
+
filter: "FlateDecode",
|
|
352
|
+
smaskData,
|
|
353
|
+
smaskBitsPerComponent: smaskData ? 8 : void 0,
|
|
354
|
+
hasTransparency: transparency !== void 0 && transparency.length > 0
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Embed a direct (Grayscale or RGB) PNG without alpha.
|
|
359
|
+
* @internal
|
|
360
|
+
*/
|
|
361
|
+
async function embedDirectPng(ihdr, idatData, transparency) {
|
|
362
|
+
const decompressed = await decompressZlib(idatData);
|
|
363
|
+
const bpp = bytesPerPixel(ihdr.colorType, ihdr.bitDepth);
|
|
364
|
+
const pixels = reconstructFilters(decompressed, ihdr.width, ihdr.height, bpp);
|
|
365
|
+
const imageData = await compressForPdf(pixels);
|
|
366
|
+
const colorSpace = ihdr.colorType === PngColorType.Grayscale ? "DeviceGray" : "DeviceRGB";
|
|
367
|
+
let smaskData;
|
|
368
|
+
if (transparency && transparency.length > 0) {
|
|
369
|
+
const pixelCount = ihdr.width * ihdr.height;
|
|
370
|
+
const alpha = new Uint8Array(pixelCount);
|
|
371
|
+
alpha.fill(255);
|
|
372
|
+
if (ihdr.colorType === PngColorType.Grayscale && transparency.length >= 2) {
|
|
373
|
+
const trnsGray = transparency[0] << 8 | transparency[1];
|
|
374
|
+
for (let i = 0; i < pixelCount; i++) if (pixels[i] === trnsGray) alpha[i] = 0;
|
|
375
|
+
smaskData = await compressForPdf(alpha);
|
|
376
|
+
} else if (ihdr.colorType === PngColorType.RGB && transparency.length >= 6) {
|
|
377
|
+
const trnsR = transparency[0] << 8 | transparency[1];
|
|
378
|
+
const trnsG = transparency[2] << 8 | transparency[3];
|
|
379
|
+
const trnsB = transparency[4] << 8 | transparency[5];
|
|
380
|
+
for (let i = 0; i < pixelCount; i++) {
|
|
381
|
+
const idx = i * 3;
|
|
382
|
+
if (pixels[idx] === trnsR && pixels[idx + 1] === trnsG && pixels[idx + 2] === trnsB) alpha[i] = 0;
|
|
383
|
+
}
|
|
384
|
+
smaskData = await compressForPdf(alpha);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
return {
|
|
388
|
+
width: ihdr.width,
|
|
389
|
+
height: ihdr.height,
|
|
390
|
+
bitsPerComponent: ihdr.bitDepth,
|
|
391
|
+
colorSpace,
|
|
392
|
+
imageData,
|
|
393
|
+
filter: "FlateDecode",
|
|
394
|
+
smaskData,
|
|
395
|
+
smaskBitsPerComponent: smaskData ? 8 : void 0,
|
|
396
|
+
hasTransparency: transparency !== void 0 && transparency.length > 0
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Full decompression path for PNGs that need alpha separation,
|
|
401
|
+
* de-interlacing, or bit-depth normalization.
|
|
402
|
+
* @internal
|
|
403
|
+
*/
|
|
404
|
+
async function embedComplexPng(ihdr, idatData, palette, transparency) {
|
|
405
|
+
const decompressed = await decompressZlib(idatData);
|
|
406
|
+
const bpp = bytesPerPixel(ihdr.colorType, ihdr.bitDepth);
|
|
407
|
+
let pixels;
|
|
408
|
+
if (ihdr.interlaceMethod !== 0) {
|
|
409
|
+
const startingRow = [
|
|
410
|
+
0,
|
|
411
|
+
0,
|
|
412
|
+
4,
|
|
413
|
+
0,
|
|
414
|
+
2,
|
|
415
|
+
0,
|
|
416
|
+
1
|
|
417
|
+
];
|
|
418
|
+
const startingCol = [
|
|
419
|
+
0,
|
|
420
|
+
4,
|
|
421
|
+
0,
|
|
422
|
+
2,
|
|
423
|
+
0,
|
|
424
|
+
1,
|
|
425
|
+
0
|
|
426
|
+
];
|
|
427
|
+
const rowStride = [
|
|
428
|
+
8,
|
|
429
|
+
8,
|
|
430
|
+
8,
|
|
431
|
+
4,
|
|
432
|
+
4,
|
|
433
|
+
2,
|
|
434
|
+
2
|
|
435
|
+
];
|
|
436
|
+
const colStride = [
|
|
437
|
+
8,
|
|
438
|
+
8,
|
|
439
|
+
4,
|
|
440
|
+
4,
|
|
441
|
+
2,
|
|
442
|
+
2,
|
|
443
|
+
1
|
|
444
|
+
];
|
|
445
|
+
const stride = ihdr.width * bpp;
|
|
446
|
+
const finalPixels = new Uint8Array(ihdr.height * stride);
|
|
447
|
+
let srcOffset = 0;
|
|
448
|
+
for (let pass = 0; pass < 7; pass++) {
|
|
449
|
+
const startRow = startingRow[pass];
|
|
450
|
+
const startCol = startingCol[pass];
|
|
451
|
+
const rStride = rowStride[pass];
|
|
452
|
+
const cStride = colStride[pass];
|
|
453
|
+
const passWidth = ihdr.width <= startCol ? 0 : Math.ceil((ihdr.width - startCol) / cStride);
|
|
454
|
+
const passHeight = ihdr.height <= startRow ? 0 : Math.ceil((ihdr.height - startRow) / rStride);
|
|
455
|
+
if (passWidth === 0 || passHeight === 0) continue;
|
|
456
|
+
const passDataLength = passHeight * (1 + passWidth * bpp);
|
|
457
|
+
const passData = decompressed.subarray(srcOffset, srcOffset + passDataLength);
|
|
458
|
+
srcOffset += passDataLength;
|
|
459
|
+
const passPixels = reconstructFilters(passData, passWidth, passHeight, bpp);
|
|
460
|
+
for (let py = 0; py < passHeight; py++) {
|
|
461
|
+
const finalY = startRow + py * rStride;
|
|
462
|
+
for (let px = 0; px < passWidth; px++) {
|
|
463
|
+
const finalX = startCol + px * cStride;
|
|
464
|
+
const srcIdx = (py * passWidth + px) * bpp;
|
|
465
|
+
const dstIdx = finalY * stride + finalX * bpp;
|
|
466
|
+
for (let b = 0; b < bpp; b++) finalPixels[dstIdx + b] = passPixels[srcIdx + b];
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
pixels = finalPixels;
|
|
471
|
+
} else pixels = reconstructFilters(decompressed, ihdr.width, ihdr.height, bpp);
|
|
472
|
+
let imageData;
|
|
473
|
+
let smaskData;
|
|
474
|
+
let colorSpace;
|
|
475
|
+
let bitsPerComponent = ihdr.bitDepth;
|
|
476
|
+
switch (ihdr.colorType) {
|
|
477
|
+
case PngColorType.RGBA: {
|
|
478
|
+
const { rgb, alpha } = separateRgba(pixels, ihdr.width, ihdr.height);
|
|
479
|
+
imageData = await compressForPdf(rgb);
|
|
480
|
+
smaskData = await compressForPdf(alpha);
|
|
481
|
+
colorSpace = "DeviceRGB";
|
|
482
|
+
bitsPerComponent = 8;
|
|
483
|
+
break;
|
|
484
|
+
}
|
|
485
|
+
case PngColorType.GrayscaleAlpha: {
|
|
486
|
+
const { gray, alpha } = separateGrayscaleAlpha(pixels, ihdr.width, ihdr.height);
|
|
487
|
+
imageData = await compressForPdf(gray);
|
|
488
|
+
smaskData = await compressForPdf(alpha);
|
|
489
|
+
colorSpace = "DeviceGray";
|
|
490
|
+
bitsPerComponent = 8;
|
|
491
|
+
break;
|
|
492
|
+
}
|
|
493
|
+
case PngColorType.Grayscale:
|
|
494
|
+
imageData = await compressForPdf(pixels);
|
|
495
|
+
colorSpace = "DeviceGray";
|
|
496
|
+
break;
|
|
497
|
+
case PngColorType.RGB:
|
|
498
|
+
imageData = await compressForPdf(pixels);
|
|
499
|
+
colorSpace = "DeviceRGB";
|
|
500
|
+
break;
|
|
501
|
+
case PngColorType.Indexed:
|
|
502
|
+
if (!palette) throw new Error("Invalid PNG: indexed color type requires PLTE chunk");
|
|
503
|
+
imageData = await compressForPdf(pixels);
|
|
504
|
+
colorSpace = "Indexed";
|
|
505
|
+
bitsPerComponent = ihdr.bitDepth;
|
|
506
|
+
break;
|
|
507
|
+
default: throw new Error(`Unsupported PNG color type: ${ihdr.colorType}`);
|
|
508
|
+
}
|
|
509
|
+
return {
|
|
510
|
+
width: ihdr.width,
|
|
511
|
+
height: ihdr.height,
|
|
512
|
+
bitsPerComponent,
|
|
513
|
+
colorSpace,
|
|
514
|
+
palette: ihdr.colorType === PngColorType.Indexed ? palette : void 0,
|
|
515
|
+
imageData,
|
|
516
|
+
filter: "FlateDecode",
|
|
517
|
+
smaskData,
|
|
518
|
+
smaskBitsPerComponent: smaskData ? 8 : void 0,
|
|
519
|
+
hasTransparency: smaskData !== void 0 || transparency !== void 0 && transparency.length > 0
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
//#endregion
|
|
524
|
+
export { pngEmbed_exports as n, embedPng as t };
|
|
525
|
+
//# sourceMappingURL=pngEmbed-DgeNWlbS.mjs.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
//#region \0rolldown/runtime.js
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __exportAll = (all, no_symbols) => {
|
|
4
|
+
let target = {};
|
|
5
|
+
for (var name in all) {
|
|
6
|
+
__defProp(target, name, {
|
|
7
|
+
get: all[name],
|
|
8
|
+
enumerable: true
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
if (!no_symbols) {
|
|
12
|
+
__defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
13
|
+
}
|
|
14
|
+
return target;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
//#endregion
|
|
18
|
+
export { __exportAll as t };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
//#region \0rolldown/runtime.js
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __exportAll = (all, no_symbols) => {
|
|
4
|
+
let target = {};
|
|
5
|
+
for (var name in all) {
|
|
6
|
+
__defProp(target, name, {
|
|
7
|
+
get: all[name],
|
|
8
|
+
enumerable: true
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
if (!no_symbols) {
|
|
12
|
+
__defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
13
|
+
}
|
|
14
|
+
return target;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
//#endregion
|
|
18
|
+
|
|
19
|
+
Object.defineProperty(exports, '__exportAll', {
|
|
20
|
+
enumerable: true,
|
|
21
|
+
get: function () {
|
|
22
|
+
return __exportAll;
|
|
23
|
+
}
|
|
24
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "modern-pdf-lib",
|
|
3
|
+
"version": "0.9.0",
|
|
4
|
+
"description": "A modern, WASM-accelerated PDF creation engine for every JavaScript runtime",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"types": "./dist/index.d.mts",
|
|
9
|
+
"import": "./dist/index.mjs",
|
|
10
|
+
"require": {
|
|
11
|
+
"types": "./dist/index.d.cts",
|
|
12
|
+
"default": "./dist/index.cjs"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"./wasm/*": "./dist/wasm/*"
|
|
16
|
+
},
|
|
17
|
+
"main": "./dist/index.cjs",
|
|
18
|
+
"module": "./dist/index.mjs",
|
|
19
|
+
"types": "./dist/index.d.mts",
|
|
20
|
+
"files": [
|
|
21
|
+
"dist/**/*.mjs",
|
|
22
|
+
"dist/**/*.cjs",
|
|
23
|
+
"dist/**/*.d.mts",
|
|
24
|
+
"dist/**/*.d.cts",
|
|
25
|
+
"dist/**/*.d.mts.map",
|
|
26
|
+
"dist/**/*.d.cts.map",
|
|
27
|
+
"LICENSE",
|
|
28
|
+
"README.md"
|
|
29
|
+
],
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=22"
|
|
32
|
+
},
|
|
33
|
+
"sideEffects": false,
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "tsdown",
|
|
36
|
+
"build:wasm": "bash scripts/build-wasm.sh",
|
|
37
|
+
"build:all": "npm run build:wasm && npm run build",
|
|
38
|
+
"dev": "vite",
|
|
39
|
+
"test": "vitest run",
|
|
40
|
+
"test:watch": "vitest",
|
|
41
|
+
"test:integration": "vitest run tests/integration",
|
|
42
|
+
"test:e2e": "playwright test",
|
|
43
|
+
"test:bench": "vitest bench",
|
|
44
|
+
"lint": "eslint src/ tests/",
|
|
45
|
+
"typecheck": "tsc --noEmit",
|
|
46
|
+
"prepublishOnly": "npm run typecheck && npm test && npm run build",
|
|
47
|
+
"docs:dev": "vitepress dev docs",
|
|
48
|
+
"docs:build": "vitepress build docs",
|
|
49
|
+
"docs:api": "typedoc",
|
|
50
|
+
"validate-pdf": "tsx scripts/validate-pdf.ts",
|
|
51
|
+
"clean": "rm -rf dist coverage"
|
|
52
|
+
},
|
|
53
|
+
"dependencies": {
|
|
54
|
+
"fflate": "0.8.2"
|
|
55
|
+
},
|
|
56
|
+
"devDependencies": {
|
|
57
|
+
"typescript": "6.0.0-dev.20260227",
|
|
58
|
+
"tsdown": "0.21.0-beta.2",
|
|
59
|
+
"esbuild": "0.27.3",
|
|
60
|
+
"rollup": "4.59.0",
|
|
61
|
+
"vite": "8.0.0-beta.16",
|
|
62
|
+
"vitest": "4.1.0-beta.5",
|
|
63
|
+
"miniflare": "4.20260305.0",
|
|
64
|
+
"@playwright/test": "1.59.0-alpha-2026-02-27",
|
|
65
|
+
"vitepress": "2.0.0-alpha.16",
|
|
66
|
+
"typedoc": "0.28.17",
|
|
67
|
+
"typedoc-plugin-markdown": "4.10.0",
|
|
68
|
+
"eslint": "10.0.2",
|
|
69
|
+
"@eslint/js": "10.0.1",
|
|
70
|
+
"typescript-eslint": "8.56.2-alpha.1",
|
|
71
|
+
"prettier": "3.8.1",
|
|
72
|
+
"fast-png": "8.0.0",
|
|
73
|
+
"tsx": "4.21.0",
|
|
74
|
+
"wasm-pack": "0.14.0"
|
|
75
|
+
},
|
|
76
|
+
"keywords": [
|
|
77
|
+
"pdf",
|
|
78
|
+
"pdf-generation",
|
|
79
|
+
"wasm",
|
|
80
|
+
"typescript",
|
|
81
|
+
"esm",
|
|
82
|
+
"cloudflare-workers",
|
|
83
|
+
"streaming"
|
|
84
|
+
],
|
|
85
|
+
"license": "MIT",
|
|
86
|
+
"repository": {
|
|
87
|
+
"type": "git",
|
|
88
|
+
"url": "https://github.com/ABCrimson/modern-pdf-lib.git"
|
|
89
|
+
},
|
|
90
|
+
"homepage": "https://github.com/ABCrimson/modern-pdf-lib#readme",
|
|
91
|
+
"bugs": {
|
|
92
|
+
"url": "https://github.com/ABCrimson/modern-pdf-lib/issues"
|
|
93
|
+
}
|
|
94
|
+
}
|