@polotno/pdf-export 0.1.18 → 0.1.20

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/utils.d.ts ADDED
@@ -0,0 +1,16 @@
1
+ import parseColor from 'parse-color';
2
+ export declare const DPI = 75;
3
+ export declare const PIXEL_RATIO = 2;
4
+ export declare function fetchWithTimeout(url: string, timeout?: number, retries?: number): Promise<any>;
5
+ export declare function pxToPt(px: number): number;
6
+ export interface ImageCache {
7
+ images: Map<string, any>;
8
+ buffers: Map<string, any>;
9
+ processedImages: Map<string, string>;
10
+ imageFiles: Map<string, string>;
11
+ tempDir: string | null;
12
+ }
13
+ export declare function loadImage(src: string, cache?: ImageCache | null): Promise<any>;
14
+ export declare function srcToBase64(src: string, cache?: ImageCache | null): Promise<string>;
15
+ export declare function srcToBuffer(src: string, cache?: ImageCache | null): Promise<Buffer>;
16
+ export { parseColor };
package/lib/utils.js CHANGED
@@ -1,78 +1,124 @@
1
- const parseColor = require('parse-color');
2
- const fetch = require('node-fetch').default;
3
- const fileTypePromise = import('file-type');
4
- const sharp = require('sharp');
5
- const Canvas = require('canvas');
6
-
7
- const DPI = 75;
8
- const PIXEL_RATIO = 2;
9
-
10
- function pxToPt(px) {
11
- return (px * DPI) / 100;
1
+ import parseColor from 'parse-color';
2
+ import fetch from 'node-fetch';
3
+ import Canvas from 'canvas';
4
+ import sharp from 'sharp';
5
+ export const DPI = 75;
6
+ export const PIXEL_RATIO = 2;
7
+ // Fetch with timeout and retry logic
8
+ export async function fetchWithTimeout(url, timeout = 30000, retries = 3) {
9
+ for (let attempt = 1; attempt <= retries; attempt++) {
10
+ try {
11
+ const controller = new AbortController();
12
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
13
+ const response = await fetch(url, {
14
+ signal: controller.signal,
15
+ });
16
+ clearTimeout(timeoutId);
17
+ if (!response.ok) {
18
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
19
+ }
20
+ return response;
21
+ }
22
+ catch (error) {
23
+ const isLastAttempt = attempt === retries;
24
+ const isAbortError = error.name === 'AbortError';
25
+ const isTimeoutError = error.code === 'ETIMEDOUT' || error.type === 'request-timeout';
26
+ if (isLastAttempt) {
27
+ throw new Error(`Failed to fetch ${url} after ${retries} attempts: ${error.message}`);
28
+ }
29
+ // Only retry on network/timeout errors, not on 4xx client errors
30
+ if (isAbortError || isTimeoutError || error.code?.startsWith('E')) {
31
+ console.warn(`Fetch attempt ${attempt}/${retries} failed for ${url}, retrying...`);
32
+ // Exponential backoff: 1s, 2s, 4s, etc.
33
+ await new Promise((resolve) => setTimeout(resolve, Math.pow(2, attempt - 1) * 1000));
34
+ continue;
35
+ }
36
+ // Don't retry on other errors (e.g., 404, 403)
37
+ throw error;
38
+ }
39
+ }
40
+ throw new Error(`Failed to fetch ${url}`);
41
+ }
42
+ export function pxToPt(px) {
43
+ return (px * DPI) / 100;
12
44
  }
13
-
14
- async function loadImage(src) {
15
- let buffer;
16
- let mime = 'unknown';
17
-
18
- if (src.startsWith('data:')) {
19
- const matches = src.match(/^data:(.+);base64,(.*)$/);
20
- if (matches) {
21
- mime = matches[1];
22
- buffer = Buffer.from(matches[2], 'base64');
23
- } else {
24
- throw new Error('Invalid data URL');
45
+ export async function loadImage(src, cache = null) {
46
+ // Check cache first
47
+ if (cache && cache.images.has(src)) {
48
+ return cache.images.get(src);
49
+ }
50
+ let buffer;
51
+ let mime = 'unknown';
52
+ if (src.startsWith('data:')) {
53
+ const matches = src.match(/^data:(.+);base64,(.*)$/);
54
+ if (matches) {
55
+ mime = matches[1];
56
+ buffer = Buffer.from(matches[2], 'base64');
57
+ }
58
+ else {
59
+ throw new Error('Invalid data URL');
60
+ }
25
61
  }
26
- } else {
27
- try {
28
- const response = await fetch(src);
29
- if (!response.ok) {
30
- throw new Error(
31
- `Failed to fetch image: ${src} (Status: ${response.status})`
32
- );
33
- }
34
- buffer = await response.buffer();
35
- const fileType = await fileTypePromise;
36
- const typeData = await fileType.fileTypeFromBuffer(buffer);
37
- if (typeData) {
38
- ({ mime } = typeData);
39
- }
40
- } catch (error) {
41
- throw new Error(`Failed to process image from ${src}: ${error.message}`);
62
+ else {
63
+ try {
64
+ const response = await fetchWithTimeout(src);
65
+ buffer = await response.buffer();
66
+ const { fileTypeFromBuffer } = await import('file-type');
67
+ const typeData = await fileTypeFromBuffer(buffer);
68
+ if (typeData) {
69
+ mime = typeData.mime;
70
+ }
71
+ }
72
+ catch (error) {
73
+ console.log(error);
74
+ throw new Error(`Failed to process image from ${src}: ${error.message}`);
75
+ }
42
76
  }
43
- }
44
-
45
- let imageBuffer = buffer;
46
- if (mime !== 'image/png' && mime !== 'image/jpeg') {
47
- imageBuffer = await sharp(buffer).toFormat('png').toBuffer();
48
- mime = 'image/png';
49
- }
50
-
51
- return await Canvas.loadImage(imageBuffer);
77
+ let imageBuffer = buffer;
78
+ if (mime !== 'image/png' && mime !== 'image/jpeg') {
79
+ imageBuffer = await sharp(buffer).toFormat('png').toBuffer();
80
+ mime = 'image/png';
81
+ }
82
+ const image = await Canvas.loadImage(imageBuffer);
83
+ // Store in cache
84
+ if (cache) {
85
+ cache.images.set(src, image);
86
+ }
87
+ return image;
52
88
  }
53
-
54
- async function srcToBase64(src) {
55
- if (src.indexOf('base64') >= 0) {
56
- return src.split('base64,')[1];
57
- }
58
- const res = await fetch(src);
59
- if (!res.ok) {
60
- throw new Error(`Failed to fetch: ${src} (Status: ${res.status})`);
61
- }
62
- const data = await res.buffer();
63
- return data.toString('base64');
89
+ export async function srcToBase64(src, cache = null) {
90
+ // For base64 caching, we use a different cache key to avoid collision with buffer cache
91
+ const base64CacheKey = `base64:${src}`;
92
+ // Check cache first
93
+ if (cache && cache.buffers.has(base64CacheKey)) {
94
+ return cache.buffers.get(base64CacheKey);
95
+ }
96
+ let base64;
97
+ if (src.indexOf('base64') >= 0) {
98
+ base64 = src.split('base64,')[1];
99
+ }
100
+ else {
101
+ const res = await fetchWithTimeout(src);
102
+ const data = await res.buffer();
103
+ base64 = data.toString('base64');
104
+ }
105
+ // Store in cache
106
+ if (cache) {
107
+ cache.buffers.set(base64CacheKey, base64);
108
+ }
109
+ return base64;
64
110
  }
65
-
66
- async function srcToBuffer(src) {
67
- return Buffer.from(await srcToBase64(src), 'base64');
111
+ export async function srcToBuffer(src, cache = null) {
112
+ // Check if we have a cached Buffer for this source
113
+ if (cache && cache.buffers.has(src)) {
114
+ return cache.buffers.get(src);
115
+ }
116
+ const base64 = await srcToBase64(src, cache);
117
+ const buffer = Buffer.from(base64, 'base64');
118
+ // Cache the actual Buffer object so PDFKit can reuse it
119
+ if (cache) {
120
+ cache.buffers.set(src, buffer);
121
+ }
122
+ return buffer;
68
123
  }
69
-
70
- module.exports = {
71
- pxToPt,
72
- loadImage,
73
- srcToBase64,
74
- srcToBuffer,
75
- DPI,
76
- PIXEL_RATIO,
77
- parseColor,
78
- };
124
+ export { parseColor };
package/package.json CHANGED
@@ -1,38 +1,58 @@
1
1
  {
2
2
  "name": "@polotno/pdf-export",
3
- "version": "0.1.18",
3
+ "version": "0.1.20",
4
4
  "description": "Convert Polotno JSON into vector PDF",
5
- "main": "index.js",
5
+ "type": "module",
6
+ "main": "./lib/index.js",
7
+ "types": "./lib/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./lib/index.js",
11
+ "types": "./lib/index.d.ts"
12
+ }
13
+ },
6
14
  "scripts": {
15
+ "build": "tsc",
7
16
  "test": "vitest",
8
- "test:update-snapshots": "vitest -u"
17
+ "test:update-snapshots": "vitest -u",
18
+ "dev": "vite client",
19
+ "compare": "node lib/compare-render.js",
20
+ "postinstall": "patch-package"
9
21
  },
10
22
  "author": "Anton Lavrenov",
11
23
  "files": [
12
- "index.js",
13
- "README.md",
14
- "lib"
24
+ "lib",
25
+ "README.md"
15
26
  ],
16
27
  "dependencies": {
17
- "@canvas/image": "^2.0.0",
18
- "@file-type/xml": "^0.4.3",
19
- "canvas": "^3.1.1",
28
+ "canvas": "^3.2.0",
20
29
  "file-type": "^21.0.0",
21
30
  "get-urls": "^12.1.0",
22
- "konva": "^9.3.20",
31
+ "konva": "^10.0.8",
23
32
  "node-fetch": "^3.3.2",
24
33
  "parse-color": "^1.0.0",
25
34
  "pdf2pic": "^3.2.0",
26
- "pdfkit": "^0.17.1",
27
- "polotno": "^2.24.1",
28
- "sharp": "^0.34.2",
35
+ "pdfkit": "^0.17.2",
36
+ "polotno": "^2.29.5",
37
+ "sharp": "^0.34.4",
38
+ "string-strip-html": "^13.5.0",
29
39
  "svg-to-pdfkit": "^0.1.8",
30
40
  "xmldom": "^0.6.0"
31
41
  },
32
42
  "devDependencies": {
33
- "@types/pdfkit": "^0.14.0",
43
+ "@types/node": "^24.10.0",
44
+ "@types/parse-color": "^1.0.3",
45
+ "@types/pdfkit": "^0.17.3",
34
46
  "jest-image-snapshot": "^6.5.1",
47
+ "patch-package": "^8.0.1",
35
48
  "pdf-img-convert": "^2.0.0",
36
- "vitest": "^3.2.4"
49
+ "pdf-to-png-converter": "^3.10.0",
50
+ "pixelmatch": "^7.1.0",
51
+ "pngjs": "^7.0.0",
52
+ "polotno-node": "^2.12.30",
53
+ "typescript": "~5.9.3",
54
+ "@vitejs/plugin-react": "^5.1.0",
55
+ "vite": "^7.2.0",
56
+ "vitest": "^4.0.7"
37
57
  }
38
58
  }
package/index.js DELETED
@@ -1,130 +0,0 @@
1
- const PDFDocument = require('pdfkit');
2
- const fs = require('fs');
3
- const path = require('path');
4
- const { srcToBuffer, parseColor } = require('./lib/utils');
5
- const { renderImage } = require('./lib/image');
6
- const { loadFontIfNeeded, renderText } = require('./lib/text');
7
- const { renderFigure } = require('./lib/figure');
8
- const { renderGroup } = require('./lib/group');
9
- const { lineToPDF } = require('./lib/line');
10
- const { renderSVG } = require('./lib/svg-render');
11
- const { convertToPDFX1a, validatePDFX1a } = require('./lib/ghostscript');
12
- const SVGtoPDF = require('svg-to-pdfkit');
13
-
14
- PDFDocument.prototype.addSVG = function (svg, x, y, options) {
15
- return SVGtoPDF(this, svg, x, y, options), this;
16
- };
17
-
18
- async function renderElement({ doc, element, fonts, attrs }) {
19
- if (!element.visible || !element.showInExport) {
20
- return;
21
- }
22
-
23
- doc.save();
24
- if (element.type !== 'group') {
25
- doc.translate(element.x, element.y);
26
- doc.rotate(element.rotation);
27
- }
28
-
29
- if (element.opacity !== undefined) {
30
- doc.opacity(element.opacity);
31
- }
32
-
33
- if (element.type === 'group') {
34
- await renderGroup(doc, element, renderElement, fonts, attrs);
35
- } else if (element.type === 'text') {
36
- await loadFontIfNeeded(doc, element, fonts);
37
- renderText(doc, element, attrs);
38
- } else if (element.type === 'line') {
39
- lineToPDF(doc, element);
40
- } else if (element.type === 'image') {
41
- await renderImage(doc, element);
42
- } else if (element.type === 'svg') {
43
- await renderSVG(doc, element);
44
- } else if (element.type === 'figure') {
45
- renderFigure(doc, element);
46
- }
47
-
48
- doc.restore();
49
- }
50
-
51
- module.exports.jsonToPDF = async function jsonToPDF(
52
- json,
53
- pdfFileName,
54
- attrs = {}
55
- ) {
56
- const fonts = {};
57
-
58
- var doc = new PDFDocument({
59
- size: [json.width, json.height],
60
- autoFirstPage: false,
61
- });
62
-
63
- for (const font of json.fonts) {
64
- doc.registerFont(font.fontFamily, await srcToBuffer(font.url));
65
- fonts[font.fontFamily] = true;
66
- }
67
-
68
- for (const page of json.pages) {
69
- doc.addPage();
70
- if (page.background) {
71
- const isURL =
72
- page.background.indexOf('http') >= 0 ||
73
- page.background.indexOf('.png') >= 0 ||
74
- page.background.indexOf('.jpg') >= 0;
75
-
76
- if (isURL) {
77
- doc.image(await srcToBuffer(page.background), 0, 0);
78
- } else {
79
- doc.rect(0, 0, json.width, json.height);
80
- doc.fill(parseColor(page.background).hex);
81
- }
82
- }
83
- for (const element of page.children) {
84
- await renderElement({ doc, element, fonts, attrs });
85
- }
86
- }
87
-
88
- doc.end();
89
-
90
- await new Promise((r) =>
91
- doc.pipe(fs.createWriteStream(pdfFileName)).on('finish', r)
92
- );
93
-
94
- // Optional PDF/X-1a conversion
95
- if (attrs.pdfx1a) {
96
- console.log('Converting to PDF/X-1a...');
97
- const tempFileName = pdfFileName.replace('.pdf', '-temp.pdf');
98
-
99
- // Rename current file to temp
100
- fs.renameSync(pdfFileName, tempFileName);
101
-
102
- try {
103
- // Convert temp file to PDF/X-1a
104
- await convertToPDFX1a(tempFileName, pdfFileName, {
105
- metadata: attrs.metadata || {},
106
- });
107
-
108
- // Clean up temp file
109
- fs.unlinkSync(tempFileName);
110
-
111
- // Optional validation
112
- if (attrs.validate) {
113
- const isValid = await validatePDFX1a(pdfFileName);
114
- if (!isValid) {
115
- console.warn(
116
- 'Warning: Generated PDF may not be fully PDF/X-1a compliant'
117
- );
118
- }
119
- }
120
-
121
- console.log('PDF/X-1a conversion completed');
122
- } catch (error) {
123
- // Restore original file if conversion fails
124
- if (fs.existsSync(tempFileName)) {
125
- fs.renameSync(tempFileName, pdfFileName);
126
- }
127
- throw new Error(`PDF/X-1a conversion failed: ${error.message}`);
128
- }
129
- }
130
- };