@polotno/pdf-export 0.1.18 → 0.1.19
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/figure.d.ts +10 -0
- package/lib/figure.js +53 -48
- package/lib/ghostscript.d.ts +21 -0
- package/lib/ghostscript.js +101 -128
- package/lib/group.d.ts +5 -0
- package/lib/group.js +4 -8
- package/lib/image.d.ts +19 -0
- package/lib/image.js +134 -89
- package/lib/index.d.ts +26 -0
- package/lib/index.js +130 -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 +203 -232
- package/lib/text.d.ts +28 -0
- package/lib/text.js +147 -174
- package/lib/utils.d.ts +15 -0
- package/lib/utils.js +88 -72
- package/package.json +22 -14
- package/index.js +0 -130
package/lib/text.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export interface TextElement {
|
|
2
|
+
fontFamily: string;
|
|
3
|
+
fontWeight?: string;
|
|
4
|
+
fontSize: number;
|
|
5
|
+
fill: string;
|
|
6
|
+
opacity: number;
|
|
7
|
+
strokeWidth: number;
|
|
8
|
+
stroke?: string;
|
|
9
|
+
align: string;
|
|
10
|
+
textDecoration: string;
|
|
11
|
+
letterSpacing?: number;
|
|
12
|
+
lineHeight: number;
|
|
13
|
+
text: string;
|
|
14
|
+
width: number;
|
|
15
|
+
height: number;
|
|
16
|
+
verticalAlign?: string;
|
|
17
|
+
backgroundEnabled?: boolean;
|
|
18
|
+
backgroundPadding?: number;
|
|
19
|
+
backgroundCornerRadius?: number;
|
|
20
|
+
backgroundColor?: string;
|
|
21
|
+
}
|
|
22
|
+
export interface RenderAttrs {
|
|
23
|
+
pdfx1a?: boolean;
|
|
24
|
+
textVerticalResizeEnabled?: boolean;
|
|
25
|
+
}
|
|
26
|
+
export declare function getGoogleFontPath(fontFamily: string, fontWeight?: string): Promise<string>;
|
|
27
|
+
export declare function loadFontIfNeeded(doc: any, element: TextElement, fonts: Record<string, boolean>): Promise<string>;
|
|
28
|
+
export declare function renderText(doc: any, element: TextElement, attrs?: RenderAttrs): void;
|
package/lib/text.js
CHANGED
|
@@ -1,184 +1,157 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
1
|
+
import { parseColor, srcToBuffer } from './utils.js';
|
|
2
|
+
import getUrls from 'get-urls';
|
|
3
|
+
import fetch from 'node-fetch';
|
|
4
|
+
export async function getGoogleFontPath(fontFamily, fontWeight = 'normal') {
|
|
5
|
+
const weight = fontWeight === 'bold' ? '700' : '400';
|
|
6
|
+
const url = `https://fonts.googleapis.com/css?family=${fontFamily}:${weight}`;
|
|
7
|
+
const req = await fetch(url);
|
|
8
|
+
if (!req.ok) {
|
|
9
|
+
if (weight !== '400') {
|
|
10
|
+
return getGoogleFontPath(fontFamily, 'normal');
|
|
11
|
+
}
|
|
12
|
+
throw new Error(`Failed to fetch Google font: ${fontFamily}`);
|
|
12
13
|
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const urls = getUrls(text);
|
|
17
|
-
return urls.values().next().value;
|
|
14
|
+
const text = await req.text();
|
|
15
|
+
const urls = getUrls(text);
|
|
16
|
+
return urls.values().next().value;
|
|
18
17
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
return fontKey;
|
|
18
|
+
export async function loadFontIfNeeded(doc, element, fonts) {
|
|
19
|
+
// check if universal font is already defined
|
|
20
|
+
if (fonts[element.fontFamily]) {
|
|
21
|
+
doc.font(element.fontFamily);
|
|
22
|
+
return element.fontFamily;
|
|
23
|
+
}
|
|
24
|
+
const fontKey = `${element.fontFamily}-${element.fontWeight || 'normal'}`;
|
|
25
|
+
if (!fonts[fontKey]) {
|
|
26
|
+
const src = await getGoogleFontPath(element.fontFamily, element.fontWeight);
|
|
27
|
+
doc.registerFont(fontKey, await srcToBuffer(src));
|
|
28
|
+
fonts[fontKey] = true;
|
|
29
|
+
}
|
|
30
|
+
doc.font(fontKey);
|
|
31
|
+
return fontKey;
|
|
34
32
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
// Standard PDF: use PDFKit's built-in stroke support
|
|
46
|
-
doc.lineWidth(element.strokeWidth / 2);
|
|
47
|
-
doc.strokeColor(parseColor(element.stroke).hex);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const props = {
|
|
51
|
-
align: element.align,
|
|
52
|
-
fill: element.fill,
|
|
53
|
-
baseline: 'top',
|
|
54
|
-
lineGap: 1,
|
|
55
|
-
width: element.width,
|
|
56
|
-
underline: element.textDecoration.indexOf('underline') >= 0,
|
|
57
|
-
characterSpacing: element.letterSpacing
|
|
58
|
-
? element.letterSpacing * element.fontSize
|
|
59
|
-
: 0,
|
|
60
|
-
lineBreak: false,
|
|
61
|
-
stroke: hasStroke && !isPDFX1a, // Only use stroke for non-PDF/X-1a
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
const currentLineHeight = doc.heightOfString('A', props);
|
|
65
|
-
const lineHeight = element.lineHeight * element.fontSize;
|
|
66
|
-
|
|
67
|
-
const fontBoundingBoxAscent = (doc._font.ascender / 1000) * element.fontSize;
|
|
68
|
-
const fontBoundingBoxDescent =
|
|
69
|
-
(doc._font.descender / 1000) * element.fontSize;
|
|
70
|
-
const translateY = (fontBoundingBoxAscent - fontBoundingBoxDescent) / 2;
|
|
71
|
-
|
|
72
|
-
const diff = currentLineHeight - lineHeight;
|
|
73
|
-
props.lineGap = props.lineGap - diff;
|
|
74
|
-
|
|
75
|
-
let yOffset = 0;
|
|
76
|
-
if ((attrs.textVerticalResizeEnabled || true) && element.verticalAlign) {
|
|
77
|
-
const textHeight = doc.heightOfString(element.text, props);
|
|
78
|
-
if (element.verticalAlign === 'middle') {
|
|
79
|
-
yOffset = (element.height - textHeight) / 2;
|
|
80
|
-
} else if (element.verticalAlign === 'bottom') {
|
|
81
|
-
yOffset = element.height - textHeight;
|
|
33
|
+
export function renderText(doc, element, attrs = {}) {
|
|
34
|
+
doc.fontSize(element.fontSize);
|
|
35
|
+
doc.fillColor(parseColor(element.fill).hex, element.opacity);
|
|
36
|
+
// Handle stroked text differently for PDF/X-1a compatibility
|
|
37
|
+
const hasStroke = element.strokeWidth > 0;
|
|
38
|
+
const isPDFX1a = attrs.pdfx1a;
|
|
39
|
+
if (hasStroke && !isPDFX1a) {
|
|
40
|
+
// Standard PDF: use PDFKit's built-in stroke support
|
|
41
|
+
doc.lineWidth(element.strokeWidth / 2);
|
|
42
|
+
doc.strokeColor(parseColor(element.stroke).hex);
|
|
82
43
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
44
|
+
const props = {
|
|
45
|
+
align: element.align,
|
|
46
|
+
fill: element.fill,
|
|
47
|
+
baseline: 'top',
|
|
48
|
+
lineGap: 1,
|
|
49
|
+
width: element.width,
|
|
50
|
+
underline: element.textDecoration.indexOf('underline') >= 0,
|
|
51
|
+
characterSpacing: element.letterSpacing
|
|
52
|
+
? element.letterSpacing * element.fontSize
|
|
53
|
+
: 0,
|
|
54
|
+
lineBreak: false,
|
|
55
|
+
stroke: hasStroke && !isPDFX1a, // Only use stroke for non-PDF/X-1a
|
|
56
|
+
};
|
|
57
|
+
const currentLineHeight = doc.heightOfString('A', props);
|
|
58
|
+
const lineHeight = element.lineHeight * element.fontSize;
|
|
59
|
+
const fontBoundingBoxAscent = (doc._font.ascender / 1000) * element.fontSize;
|
|
60
|
+
const fontBoundingBoxDescent = (doc._font.descender / 1000) * element.fontSize;
|
|
61
|
+
const translateY = (fontBoundingBoxAscent - fontBoundingBoxDescent) / 2;
|
|
62
|
+
const diff = currentLineHeight - lineHeight;
|
|
63
|
+
props.lineGap = props.lineGap - diff;
|
|
64
|
+
let yOffset = 0;
|
|
65
|
+
if ((attrs.textVerticalResizeEnabled || true) && element.verticalAlign) {
|
|
66
|
+
const textHeight = doc.heightOfString(element.text, props);
|
|
67
|
+
if (element.verticalAlign === 'middle') {
|
|
68
|
+
yOffset = (element.height - textHeight) / 2;
|
|
69
|
+
}
|
|
70
|
+
else if (element.verticalAlign === 'bottom') {
|
|
71
|
+
yOffset = element.height - textHeight;
|
|
72
|
+
}
|
|
92
73
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
element.backgroundCornerRadius *
|
|
102
|
-
(element.fontSize * element.lineHeight * 0.5);
|
|
103
|
-
|
|
104
|
-
const textWidth = doc.widthOfString(element.text, {
|
|
105
|
-
...props,
|
|
106
|
-
width: element.width,
|
|
107
|
-
});
|
|
108
|
-
const textHeight = doc.heightOfString(element.text, {
|
|
109
|
-
...props,
|
|
110
|
-
width: element.width,
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
let bgX = -backPadding / 2;
|
|
114
|
-
let bgY = -backPadding / 2;
|
|
115
|
-
let bgWidth = textWidth + backPadding;
|
|
116
|
-
let bgHeight = textHeight + backPadding;
|
|
117
|
-
|
|
118
|
-
if (element.align === 'center') {
|
|
119
|
-
bgX = (element.width - textWidth) / 2 - backPadding / 2;
|
|
120
|
-
} else if (element.align === 'right') {
|
|
121
|
-
bgX = element.width - textWidth - backPadding / 2;
|
|
74
|
+
for (var size = element.fontSize; size > 0; size -= 1) {
|
|
75
|
+
doc.fontSize(size);
|
|
76
|
+
const height = doc.heightOfString(element.text, {
|
|
77
|
+
...props,
|
|
78
|
+
});
|
|
79
|
+
if (height <= element.height) {
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
122
82
|
}
|
|
123
|
-
|
|
124
|
-
if (element.
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
83
|
+
const halfLineHeight = ((element.lineHeight - 1) / 2) * element.fontSize;
|
|
84
|
+
if (element.backgroundEnabled) {
|
|
85
|
+
const backPadding = element.backgroundPadding * (element.fontSize * element.lineHeight);
|
|
86
|
+
const cornerRadius = element.backgroundCornerRadius *
|
|
87
|
+
(element.fontSize * element.lineHeight * 0.5);
|
|
88
|
+
const textWidth = doc.widthOfString(element.text, {
|
|
89
|
+
...props,
|
|
90
|
+
width: element.width,
|
|
91
|
+
});
|
|
92
|
+
const textHeight = doc.heightOfString(element.text, {
|
|
93
|
+
...props,
|
|
94
|
+
width: element.width,
|
|
95
|
+
});
|
|
96
|
+
let bgX = -backPadding / 2;
|
|
97
|
+
let bgY = -backPadding / 2;
|
|
98
|
+
let bgWidth = textWidth + backPadding;
|
|
99
|
+
let bgHeight = textHeight + backPadding;
|
|
100
|
+
if (element.align === 'center') {
|
|
101
|
+
bgX = (element.width - textWidth) / 2 - backPadding / 2;
|
|
102
|
+
}
|
|
103
|
+
else if (element.align === 'right') {
|
|
104
|
+
bgX = element.width - textWidth - backPadding / 2;
|
|
105
|
+
}
|
|
106
|
+
if (element.verticalAlign === 'middle') {
|
|
107
|
+
bgY = (element.height - textHeight) / 2 - backPadding / 2;
|
|
108
|
+
}
|
|
109
|
+
else if (element.verticalAlign === 'bottom') {
|
|
110
|
+
bgY = element.height - textHeight - backPadding / 2;
|
|
111
|
+
}
|
|
112
|
+
doc.roundedRect(bgX, bgY, bgWidth, bgHeight, cornerRadius);
|
|
113
|
+
doc.fillColor(parseColor(element.backgroundColor).hex);
|
|
114
|
+
doc.fill();
|
|
115
|
+
doc.fillColor(parseColor(element.fill).hex, element.opacity);
|
|
128
116
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
117
|
+
// Render text with PDF/X-1a compatible stroke simulation
|
|
118
|
+
if (hasStroke && isPDFX1a) {
|
|
119
|
+
// For PDF/X-1a: simulate stroke by drawing text multiple times
|
|
120
|
+
const strokeColor = parseColor(element.stroke).hex;
|
|
121
|
+
const strokeWidth = element.strokeWidth;
|
|
122
|
+
// Draw stroke by rendering text multiple times with offsets
|
|
123
|
+
const offsets = [];
|
|
124
|
+
for (let angle = 0; angle < 360; angle += 45) {
|
|
125
|
+
const radian = (angle * Math.PI) / 180;
|
|
126
|
+
offsets.push({
|
|
127
|
+
x: Math.cos(radian) * strokeWidth,
|
|
128
|
+
y: Math.sin(radian) * strokeWidth,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
// Draw stroke layers
|
|
132
|
+
doc.save();
|
|
133
|
+
doc.fillColor(strokeColor, element.opacity);
|
|
134
|
+
for (const offset of offsets) {
|
|
135
|
+
doc.text(element.text, offset.x, yOffset + halfLineHeight + offset.y, {
|
|
136
|
+
...props,
|
|
137
|
+
stroke: false, // Force no stroke for compatibility
|
|
138
|
+
height: element.height + element.fontSize,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
doc.restore();
|
|
142
|
+
// Draw fill text on top
|
|
143
|
+
doc.fillColor(parseColor(element.fill).hex, element.opacity);
|
|
144
|
+
doc.text(element.text, 0, yOffset + halfLineHeight, {
|
|
145
|
+
...props,
|
|
146
|
+
stroke: false, // Force no stroke for compatibility
|
|
147
|
+
height: element.height + element.fontSize,
|
|
148
|
+
});
|
|
150
149
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
...props,
|
|
158
|
-
stroke: false, // Force no stroke for compatibility
|
|
159
|
-
height: element.height + element.fontSize,
|
|
160
|
-
});
|
|
150
|
+
else {
|
|
151
|
+
// Standard rendering
|
|
152
|
+
doc.text(element.text, 0, yOffset + halfLineHeight, {
|
|
153
|
+
...props,
|
|
154
|
+
height: element.height + element.fontSize,
|
|
155
|
+
});
|
|
161
156
|
}
|
|
162
|
-
doc.restore();
|
|
163
|
-
|
|
164
|
-
// Draw fill text on top
|
|
165
|
-
doc.fillColor(parseColor(element.fill).hex, element.opacity);
|
|
166
|
-
doc.text(element.text, 0, yOffset + halfLineHeight, {
|
|
167
|
-
...props,
|
|
168
|
-
stroke: false, // Force no stroke for compatibility
|
|
169
|
-
height: element.height + element.fontSize,
|
|
170
|
-
});
|
|
171
|
-
} else {
|
|
172
|
-
// Standard rendering
|
|
173
|
-
doc.text(element.text, 0, yOffset + halfLineHeight, {
|
|
174
|
-
...props,
|
|
175
|
-
height: element.height + element.fontSize,
|
|
176
|
-
});
|
|
177
|
-
}
|
|
178
157
|
}
|
|
179
|
-
|
|
180
|
-
module.exports = {
|
|
181
|
-
getGoogleFontPath,
|
|
182
|
-
renderText,
|
|
183
|
-
loadFontIfNeeded,
|
|
184
|
-
};
|
package/lib/utils.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import parseColor from 'parse-color';
|
|
2
|
+
export declare const DPI = 75;
|
|
3
|
+
export declare const PIXEL_RATIO = 2;
|
|
4
|
+
export declare function pxToPt(px: number): number;
|
|
5
|
+
export interface ImageCache {
|
|
6
|
+
images: Map<string, any>;
|
|
7
|
+
buffers: Map<string, any>;
|
|
8
|
+
processedImages: Map<string, string>;
|
|
9
|
+
imageFiles: Map<string, string>;
|
|
10
|
+
tempDir: string | null;
|
|
11
|
+
}
|
|
12
|
+
export declare function loadImage(src: string, cache?: ImageCache | null): Promise<any>;
|
|
13
|
+
export declare function srcToBase64(src: string, cache?: ImageCache | null): Promise<string>;
|
|
14
|
+
export declare function srcToBuffer(src: string, cache?: ImageCache | null): Promise<Buffer>;
|
|
15
|
+
export { parseColor };
|
package/lib/utils.js
CHANGED
|
@@ -1,78 +1,94 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
+
export function pxToPt(px) {
|
|
8
|
+
return (px * DPI) / 100;
|
|
12
9
|
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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');
|
|
10
|
+
export async function loadImage(src, cache = null) {
|
|
11
|
+
// Check cache first
|
|
12
|
+
if (cache && cache.images.has(src)) {
|
|
13
|
+
return cache.images.get(src);
|
|
25
14
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
if (typeData) {
|
|
38
|
-
({ mime } = typeData);
|
|
39
|
-
}
|
|
40
|
-
} catch (error) {
|
|
41
|
-
throw new Error(`Failed to process image from ${src}: ${error.message}`);
|
|
15
|
+
let buffer;
|
|
16
|
+
let mime = 'unknown';
|
|
17
|
+
if (src.startsWith('data:')) {
|
|
18
|
+
const matches = src.match(/^data:(.+);base64,(.*)$/);
|
|
19
|
+
if (matches) {
|
|
20
|
+
mime = matches[1];
|
|
21
|
+
buffer = Buffer.from(matches[2], 'base64');
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
throw new Error('Invalid data URL');
|
|
25
|
+
}
|
|
42
26
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
27
|
+
else {
|
|
28
|
+
try {
|
|
29
|
+
const response = await fetch(src);
|
|
30
|
+
if (!response.ok) {
|
|
31
|
+
throw new Error(`Failed to fetch image: ${src} (Status: ${response.status})`);
|
|
32
|
+
}
|
|
33
|
+
buffer = await response.buffer();
|
|
34
|
+
const { fileTypeFromBuffer } = await import('file-type');
|
|
35
|
+
const typeData = await fileTypeFromBuffer(buffer);
|
|
36
|
+
if (typeData) {
|
|
37
|
+
mime = typeData.mime;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
throw new Error(`Failed to process image from ${src}: ${error.message}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
let imageBuffer = buffer;
|
|
45
|
+
if (mime !== 'image/png' && mime !== 'image/jpeg') {
|
|
46
|
+
imageBuffer = await sharp(buffer).toFormat('png').toBuffer();
|
|
47
|
+
mime = 'image/png';
|
|
48
|
+
}
|
|
49
|
+
const image = await Canvas.loadImage(imageBuffer);
|
|
50
|
+
// Store in cache
|
|
51
|
+
if (cache) {
|
|
52
|
+
cache.images.set(src, image);
|
|
53
|
+
}
|
|
54
|
+
return image;
|
|
52
55
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
56
|
+
export async function srcToBase64(src, cache = null) {
|
|
57
|
+
// For base64 caching, we use a different cache key to avoid collision with buffer cache
|
|
58
|
+
const base64CacheKey = `base64:${src}`;
|
|
59
|
+
// Check cache first
|
|
60
|
+
if (cache && cache.buffers.has(base64CacheKey)) {
|
|
61
|
+
return cache.buffers.get(base64CacheKey);
|
|
62
|
+
}
|
|
63
|
+
let base64;
|
|
64
|
+
if (src.indexOf('base64') >= 0) {
|
|
65
|
+
base64 = src.split('base64,')[1];
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
const res = await fetch(src);
|
|
69
|
+
if (!res.ok) {
|
|
70
|
+
throw new Error(`Failed to fetch: ${src} (Status: ${res.status})`);
|
|
71
|
+
}
|
|
72
|
+
const data = await res.buffer();
|
|
73
|
+
base64 = data.toString('base64');
|
|
74
|
+
}
|
|
75
|
+
// Store in cache
|
|
76
|
+
if (cache) {
|
|
77
|
+
cache.buffers.set(base64CacheKey, base64);
|
|
78
|
+
}
|
|
79
|
+
return base64;
|
|
64
80
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
81
|
+
export async function srcToBuffer(src, cache = null) {
|
|
82
|
+
// Check if we have a cached Buffer for this source
|
|
83
|
+
if (cache && cache.buffers.has(src)) {
|
|
84
|
+
return cache.buffers.get(src);
|
|
85
|
+
}
|
|
86
|
+
const base64 = await srcToBase64(src, cache);
|
|
87
|
+
const buffer = Buffer.from(base64, 'base64');
|
|
88
|
+
// Cache the actual Buffer object so PDFKit can reuse it
|
|
89
|
+
if (cache) {
|
|
90
|
+
cache.buffers.set(src, buffer);
|
|
91
|
+
}
|
|
92
|
+
return buffer;
|
|
68
93
|
}
|
|
69
|
-
|
|
70
|
-
module.exports = {
|
|
71
|
-
pxToPt,
|
|
72
|
-
loadImage,
|
|
73
|
-
srcToBase64,
|
|
74
|
-
srcToBuffer,
|
|
75
|
-
DPI,
|
|
76
|
-
PIXEL_RATIO,
|
|
77
|
-
parseColor,
|
|
78
|
-
};
|
|
94
|
+
export { parseColor };
|
package/package.json
CHANGED
|
@@ -1,38 +1,46 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@polotno/pdf-export",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.19",
|
|
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
17
|
"test:update-snapshots": "vitest -u"
|
|
9
18
|
},
|
|
10
19
|
"author": "Anton Lavrenov",
|
|
11
20
|
"files": [
|
|
12
|
-
"
|
|
13
|
-
"README.md"
|
|
14
|
-
"lib"
|
|
21
|
+
"lib",
|
|
22
|
+
"README.md"
|
|
15
23
|
],
|
|
16
24
|
"dependencies": {
|
|
17
|
-
"
|
|
18
|
-
"@file-type/xml": "^0.4.3",
|
|
19
|
-
"canvas": "^3.1.1",
|
|
25
|
+
"canvas": "^3.2.0",
|
|
20
26
|
"file-type": "^21.0.0",
|
|
21
27
|
"get-urls": "^12.1.0",
|
|
22
|
-
"konva": "^
|
|
28
|
+
"konva": "^10.0.8",
|
|
23
29
|
"node-fetch": "^3.3.2",
|
|
24
30
|
"parse-color": "^1.0.0",
|
|
31
|
+
"pdfkit": "^0.17.2",
|
|
25
32
|
"pdf2pic": "^3.2.0",
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"sharp": "^0.34.2",
|
|
33
|
+
"polotno": "^2.29.5",
|
|
34
|
+
"sharp": "^0.34.4",
|
|
29
35
|
"svg-to-pdfkit": "^0.1.8",
|
|
30
36
|
"xmldom": "^0.6.0"
|
|
31
37
|
},
|
|
32
38
|
"devDependencies": {
|
|
33
|
-
"@types/
|
|
39
|
+
"@types/node": "^24.9.1",
|
|
40
|
+
"@types/pdfkit": "^0.17.3",
|
|
34
41
|
"jest-image-snapshot": "^6.5.1",
|
|
35
42
|
"pdf-img-convert": "^2.0.0",
|
|
36
|
-
"
|
|
43
|
+
"typescript": "~5.9.3",
|
|
44
|
+
"vitest": "^4.0.3"
|
|
37
45
|
}
|
|
38
46
|
}
|