@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/README.md +52 -0
- package/lib/compare-render.d.ts +1 -0
- package/lib/compare-render.js +185 -0
- package/lib/figure.d.ts +10 -0
- package/lib/figure.js +53 -48
- package/lib/ghostscript.d.ts +21 -0
- package/lib/ghostscript.js +98 -128
- package/lib/group.d.ts +5 -0
- package/lib/group.js +4 -8
- package/lib/image.d.ts +21 -0
- package/lib/image.js +147 -89
- package/lib/index.d.ts +26 -0
- package/lib/index.js +133 -0
- package/lib/line.d.ts +10 -0
- package/lib/line.js +41 -57
- package/lib/spot-colors.d.ts +38 -0
- package/lib/spot-colors.js +141 -0
- package/lib/svg-render.d.ts +9 -0
- package/lib/svg-render.js +19 -28
- package/lib/svg.d.ts +11 -0
- package/lib/svg.js +212 -233
- package/lib/text.d.ts +39 -0
- package/lib/text.js +558 -165
- package/lib/utils.d.ts +16 -0
- package/lib/utils.js +118 -72
- package/package.json +35 -15
- package/index.js +0 -130
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
if (
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
67
|
-
|
|
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.
|
|
3
|
+
"version": "0.1.20",
|
|
4
4
|
"description": "Convert Polotno JSON into vector PDF",
|
|
5
|
-
"
|
|
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
|
-
"
|
|
13
|
-
"README.md"
|
|
14
|
-
"lib"
|
|
24
|
+
"lib",
|
|
25
|
+
"README.md"
|
|
15
26
|
],
|
|
16
27
|
"dependencies": {
|
|
17
|
-
"
|
|
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": "^
|
|
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.
|
|
27
|
-
"polotno": "^2.
|
|
28
|
-
"sharp": "^0.34.
|
|
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/
|
|
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
|
-
"
|
|
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
|
-
};
|