@fauzi-dhuhuri/react-pdf-image 3.1.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/lib/index.d.ts ADDED
@@ -0,0 +1,45 @@
1
+ import { SvgNode } from '@react-pdf/svg';
2
+
3
+ interface RasterImage {
4
+ width: number;
5
+ height: number;
6
+ data: Buffer;
7
+ format: 'jpeg' | 'png' | 'webp';
8
+ key?: string;
9
+ }
10
+ interface SvgImage {
11
+ width: number;
12
+ height: number;
13
+ data: SvgNode;
14
+ format: 'svg';
15
+ key?: string;
16
+ }
17
+ type Image = RasterImage | SvgImage;
18
+ type ImageFormat = 'jpg' | 'jpeg' | 'png' | 'svg' | 'webp';
19
+ type DataImageSrc = {
20
+ data: Buffer;
21
+ format: ImageFormat;
22
+ };
23
+ type LocalImageSrc = {
24
+ uri: string;
25
+ format?: ImageFormat;
26
+ };
27
+ type RemoteImageSrc = {
28
+ uri: string;
29
+ method?: 'GET' | 'HEAD' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
30
+ headers?: Record<string, string>;
31
+ format?: ImageFormat;
32
+ body?: any;
33
+ credentials?: 'omit' | 'same-origin' | 'include';
34
+ };
35
+ type Base64ImageSrc = {
36
+ uri: `data:image${string}`;
37
+ };
38
+ type ImageSrc = Blob | Buffer | DataImageSrc | LocalImageSrc | RemoteImageSrc | Base64ImageSrc;
39
+
40
+ declare const resolveImage: (src: ImageSrc, { cache }?: {
41
+ cache?: boolean | undefined;
42
+ }) => Promise<Image | null> | null | undefined;
43
+
44
+ export { resolveImage as default };
45
+ export type { Image, ImageSrc, RasterImage, SvgImage };
package/lib/index.js ADDED
@@ -0,0 +1,361 @@
1
+ import fs from 'fs';
2
+ import url from 'url';
3
+ import path from 'path';
4
+ import _PNG from 'png-js';
5
+ import _JPEG from 'jay-peg';
6
+ import { parseSvg } from '@react-pdf/svg';
7
+ import decode, { init } from '@jsquash/webp/decode';
8
+
9
+ class PNG {
10
+ data;
11
+ width;
12
+ height;
13
+ format;
14
+ constructor(data) {
15
+ const png = new _PNG(data);
16
+ this.data = data;
17
+ this.width = png.width;
18
+ this.height = png.height;
19
+ this.format = 'png';
20
+ }
21
+ static isValid(data) {
22
+ return (data &&
23
+ Buffer.isBuffer(data) &&
24
+ data[0] === 137 &&
25
+ data[1] === 80 &&
26
+ data[2] === 78 &&
27
+ data[3] === 71 &&
28
+ data[4] === 13 &&
29
+ data[5] === 10 &&
30
+ data[6] === 26 &&
31
+ data[7] === 10);
32
+ }
33
+ }
34
+
35
+ class JPEG {
36
+ data;
37
+ width;
38
+ height;
39
+ format;
40
+ constructor(data) {
41
+ this.data = data;
42
+ this.format = 'jpeg';
43
+ this.width = 0;
44
+ this.height = 0;
45
+ if (data.readUInt16BE(0) !== 0xffd8) {
46
+ throw new Error('SOI not found in JPEG');
47
+ }
48
+ const markers = _JPEG.decode(this.data);
49
+ let orientation;
50
+ for (let i = 0; i < markers.length; i += 1) {
51
+ const marker = markers[i];
52
+ if (marker.name === 'EXIF' && marker.entries.orientation) {
53
+ orientation = marker.entries.orientation;
54
+ }
55
+ if (marker.name === 'SOF') {
56
+ this.width ||= marker.width;
57
+ this.height ||= marker.height;
58
+ }
59
+ }
60
+ if (orientation > 4) {
61
+ [this.width, this.height] = [this.height, this.width];
62
+ }
63
+ }
64
+ static isValid(data) {
65
+ return data && Buffer.isBuffer(data) && data.readUInt16BE(0) === 0xffd8;
66
+ }
67
+ }
68
+
69
+ const UNIT_TO_PT = {
70
+ px: 72 / 96,
71
+ pt: 1,
72
+ in: 72,
73
+ cm: 72 / 2.54,
74
+ mm: 72 / 25.4,
75
+ };
76
+ function parseNumber(value) {
77
+ if (typeof value !== 'string')
78
+ return undefined;
79
+ const match = value.match(/^(-?\d*\.?\d+)(px|pt|in|cm|mm)?$/);
80
+ if (!match)
81
+ return undefined;
82
+ const num = parseFloat(match[1]);
83
+ const unit = match[2];
84
+ if (!unit)
85
+ return num;
86
+ return num * (UNIT_TO_PT[unit] ?? 1);
87
+ }
88
+ function parseViewBox(value) {
89
+ if (typeof value !== 'string')
90
+ return undefined;
91
+ const parts = value
92
+ .trim()
93
+ .split(/[\s,]+/)
94
+ .map(Number);
95
+ if (parts.length !== 4 || parts.some(isNaN))
96
+ return undefined;
97
+ return {
98
+ minX: parts[0],
99
+ minY: parts[1],
100
+ maxX: parts[2],
101
+ maxY: parts[3],
102
+ };
103
+ }
104
+ class SVG {
105
+ data;
106
+ width;
107
+ height;
108
+ format;
109
+ constructor(data) {
110
+ const svgString = data.toString('utf-8');
111
+ const parsed = parseSvg(svgString);
112
+ const viewBox = parseViewBox(parsed.props.viewBox);
113
+ this.data = parsed;
114
+ this.format = 'svg';
115
+ this.width = parseNumber(parsed.props.width) ?? viewBox?.maxX ?? 0;
116
+ this.height = parseNumber(parsed.props.height) ?? viewBox?.maxY ?? 0;
117
+ }
118
+ static isValid(data) {
119
+ if (!Buffer.isBuffer(data))
120
+ return false;
121
+ const str = data.toString('utf-8').trimStart();
122
+ return str.startsWith('<?xml') || str.startsWith('<svg');
123
+ }
124
+ }
125
+
126
+ let isWasmInitialised = false;
127
+ function BufferToArrayBuffer(buffer) {
128
+ return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
129
+ }
130
+ function isWebp(buffer) {
131
+ if (!buffer || buffer.length < 12) {
132
+ return false;
133
+ }
134
+ const isRiff = buffer.toString('ascii', 0, 4) === 'RIFF';
135
+ const isWebp = buffer.toString('ascii', 8, 12) === 'WEBP';
136
+ return isRiff && isWebp;
137
+ }
138
+ async function decodeWebp(data) {
139
+ const image = await decode(data);
140
+ return image;
141
+ }
142
+ async function ensureWASMloaded() {
143
+ if (isWasmInitialised)
144
+ return;
145
+ const wasmPath = path.resolve(require.resolve('@jsquash/webp/package.json'), '../codec/dec/webp_dec.wasm');
146
+ const wasmBuffer = fs.readFileSync(wasmPath);
147
+ const wasmArrayBuffer = wasmBuffer.buffer.slice(wasmBuffer.byteOffset, wasmBuffer.byteOffset + wasmBuffer.byteLength);
148
+ await init({
149
+ wasmBinary: wasmArrayBuffer
150
+ });
151
+ isWasmInitialised = true;
152
+ }
153
+ class WEBP {
154
+ data;
155
+ width;
156
+ height;
157
+ format;
158
+ constructor(data, width, height, format) {
159
+ this.data = data;
160
+ this.width = width;
161
+ this.height = height;
162
+ this.format = format;
163
+ }
164
+ static async create(data) {
165
+ await ensureWASMloaded();
166
+ const image = await decodeWebp(BufferToArrayBuffer(data));
167
+ return new WEBP(data, image.width, image.height, "webp");
168
+ }
169
+ static isValid(data) {
170
+ return isWebp(data);
171
+ }
172
+ }
173
+
174
+ const createCache = ({ limit = 100 } = {}) => {
175
+ let cache = new Map();
176
+ return {
177
+ get: (key) => key ? cache.get(key) ?? undefined : null,
178
+ set: (key, value) => {
179
+ cache.delete(key);
180
+ if (cache.size >= limit) {
181
+ const firstKey = cache.keys().next().value;
182
+ cache.delete(firstKey);
183
+ }
184
+ cache.set(key, value);
185
+ },
186
+ reset: () => {
187
+ cache = new Map();
188
+ },
189
+ length: () => cache.size,
190
+ };
191
+ };
192
+
193
+ const IMAGE_CACHE = createCache({ limit: 30 });
194
+ const isBuffer = Buffer.isBuffer;
195
+ const isBlob = (src) => {
196
+ return typeof Blob !== 'undefined' && src instanceof Blob;
197
+ };
198
+ const isDataImageSrc = (src) => {
199
+ return 'data' in src;
200
+ };
201
+ const isDataUri = (imageSrc) => 'uri' in imageSrc && imageSrc.uri.startsWith('data:');
202
+ const getAbsoluteLocalPath = (src) => {
203
+ const { protocol, auth, host, port, hostname, path: pathname, } = url.parse(src);
204
+ const absolutePath = pathname ? path.resolve(src) : undefined;
205
+ if ((protocol && protocol !== 'file:') || auth || host || port || hostname) {
206
+ return undefined;
207
+ }
208
+ return absolutePath;
209
+ };
210
+ const fetchLocalFile = (src) => new Promise((resolve, reject) => {
211
+ try {
212
+ if (false) ;
213
+ const absolutePath = getAbsoluteLocalPath(src.uri);
214
+ if (!absolutePath) {
215
+ reject(new Error(`Cannot fetch non-local path: ${src.uri}`));
216
+ return;
217
+ }
218
+ fs.readFile(absolutePath, (err, data) => err ? reject(err) : resolve(data));
219
+ }
220
+ catch (err) {
221
+ reject(err);
222
+ }
223
+ });
224
+ const fetchRemoteFile = async (src) => {
225
+ const { method = 'GET', headers, body, credentials } = src;
226
+ const response = await fetch(src.uri, {
227
+ method,
228
+ headers,
229
+ body,
230
+ credentials,
231
+ });
232
+ const buffer = await response.arrayBuffer();
233
+ return Buffer.from(buffer);
234
+ };
235
+ const isValidFormat = (format) => {
236
+ const lower = format.toLowerCase();
237
+ return (lower === 'jpg' ||
238
+ lower === 'jpeg' ||
239
+ lower === 'png' ||
240
+ lower === 'webp' ||
241
+ lower === 'svg' ||
242
+ lower === 'svg+xml');
243
+ };
244
+ const getImageFormat = (buffer) => {
245
+ let format;
246
+ if (JPEG.isValid(buffer)) {
247
+ format = 'jpg';
248
+ }
249
+ else if (PNG.isValid(buffer)) {
250
+ format = 'png';
251
+ }
252
+ else if (SVG.isValid(buffer)) {
253
+ format = 'svg';
254
+ }
255
+ else if (WEBP.isValid(buffer)) {
256
+ format = 'webp';
257
+ }
258
+ return format;
259
+ };
260
+ async function getImage(body, format) {
261
+ switch (format.toLowerCase()) {
262
+ case 'jpg':
263
+ case 'jpeg':
264
+ return new JPEG(body);
265
+ case 'png':
266
+ return new PNG(body);
267
+ case 'svg':
268
+ case 'svg+xml':
269
+ return new SVG(body);
270
+ case 'webp':
271
+ const image = await WEBP.create(body);
272
+ return image;
273
+ default:
274
+ return null;
275
+ }
276
+ }
277
+ const resolveBase64Image = async ({ uri }) => {
278
+ const match = /^data:image\/([a-zA-Z+]*);base64,([^"]*)/g.exec(uri);
279
+ if (!match)
280
+ throw new Error(`Invalid base64 image: ${uri}`);
281
+ const format = match[1];
282
+ const data = match[2];
283
+ if (!isValidFormat(format))
284
+ throw new Error(`Base64 image invalid format: ${format}`);
285
+ return getImage(Buffer.from(data, 'base64'), format);
286
+ };
287
+ const resolveImageFromData = async (src) => {
288
+ if (src.data && src.format) {
289
+ return getImage(src.data, src.format);
290
+ }
291
+ throw new Error(`Invalid data given for local file: ${JSON.stringify(src)}`);
292
+ };
293
+ const resolveBufferImage = async (buffer) => {
294
+ const format = getImageFormat(buffer);
295
+ if (format) {
296
+ return getImage(buffer, format);
297
+ }
298
+ return null;
299
+ };
300
+ const resolveBlobImage = async (blob) => {
301
+ const { type } = blob;
302
+ if (!type || type === 'application/octet-stream') {
303
+ const arrayBuffer = await blob.arrayBuffer();
304
+ const buffer = Buffer.from(arrayBuffer);
305
+ return resolveBufferImage(buffer);
306
+ }
307
+ if (!type.startsWith('image/')) {
308
+ throw new Error(`Invalid blob type: ${type}`);
309
+ }
310
+ const format = type.replace('image/', '');
311
+ if (!isValidFormat(format)) {
312
+ throw new Error(`Invalid blob type: ${type}`);
313
+ }
314
+ const buffer = await blob.arrayBuffer();
315
+ return getImage(Buffer.from(buffer), format);
316
+ };
317
+ const resolveImageFromUrl = async (src) => {
318
+ const data = getAbsoluteLocalPath(src.uri)
319
+ ? await fetchLocalFile(src)
320
+ : await fetchRemoteFile(src);
321
+ const format = getImageFormat(data);
322
+ if (!format) {
323
+ throw new Error('Not valid image extension');
324
+ }
325
+ return getImage(data, format);
326
+ };
327
+ const getCacheKey = (src) => {
328
+ if (isBlob(src) || isBuffer(src))
329
+ return null;
330
+ if (isDataImageSrc(src))
331
+ return src.data?.toString('base64') ?? null;
332
+ return src.uri;
333
+ };
334
+ const resolveImage = (src, { cache = true } = {}) => {
335
+ let image;
336
+ const cacheKey = getCacheKey(src);
337
+ if (isBlob(src)) {
338
+ image = resolveBlobImage(src);
339
+ }
340
+ else if (isBuffer(src)) {
341
+ image = resolveBufferImage(src);
342
+ }
343
+ else if (cache && IMAGE_CACHE.get(cacheKey)) {
344
+ return IMAGE_CACHE.get(cacheKey);
345
+ }
346
+ else if (isDataUri(src)) {
347
+ image = resolveBase64Image(src);
348
+ }
349
+ else if (isDataImageSrc(src)) {
350
+ image = resolveImageFromData(src);
351
+ }
352
+ else {
353
+ image = resolveImageFromUrl(src);
354
+ }
355
+ if (cache && cacheKey) {
356
+ IMAGE_CACHE.set(cacheKey, image);
357
+ }
358
+ return image;
359
+ };
360
+
361
+ export { resolveImage as default };
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@fauzi-dhuhuri/react-pdf-image",
3
+ "version": "3.1.0",
4
+ "license": "MIT",
5
+ "description": "Parses the images in png or jpeg format for react-pdf document",
6
+ "author": "Diego Muracciole <diegomuracciole@gmail.com>",
7
+ "homepage": "https://github.com/diegomura/react-pdf#readme",
8
+ "type": "module",
9
+ "main": "./lib/index.js",
10
+ "types": "./lib/index.d.ts",
11
+ "browser": {
12
+ "./lib/index.js": "./lib/index.browser.js"
13
+ },
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "https://github.com/diegomura/react-pdf.git",
17
+ "directory": "packages/image"
18
+ },
19
+ "scripts": {
20
+ "test": "vitest",
21
+ "build": "rimraf ./lib && rollup -c",
22
+ "watch": "rimraf ./lib && rollup -c -w",
23
+ "typecheck": "tsc --noEmit"
24
+ },
25
+ "dependencies": {
26
+ "@jsquash/webp": "^1.5.0",
27
+ "@react-pdf/svg": "^1.1.0",
28
+ "jay-peg": "^1.1.1",
29
+ "png-js": "^2.0.0"
30
+ },
31
+ "files": [
32
+ "lib"
33
+ ]
34
+ }