@polotno/pdf-export 0.1.38 → 0.1.40
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 +61 -8
- package/lib/index.d.ts +66 -8
- package/lib/index.js +25 -145
- package/package.json +17 -18
- package/lib/compare-render.d.ts +0 -1
- package/lib/compare-render.js +0 -185
- package/lib/figure.d.ts +0 -10
- package/lib/figure.js +0 -54
- package/lib/filters.d.ts +0 -2
- package/lib/filters.js +0 -163
- package/lib/ghostscript.d.ts +0 -21
- package/lib/ghostscript.js +0 -132
- package/lib/group.d.ts +0 -5
- package/lib/group.js +0 -5
- package/lib/image.d.ts +0 -38
- package/lib/image.js +0 -279
- package/lib/line.d.ts +0 -10
- package/lib/line.js +0 -66
- package/lib/pdf-import/coordinate-transform.d.ts +0 -51
- package/lib/pdf-import/coordinate-transform.js +0 -99
- package/lib/pdf-import/element-builder.d.ts +0 -21
- package/lib/pdf-import/element-builder.js +0 -163
- package/lib/pdf-import/font-mapper.d.ts +0 -17
- package/lib/pdf-import/font-mapper.js +0 -142
- package/lib/pdf-import/index.d.ts +0 -35
- package/lib/pdf-import/index.js +0 -105
- package/lib/pdf-import/parser.d.ts +0 -29
- package/lib/pdf-import/parser.js +0 -285
- package/lib/pdf-import/text-analysis.d.ts +0 -17
- package/lib/pdf-import/text-analysis.js +0 -186
- package/lib/pdf-import/types.d.ts +0 -101
- package/lib/pdf-import/types.js +0 -1
- package/lib/scripts/compare-json.d.ts +0 -1
- package/lib/scripts/compare-json.js +0 -141
- package/lib/spot-colors.d.ts +0 -38
- package/lib/spot-colors.js +0 -141
- package/lib/svg-render.d.ts +0 -9
- package/lib/svg-render.js +0 -63
- package/lib/svg.d.ts +0 -12
- package/lib/svg.js +0 -224
- package/lib/text/fonts.d.ts +0 -16
- package/lib/text/fonts.js +0 -113
- package/lib/text/index.d.ts +0 -8
- package/lib/text/index.js +0 -42
- package/lib/text/layout.d.ts +0 -22
- package/lib/text/layout.js +0 -522
- package/lib/text/parser.d.ts +0 -46
- package/lib/text/parser.js +0 -415
- package/lib/text/render.d.ts +0 -8
- package/lib/text/render.js +0 -237
- package/lib/text/types.d.ts +0 -91
- package/lib/text/types.js +0 -1
- package/lib/text.d.ts +0 -39
- package/lib/text.js +0 -576
- package/lib/utils.d.ts +0 -16
- package/lib/utils.js +0 -124
package/lib/image.js
DELETED
|
@@ -1,279 +0,0 @@
|
|
|
1
|
-
import { loadImage, PIXEL_RATIO, srcToBuffer, parseColor, } from './utils.js';
|
|
2
|
-
import Canvas from 'canvas';
|
|
3
|
-
import fs from 'fs';
|
|
4
|
-
import path from 'path';
|
|
5
|
-
import os from 'os';
|
|
6
|
-
import crypto from 'crypto';
|
|
7
|
-
import Konva from 'konva';
|
|
8
|
-
import { elementFilterToKonva } from './filters.js';
|
|
9
|
-
async function applyFlip(image, element) {
|
|
10
|
-
const { flipX, flipY } = element;
|
|
11
|
-
if (!flipX && !flipY) {
|
|
12
|
-
return image;
|
|
13
|
-
}
|
|
14
|
-
if (!image || !image.width || !image.height) {
|
|
15
|
-
return null;
|
|
16
|
-
}
|
|
17
|
-
const canvas = new Canvas.Canvas(image.width, image.height);
|
|
18
|
-
const ctx = canvas.getContext('2d');
|
|
19
|
-
let x = flipX ? -canvas.width : 0;
|
|
20
|
-
let y = flipY ? -canvas.height : 0;
|
|
21
|
-
ctx.scale(flipX ? -1 : 1, flipY ? -1 : 1);
|
|
22
|
-
ctx.drawImage(image, x, y, canvas.width, canvas.height);
|
|
23
|
-
return canvas;
|
|
24
|
-
}
|
|
25
|
-
// Helper to create cache key for processed images
|
|
26
|
-
export function getProcessedImageKey(element) {
|
|
27
|
-
return JSON.stringify({
|
|
28
|
-
src: element.src,
|
|
29
|
-
width: element.width,
|
|
30
|
-
height: element.height,
|
|
31
|
-
cropX: element.cropX,
|
|
32
|
-
cropY: element.cropY,
|
|
33
|
-
cropWidth: element.cropWidth,
|
|
34
|
-
cropHeight: element.cropHeight,
|
|
35
|
-
clipSrc: element.clipSrc,
|
|
36
|
-
brightnessEnabled: element.brightnessEnabled,
|
|
37
|
-
brightness: element.brightness,
|
|
38
|
-
grayscaleEnabled: element.grayscaleEnabled,
|
|
39
|
-
sepiaEnabled: element.sepiaEnabled,
|
|
40
|
-
blurEnabled: element.blurEnabled,
|
|
41
|
-
blurRadius: element.blurRadius,
|
|
42
|
-
filters: element.filters,
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
export async function cropImage(src, element, cache = null) {
|
|
46
|
-
let image = await loadImage(src);
|
|
47
|
-
// Apply flip transformations first
|
|
48
|
-
image = await applyFlip(image, element);
|
|
49
|
-
if (!image) {
|
|
50
|
-
return null;
|
|
51
|
-
}
|
|
52
|
-
const canvas = Canvas.createCanvas(element.width * PIXEL_RATIO, element.height * PIXEL_RATIO);
|
|
53
|
-
const ctx = canvas.getContext('2d');
|
|
54
|
-
let { cropX, cropY } = element;
|
|
55
|
-
const availableWidth = image.width * element.cropWidth;
|
|
56
|
-
const availableHeight = image.height * element.cropHeight;
|
|
57
|
-
const aspectRatio = element.width / element.height;
|
|
58
|
-
let cropAbsoluteWidth;
|
|
59
|
-
let cropAbsoluteHeight;
|
|
60
|
-
const imageRatio = availableWidth / availableHeight;
|
|
61
|
-
const allowScale = element.type === 'svg';
|
|
62
|
-
if (allowScale) {
|
|
63
|
-
cropAbsoluteWidth = availableWidth;
|
|
64
|
-
cropAbsoluteHeight = availableHeight;
|
|
65
|
-
}
|
|
66
|
-
else if (aspectRatio >= imageRatio) {
|
|
67
|
-
cropAbsoluteWidth = availableWidth;
|
|
68
|
-
cropAbsoluteHeight = availableWidth / aspectRatio;
|
|
69
|
-
}
|
|
70
|
-
else {
|
|
71
|
-
cropAbsoluteWidth = availableHeight * aspectRatio;
|
|
72
|
-
cropAbsoluteHeight = availableHeight;
|
|
73
|
-
}
|
|
74
|
-
ctx.drawImage(image, cropX * image.width, cropY * image.height, cropAbsoluteWidth, cropAbsoluteHeight, 0, 0, canvas.width, canvas.height);
|
|
75
|
-
return canvas.toDataURL('image/png');
|
|
76
|
-
}
|
|
77
|
-
export async function clipImage(src, element, cache = null) {
|
|
78
|
-
const image = await loadImage(src, cache);
|
|
79
|
-
const clipImage = await loadImage(element.clipSrc, cache);
|
|
80
|
-
const canvas = Canvas.createCanvas(element.width, element.height);
|
|
81
|
-
const ctx = canvas.getContext('2d');
|
|
82
|
-
ctx.drawImage(image, 0, 0, element.width, element.height);
|
|
83
|
-
const clipCanvas = Canvas.createCanvas(element.width, element.height);
|
|
84
|
-
const clipCtx = clipCanvas.getContext('2d');
|
|
85
|
-
clipCtx.drawImage(clipImage, 0, 0, element.width, element.height);
|
|
86
|
-
ctx.globalCompositeOperation = 'destination-in';
|
|
87
|
-
ctx.drawImage(clipCanvas, 0, 0);
|
|
88
|
-
ctx.globalCompositeOperation = 'source-over';
|
|
89
|
-
return canvas.toDataURL('image/png');
|
|
90
|
-
}
|
|
91
|
-
async function applyFilter(src, element, cache = null) {
|
|
92
|
-
const image = await loadImage(src, cache);
|
|
93
|
-
const canvas = Canvas.createCanvas(element.width * PIXEL_RATIO, element.height * PIXEL_RATIO);
|
|
94
|
-
const ctx = canvas.getContext('2d');
|
|
95
|
-
ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
|
|
96
|
-
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
97
|
-
const data = imageData.data;
|
|
98
|
-
if (element.brightnessEnabled) {
|
|
99
|
-
Konva.Filters.Brighten.bind({
|
|
100
|
-
brightness: () => element.brightness,
|
|
101
|
-
})({ data });
|
|
102
|
-
}
|
|
103
|
-
// grayscale
|
|
104
|
-
if (element.grayscaleEnabled) {
|
|
105
|
-
Konva.Filters.Grayscale.bind({})({ data });
|
|
106
|
-
}
|
|
107
|
-
// sepia
|
|
108
|
-
if (element.sepiaEnabled) {
|
|
109
|
-
Konva.Filters.Sepia.bind({})({ data });
|
|
110
|
-
}
|
|
111
|
-
// blur
|
|
112
|
-
if (element.blurEnabled) {
|
|
113
|
-
Konva.Filters.Blur.bind({
|
|
114
|
-
blurRadius: () => element.blurRadius,
|
|
115
|
-
})({ data });
|
|
116
|
-
}
|
|
117
|
-
// filters
|
|
118
|
-
if (element.filters) {
|
|
119
|
-
Object.entries(element.filters).forEach(([type, effect]) => {
|
|
120
|
-
const filter = elementFilterToKonva[type];
|
|
121
|
-
if (filter) {
|
|
122
|
-
filter(effect.intensity).bind({})({ data });
|
|
123
|
-
}
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
ctx.putImageData(imageData, 0, 0);
|
|
127
|
-
return canvas.toDataURL('image/png');
|
|
128
|
-
}
|
|
129
|
-
function applyBorder(doc, element) {
|
|
130
|
-
if (element.borderSize > 0) {
|
|
131
|
-
const borderColor = parseColor(element.borderColor).keyword || 'black';
|
|
132
|
-
doc
|
|
133
|
-
.rect(element.borderSize / 2, element.borderSize / 2, element.width - element.borderSize, element.height - element.borderSize)
|
|
134
|
-
.lineWidth(element.borderSize)
|
|
135
|
-
.strokeColor(borderColor)
|
|
136
|
-
.stroke();
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
function saveToTempFile(buffer, key, cache) {
|
|
140
|
-
if (cache) {
|
|
141
|
-
if (!cache.imageFiles) {
|
|
142
|
-
cache.imageFiles = new Map();
|
|
143
|
-
}
|
|
144
|
-
if (!cache.tempDir) {
|
|
145
|
-
cache.tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pdfkit-images-'));
|
|
146
|
-
}
|
|
147
|
-
// Create a unique filename based on cache key hash
|
|
148
|
-
const hash = crypto.createHash('md5').update(key).digest('hex');
|
|
149
|
-
const filePath = path.join(cache.tempDir, `${hash}.png`);
|
|
150
|
-
// Write buffer to file
|
|
151
|
-
fs.writeFileSync(filePath, buffer);
|
|
152
|
-
cache.imageFiles.set(key, filePath);
|
|
153
|
-
return filePath;
|
|
154
|
-
}
|
|
155
|
-
else {
|
|
156
|
-
return buffer;
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
async function getShadowImage(src, element, cache) {
|
|
160
|
-
const image = await loadImage(src, cache);
|
|
161
|
-
const { shadowBlur, shadowColor, shadowOpacity } = element;
|
|
162
|
-
// Shadow blur in Konva is standard. In Canvas it is standard.
|
|
163
|
-
// We need to scale it by PIXEL_RATIO as our canvas is scaled.
|
|
164
|
-
const ratio = PIXEL_RATIO;
|
|
165
|
-
const blur = (shadowBlur || 0) * ratio;
|
|
166
|
-
// Padding.
|
|
167
|
-
const padding = blur * 4 + 20; // Sufficient padding
|
|
168
|
-
const width = image.width + padding * 2;
|
|
169
|
-
const height = image.height + padding * 2;
|
|
170
|
-
const canvas = Canvas.createCanvas(width, height);
|
|
171
|
-
const ctx = canvas.getContext('2d');
|
|
172
|
-
// Parse color
|
|
173
|
-
const parsed = parseColor(shadowColor || 'black');
|
|
174
|
-
const opacity = shadowOpacity !== undefined ? shadowOpacity : 1;
|
|
175
|
-
const r = parsed.rgb[0];
|
|
176
|
-
const g = parsed.rgb[1];
|
|
177
|
-
const b = parsed.rgb[2];
|
|
178
|
-
const a = opacity;
|
|
179
|
-
const colorString = `rgba(${r},${g},${b},${a})`;
|
|
180
|
-
ctx.shadowColor = colorString;
|
|
181
|
-
ctx.shadowBlur = blur;
|
|
182
|
-
// We want the shadow to appear at (padding, padding) relative to canvas.
|
|
183
|
-
// We draw the image at (padding - OFFSET, padding - OFFSET)
|
|
184
|
-
// And set shadowOffset to (OFFSET, OFFSET).
|
|
185
|
-
// OFFSET should be large enough to move image out of view.
|
|
186
|
-
const OFFSET = 10000;
|
|
187
|
-
ctx.shadowOffsetX = OFFSET;
|
|
188
|
-
ctx.shadowOffsetY = OFFSET;
|
|
189
|
-
// Draw image
|
|
190
|
-
ctx.drawImage(image, padding - OFFSET, padding - OFFSET, image.width, image.height);
|
|
191
|
-
return {
|
|
192
|
-
src: canvas.toDataURL('image/png'),
|
|
193
|
-
padding: padding / ratio, // return padding in original units (points)
|
|
194
|
-
width: width / ratio,
|
|
195
|
-
height: height / ratio,
|
|
196
|
-
};
|
|
197
|
-
}
|
|
198
|
-
export async function renderImage(doc, element, cache = null) {
|
|
199
|
-
// Check if we have a cached processed version
|
|
200
|
-
const cacheKey = getProcessedImageKey(element);
|
|
201
|
-
let src = null;
|
|
202
|
-
const hasCachedFile = cache && cache.imageFiles && cache.imageFiles.has(cacheKey);
|
|
203
|
-
if (hasCachedFile) {
|
|
204
|
-
src = cache.imageFiles.get(cacheKey);
|
|
205
|
-
}
|
|
206
|
-
else if (cache && cache.processedImages.has(cacheKey)) {
|
|
207
|
-
src = cache.processedImages.get(cacheKey);
|
|
208
|
-
}
|
|
209
|
-
else {
|
|
210
|
-
src = await cropImage(element.src, element, cache);
|
|
211
|
-
if (element.clipSrc) {
|
|
212
|
-
src = await clipImage(src, element, cache);
|
|
213
|
-
}
|
|
214
|
-
src = await applyFilter(src, element, cache);
|
|
215
|
-
// Cache the processed result
|
|
216
|
-
if (cache && src) {
|
|
217
|
-
cache.processedImages.set(cacheKey, src);
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
if (element.shadowEnabled && src) {
|
|
221
|
-
const shadowKey = cacheKey +
|
|
222
|
-
'_shadow_' +
|
|
223
|
-
JSON.stringify({
|
|
224
|
-
blur: element.shadowBlur,
|
|
225
|
-
color: element.shadowColor,
|
|
226
|
-
opacity: element.shadowOpacity,
|
|
227
|
-
});
|
|
228
|
-
let shadowPadding = 0;
|
|
229
|
-
let shadowW = 0;
|
|
230
|
-
let shadowH = 0;
|
|
231
|
-
if (cache && cache.imageFiles && cache.imageFiles.has(shadowKey)) {
|
|
232
|
-
const filePath = cache.imageFiles.get(shadowKey);
|
|
233
|
-
// Recalculate padding/dimensions as we don't cache them
|
|
234
|
-
const ratio = PIXEL_RATIO;
|
|
235
|
-
const blur = (element.shadowBlur || 0) * ratio;
|
|
236
|
-
shadowPadding = (blur * 4 + 20) / ratio;
|
|
237
|
-
shadowW = element.width + shadowPadding * 2;
|
|
238
|
-
shadowH = element.height + shadowPadding * 2;
|
|
239
|
-
console.log('✓ Using cached shadow file:', path.basename(filePath));
|
|
240
|
-
doc.image(filePath, (element.shadowOffsetX || 0) - shadowPadding, (element.shadowOffsetY || 0) - shadowPadding, {
|
|
241
|
-
width: shadowW,
|
|
242
|
-
height: shadowH,
|
|
243
|
-
});
|
|
244
|
-
}
|
|
245
|
-
else {
|
|
246
|
-
const shadowResult = await getShadowImage(src, element, cache);
|
|
247
|
-
const shadowSrc = shadowResult.src;
|
|
248
|
-
shadowPadding = shadowResult.padding;
|
|
249
|
-
shadowW = shadowResult.width;
|
|
250
|
-
shadowH = shadowResult.height;
|
|
251
|
-
if (shadowSrc) {
|
|
252
|
-
const buffer = await srcToBuffer(shadowSrc, cache);
|
|
253
|
-
const filePath = saveToTempFile(buffer, shadowKey, cache);
|
|
254
|
-
doc.image(filePath, (element.shadowOffsetX || 0) - shadowPadding, (element.shadowOffsetY || 0) - shadowPadding, {
|
|
255
|
-
width: shadowW,
|
|
256
|
-
height: shadowH,
|
|
257
|
-
});
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
if (src) {
|
|
262
|
-
if (hasCachedFile) {
|
|
263
|
-
console.log('✓ Using cached image file:', path.basename(src));
|
|
264
|
-
doc.image(src, 0, 0, {
|
|
265
|
-
width: element.width,
|
|
266
|
-
height: element.height,
|
|
267
|
-
});
|
|
268
|
-
}
|
|
269
|
-
else {
|
|
270
|
-
const buffer = await srcToBuffer(src, cache);
|
|
271
|
-
const filePath = saveToTempFile(buffer, cacheKey, cache);
|
|
272
|
-
doc.image(filePath, 0, 0, {
|
|
273
|
-
width: element.width,
|
|
274
|
-
height: element.height,
|
|
275
|
-
});
|
|
276
|
-
}
|
|
277
|
-
applyBorder(doc, element);
|
|
278
|
-
}
|
|
279
|
-
}
|
package/lib/line.d.ts
DELETED
package/lib/line.js
DELETED
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import { Util } from 'konva/lib/Util.js';
|
|
2
|
-
function rgbToHex({ r, g, b }) {
|
|
3
|
-
// Ensure each value is within the valid range
|
|
4
|
-
r = Math.max(0, Math.min(255, r));
|
|
5
|
-
g = Math.max(0, Math.min(255, g));
|
|
6
|
-
b = Math.max(0, Math.min(255, b));
|
|
7
|
-
// Convert each value to a 2-digit hexadecimal string
|
|
8
|
-
const hexR = r.toString(16).padStart(2, '0');
|
|
9
|
-
const hexG = g.toString(16).padStart(2, '0');
|
|
10
|
-
const hexB = b.toString(16).padStart(2, '0');
|
|
11
|
-
// Return the concatenated hex string
|
|
12
|
-
return `#${hexR}${hexG}${hexB}`;
|
|
13
|
-
}
|
|
14
|
-
function getLineHead({ element, type, doc }) {
|
|
15
|
-
doc.lineWidth(element.height);
|
|
16
|
-
doc.lineCap('round');
|
|
17
|
-
doc.lineJoin('round');
|
|
18
|
-
doc.opacity(element.opacity);
|
|
19
|
-
const rgba = Util.colorToRGBA(element.color);
|
|
20
|
-
const fillColor = rgbToHex(rgba);
|
|
21
|
-
if (type === 'arrow') {
|
|
22
|
-
doc
|
|
23
|
-
.moveTo(element.height * 3, -element.height * 2)
|
|
24
|
-
.lineTo(0, 0)
|
|
25
|
-
.lineTo(element.height * 3, element.height * 2);
|
|
26
|
-
doc.stroke();
|
|
27
|
-
return;
|
|
28
|
-
}
|
|
29
|
-
else if (type === 'triangle') {
|
|
30
|
-
doc.polygon([element.height * 3, -element.height * 2], [0, 0], [element.height * 3, element.height * 2]);
|
|
31
|
-
}
|
|
32
|
-
else if (type === 'bar') {
|
|
33
|
-
doc.polygon([0, -element.height * 2], [0, 0], [0, element.height * 2]);
|
|
34
|
-
}
|
|
35
|
-
else if (type === 'circle') {
|
|
36
|
-
doc.circle(element.height * 2, 0, element.height * 2);
|
|
37
|
-
}
|
|
38
|
-
else if (type === 'square') {
|
|
39
|
-
doc.rect(0, -element.height * 2, element.height * 4, element.height * 4);
|
|
40
|
-
}
|
|
41
|
-
else {
|
|
42
|
-
return;
|
|
43
|
-
}
|
|
44
|
-
doc.fillAndStroke(fillColor, fillColor);
|
|
45
|
-
}
|
|
46
|
-
export function lineToPDF(doc, element) {
|
|
47
|
-
doc.translate(0, element.height / 2);
|
|
48
|
-
doc.lineWidth(element.height);
|
|
49
|
-
doc.moveTo(0, 0);
|
|
50
|
-
doc.lineTo(element.width, 0);
|
|
51
|
-
if (element.dash && element.dash.length > 0) {
|
|
52
|
-
doc.dash(element.dash.map((dash) => dash * element.height));
|
|
53
|
-
}
|
|
54
|
-
const rgba = Util.colorToRGBA(element.color);
|
|
55
|
-
doc.strokeColor(rgbToHex(rgba));
|
|
56
|
-
doc.stroke();
|
|
57
|
-
if (element.dash && element.dash.length > 0) {
|
|
58
|
-
doc.undash();
|
|
59
|
-
}
|
|
60
|
-
getLineHead({ element, doc: doc, type: element.startHead });
|
|
61
|
-
getLineHead({
|
|
62
|
-
element,
|
|
63
|
-
doc: doc.translate(element.width, 0).rotate(180),
|
|
64
|
-
type: element.endHead,
|
|
65
|
-
});
|
|
66
|
-
}
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Transform PDF coordinates (bottom-left origin) to Polotno coordinates (top-left origin)
|
|
3
|
-
*/
|
|
4
|
-
export declare function pdfToPolotnoY(pdfY: number, elementHeight: number, pageHeight: number): number;
|
|
5
|
-
/**
|
|
6
|
-
* X coordinate remains the same between PDF and Polotno
|
|
7
|
-
*/
|
|
8
|
-
export declare function pdfToPolotnoX(pdfX: number): number;
|
|
9
|
-
/**
|
|
10
|
-
* Extract rotation angle from PDF transformation matrix
|
|
11
|
-
* PDF transformation matrix is [a, b, c, d, e, f]
|
|
12
|
-
* where rotation is encoded in a, b, c, d components
|
|
13
|
-
*/
|
|
14
|
-
export declare function extractRotation(transform: number[]): number;
|
|
15
|
-
/**
|
|
16
|
-
* Extract scale from PDF transformation matrix
|
|
17
|
-
*/
|
|
18
|
-
export declare function extractScale(transform: number[]): {
|
|
19
|
-
scaleX: number;
|
|
20
|
-
scaleY: number;
|
|
21
|
-
};
|
|
22
|
-
/**
|
|
23
|
-
* Extract position from PDF transformation matrix
|
|
24
|
-
* Returns position in PDF coordinates (will need to be converted to Polotno)
|
|
25
|
-
*/
|
|
26
|
-
export declare function extractPosition(transform: number[]): {
|
|
27
|
-
x: number;
|
|
28
|
-
y: number;
|
|
29
|
-
};
|
|
30
|
-
/**
|
|
31
|
-
* Calculate font size from PDF transformation matrix
|
|
32
|
-
* Font size is encoded in the scale component of the matrix
|
|
33
|
-
*/
|
|
34
|
-
export declare function calculateFontSize(transform: number[]): number;
|
|
35
|
-
/**
|
|
36
|
-
* Transform a complete bounding box from PDF to Polotno coordinates
|
|
37
|
-
*/
|
|
38
|
-
export declare function transformBoundingBox(pdfX: number, pdfY: number, width: number, height: number, pageHeight: number): {
|
|
39
|
-
x: number;
|
|
40
|
-
y: number;
|
|
41
|
-
width: number;
|
|
42
|
-
height: number;
|
|
43
|
-
};
|
|
44
|
-
/**
|
|
45
|
-
* Convert PDF color array [r, g, b] (0-1 range) to hex color string
|
|
46
|
-
*/
|
|
47
|
-
export declare function pdfColorToHex(color: number[]): string;
|
|
48
|
-
/**
|
|
49
|
-
* Apply unit conversion based on output unit and DPI
|
|
50
|
-
*/
|
|
51
|
-
export declare function convertUnits(valueInPx: number, targetUnit: 'px' | 'cm' | 'in', dpi?: number): number;
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Transform PDF coordinates (bottom-left origin) to Polotno coordinates (top-left origin)
|
|
3
|
-
*/
|
|
4
|
-
export function pdfToPolotnoY(pdfY, elementHeight, pageHeight) {
|
|
5
|
-
// PDF uses bottom-left origin, Polotno uses top-left
|
|
6
|
-
// PDFy is distance from bottom, we need distance from top
|
|
7
|
-
return pageHeight - pdfY - elementHeight;
|
|
8
|
-
}
|
|
9
|
-
/**
|
|
10
|
-
* X coordinate remains the same between PDF and Polotno
|
|
11
|
-
*/
|
|
12
|
-
export function pdfToPolotnoX(pdfX) {
|
|
13
|
-
return pdfX;
|
|
14
|
-
}
|
|
15
|
-
/**
|
|
16
|
-
* Extract rotation angle from PDF transformation matrix
|
|
17
|
-
* PDF transformation matrix is [a, b, c, d, e, f]
|
|
18
|
-
* where rotation is encoded in a, b, c, d components
|
|
19
|
-
*/
|
|
20
|
-
export function extractRotation(transform) {
|
|
21
|
-
const [a, b, c, d] = transform;
|
|
22
|
-
// Calculate rotation angle from matrix
|
|
23
|
-
// For a rotation matrix: a = cos(θ), b = sin(θ), c = -sin(θ), d = cos(θ)
|
|
24
|
-
const rotation = Math.atan2(b, a) * (180 / Math.PI);
|
|
25
|
-
return rotation;
|
|
26
|
-
}
|
|
27
|
-
/**
|
|
28
|
-
* Extract scale from PDF transformation matrix
|
|
29
|
-
*/
|
|
30
|
-
export function extractScale(transform) {
|
|
31
|
-
const [a, b, c, d] = transform;
|
|
32
|
-
// Scale is the magnitude of the transformation vectors
|
|
33
|
-
const scaleX = Math.sqrt(a * a + b * b);
|
|
34
|
-
const scaleY = Math.sqrt(c * c + d * d);
|
|
35
|
-
return { scaleX, scaleY };
|
|
36
|
-
}
|
|
37
|
-
/**
|
|
38
|
-
* Extract position from PDF transformation matrix
|
|
39
|
-
* Returns position in PDF coordinates (will need to be converted to Polotno)
|
|
40
|
-
*/
|
|
41
|
-
export function extractPosition(transform) {
|
|
42
|
-
return {
|
|
43
|
-
x: transform[4],
|
|
44
|
-
y: transform[5],
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
/**
|
|
48
|
-
* Calculate font size from PDF transformation matrix
|
|
49
|
-
* Font size is encoded in the scale component of the matrix
|
|
50
|
-
*/
|
|
51
|
-
export function calculateFontSize(transform) {
|
|
52
|
-
const { scaleY } = extractScale(transform);
|
|
53
|
-
return Math.abs(scaleY);
|
|
54
|
-
}
|
|
55
|
-
/**
|
|
56
|
-
* Transform a complete bounding box from PDF to Polotno coordinates
|
|
57
|
-
*/
|
|
58
|
-
export function transformBoundingBox(pdfX, pdfY, width, height, pageHeight) {
|
|
59
|
-
return {
|
|
60
|
-
x: pdfToPolotnoX(pdfX),
|
|
61
|
-
y: pdfToPolotnoY(pdfY, height, pageHeight),
|
|
62
|
-
width,
|
|
63
|
-
height,
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
/**
|
|
67
|
-
* Convert PDF color array [r, g, b] (0-1 range) to hex color string
|
|
68
|
-
*/
|
|
69
|
-
export function pdfColorToHex(color) {
|
|
70
|
-
if (!color || color.length < 3) {
|
|
71
|
-
return '#000000';
|
|
72
|
-
}
|
|
73
|
-
// PDF colors are in 0-1 range, convert to 0-255
|
|
74
|
-
const r = Math.round(color[0] * 255);
|
|
75
|
-
const g = Math.round(color[1] * 255);
|
|
76
|
-
const b = Math.round(color[2] * 255);
|
|
77
|
-
// Convert to hex
|
|
78
|
-
const hex = '#' + [r, g, b]
|
|
79
|
-
.map(x => x.toString(16).padStart(2, '0'))
|
|
80
|
-
.join('');
|
|
81
|
-
return hex;
|
|
82
|
-
}
|
|
83
|
-
/**
|
|
84
|
-
* Apply unit conversion based on output unit and DPI
|
|
85
|
-
*/
|
|
86
|
-
export function convertUnits(valueInPx, targetUnit, dpi = 72) {
|
|
87
|
-
if (targetUnit === 'px') {
|
|
88
|
-
return valueInPx;
|
|
89
|
-
}
|
|
90
|
-
// Convert pixels to points first (assuming 72 DPI as PDF standard)
|
|
91
|
-
const points = valueInPx;
|
|
92
|
-
if (targetUnit === 'in') {
|
|
93
|
-
return points / dpi;
|
|
94
|
-
}
|
|
95
|
-
if (targetUnit === 'cm') {
|
|
96
|
-
return (points / dpi) * 2.54;
|
|
97
|
-
}
|
|
98
|
-
return valueInPx;
|
|
99
|
-
}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import type { TextBlock, ImageBlock, PDFImageObject, PDFImportOptions } from './types.js';
|
|
2
|
-
/**
|
|
3
|
-
* Build Polotno text element from text block
|
|
4
|
-
*/
|
|
5
|
-
export declare function buildTextElement(block: TextBlock): any;
|
|
6
|
-
/**
|
|
7
|
-
* Build Polotno image element from image block
|
|
8
|
-
*/
|
|
9
|
-
export declare function buildImageElement(imageBlock: ImageBlock): any;
|
|
10
|
-
/**
|
|
11
|
-
* Convert PDF image to data URL
|
|
12
|
-
*/
|
|
13
|
-
export declare function imageToDataURL(buffer: Buffer, mimeType: string): Promise<string>;
|
|
14
|
-
/**
|
|
15
|
-
* Process PDF image and create image block
|
|
16
|
-
*/
|
|
17
|
-
export declare function processImage(pdfImage: PDFImageObject, pageHeight: number, options: PDFImportOptions): Promise<ImageBlock | null>;
|
|
18
|
-
/**
|
|
19
|
-
* Process all images from a PDF page
|
|
20
|
-
*/
|
|
21
|
-
export declare function processImages(pdfImages: PDFImageObject[], pageHeight: number, options: PDFImportOptions): Promise<any[]>;
|
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
import { pdfToPolotnoX, pdfToPolotnoY, extractRotation } from './coordinate-transform.js';
|
|
2
|
-
/**
|
|
3
|
-
* Generate a random ID for Polotno elements
|
|
4
|
-
*/
|
|
5
|
-
function randomId() {
|
|
6
|
-
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
|
7
|
-
let result = '';
|
|
8
|
-
for (let i = 0; i < 10; i++) {
|
|
9
|
-
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
10
|
-
}
|
|
11
|
-
return result;
|
|
12
|
-
}
|
|
13
|
-
/**
|
|
14
|
-
* Build Polotno text element from text block
|
|
15
|
-
*/
|
|
16
|
-
export function buildTextElement(block) {
|
|
17
|
-
return {
|
|
18
|
-
type: 'text',
|
|
19
|
-
id: randomId(),
|
|
20
|
-
name: '',
|
|
21
|
-
x: block.x,
|
|
22
|
-
y: block.y,
|
|
23
|
-
width: block.width,
|
|
24
|
-
height: block.height,
|
|
25
|
-
rotation: block.rotation || 0,
|
|
26
|
-
// Text content
|
|
27
|
-
text: block.text,
|
|
28
|
-
// Font properties
|
|
29
|
-
fontSize: block.fontSize,
|
|
30
|
-
fontFamily: block.fontName,
|
|
31
|
-
fontWeight: block.fontWeight,
|
|
32
|
-
fontStyle: block.fontStyle,
|
|
33
|
-
// Color and styling
|
|
34
|
-
fill: block.color,
|
|
35
|
-
stroke: 'black',
|
|
36
|
-
strokeWidth: 0,
|
|
37
|
-
// Alignment
|
|
38
|
-
align: block.align || 'left',
|
|
39
|
-
verticalAlign: 'top',
|
|
40
|
-
// Text properties
|
|
41
|
-
lineHeight: 1.2,
|
|
42
|
-
letterSpacing: 0,
|
|
43
|
-
// Visibility
|
|
44
|
-
opacity: 1,
|
|
45
|
-
visible: true,
|
|
46
|
-
selectable: true,
|
|
47
|
-
draggable: true,
|
|
48
|
-
resizable: true,
|
|
49
|
-
contentEditable: true,
|
|
50
|
-
removable: true,
|
|
51
|
-
// Background (disabled by default)
|
|
52
|
-
backgroundEnabled: false,
|
|
53
|
-
// No effects by default
|
|
54
|
-
shadowEnabled: false,
|
|
55
|
-
blurEnabled: false,
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
/**
|
|
59
|
-
* Build Polotno image element from image block
|
|
60
|
-
*/
|
|
61
|
-
export function buildImageElement(imageBlock) {
|
|
62
|
-
return {
|
|
63
|
-
type: 'image',
|
|
64
|
-
id: randomId(),
|
|
65
|
-
name: '',
|
|
66
|
-
x: imageBlock.x,
|
|
67
|
-
y: imageBlock.y,
|
|
68
|
-
width: imageBlock.width,
|
|
69
|
-
height: imageBlock.height,
|
|
70
|
-
rotation: imageBlock.rotation || 0,
|
|
71
|
-
// Image source
|
|
72
|
-
src: imageBlock.src,
|
|
73
|
-
// Cropping (no crop by default)
|
|
74
|
-
cropX: 0,
|
|
75
|
-
cropY: 0,
|
|
76
|
-
cropWidth: 1,
|
|
77
|
-
cropHeight: 1,
|
|
78
|
-
// Transformations
|
|
79
|
-
flipX: false,
|
|
80
|
-
flipY: false,
|
|
81
|
-
// Border
|
|
82
|
-
borderColor: 'black',
|
|
83
|
-
borderSize: 0,
|
|
84
|
-
cornerRadius: 0,
|
|
85
|
-
// Visibility
|
|
86
|
-
opacity: 1,
|
|
87
|
-
visible: true,
|
|
88
|
-
selectable: true,
|
|
89
|
-
draggable: true,
|
|
90
|
-
resizable: true,
|
|
91
|
-
removable: true,
|
|
92
|
-
// No effects by default
|
|
93
|
-
shadowEnabled: false,
|
|
94
|
-
blurEnabled: false,
|
|
95
|
-
filters: {},
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
/**
|
|
99
|
-
* Convert PDF image to data URL
|
|
100
|
-
*/
|
|
101
|
-
export async function imageToDataURL(buffer, mimeType) {
|
|
102
|
-
const base64 = buffer.toString('base64');
|
|
103
|
-
return `data:${mimeType};base64,${base64}`;
|
|
104
|
-
}
|
|
105
|
-
/**
|
|
106
|
-
* Process PDF image and create image block
|
|
107
|
-
*/
|
|
108
|
-
export async function processImage(pdfImage, pageHeight, options) {
|
|
109
|
-
try {
|
|
110
|
-
// Extract components from transform matrix
|
|
111
|
-
// Transform matrix: [a, b, c, d, e, f] where:
|
|
112
|
-
// - a,d are scale X,Y (or combined with rotation)
|
|
113
|
-
// - b,c are rotation/skew
|
|
114
|
-
// - e,f are translation X,Y
|
|
115
|
-
const [a, b, c, d, e, f] = pdfImage.transform;
|
|
116
|
-
// Calculate dimensions from scale components
|
|
117
|
-
const scaleX = Math.sqrt(a * a + b * b);
|
|
118
|
-
const scaleY = Math.sqrt(c * c + d * d);
|
|
119
|
-
const width = Math.abs(scaleX);
|
|
120
|
-
const height = Math.abs(scaleY);
|
|
121
|
-
// Extract rotation
|
|
122
|
-
const rotation = extractRotation(pdfImage.transform);
|
|
123
|
-
// Extract position (e,f are the translation components)
|
|
124
|
-
const x = pdfToPolotnoX(e);
|
|
125
|
-
// Check if Y is flipped (negative scaleY means coordinate system is already top-left)
|
|
126
|
-
const isYFlipped = d < 0;
|
|
127
|
-
const y = isYFlipped
|
|
128
|
-
? f // Already top-left origin, use directly
|
|
129
|
-
: pdfToPolotnoY(f, height, pageHeight); // Bottom-left origin, needs conversion
|
|
130
|
-
// Handle image data based on mode
|
|
131
|
-
let src;
|
|
132
|
-
if (options.imageMode === 'upload' && options.imageUploadFn) {
|
|
133
|
-
// Upload image and get URL
|
|
134
|
-
src = await options.imageUploadFn(pdfImage.buffer, pdfImage.mimeType);
|
|
135
|
-
}
|
|
136
|
-
else {
|
|
137
|
-
// Convert to data URL (default)
|
|
138
|
-
src = await imageToDataURL(pdfImage.buffer, pdfImage.mimeType);
|
|
139
|
-
}
|
|
140
|
-
return {
|
|
141
|
-
src,
|
|
142
|
-
x,
|
|
143
|
-
y,
|
|
144
|
-
width,
|
|
145
|
-
height,
|
|
146
|
-
rotation,
|
|
147
|
-
};
|
|
148
|
-
}
|
|
149
|
-
catch (error) {
|
|
150
|
-
console.warn('Failed to process image:', error);
|
|
151
|
-
return null;
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
/**
|
|
155
|
-
* Process all images from a PDF page
|
|
156
|
-
*/
|
|
157
|
-
export async function processImages(pdfImages, pageHeight, options) {
|
|
158
|
-
const imageBlocks = await Promise.all(pdfImages.map(img => processImage(img, pageHeight, options)));
|
|
159
|
-
// Filter out null results and build elements
|
|
160
|
-
return imageBlocks
|
|
161
|
-
.filter((block) => block !== null)
|
|
162
|
-
.map(block => buildImageElement(block));
|
|
163
|
-
}
|