polotno-node 2.15.18 → 3.0.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/README.md +15 -15
- package/dist/assets/html2canvas-CfrNTgDn.js +5 -0
- package/dist/assets/index-oYy3TsOk.js +464 -0
- package/dist/assets/{index.es-BZlJTB_J.js → index.es-C7BBG6KT.js} +5 -5
- package/dist/assets/jspdf.es.min-D1g0nHTs.js +79 -0
- package/dist/assets/lib-U0NRl5MI.js +65 -0
- package/dist/assets/purify.es-DlxR2Lg_.js +3 -0
- package/dist/index.html +1 -2
- package/font-cache.js +109 -0
- package/index.d.ts +7 -0
- package/index.js +2 -0
- package/instance.js +205 -223
- package/package.json +21 -17
- package/dist/assets/chunk-CilyBKbf.js +0 -1
- package/dist/assets/html2canvas-CmxtTAk0.js +0 -5
- package/dist/assets/index-dqgNRWnn.js +0 -344
- package/dist/assets/jspdf.es.min-CMl2XfPr.js +0 -79
- package/dist/assets/lib-Y0AZVm_8.js +0 -65
- package/dist/assets/purify.es-DjJUbhsB.js +0 -2
- package/video-parallel.js +0 -414
- /package/dist/assets/{typeof-GPCnPm50.js → typeof-B5XbjTb1.js} +0 -0
package/instance.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
const path = require(
|
|
2
|
-
const fs = require(
|
|
3
|
-
const os = require(
|
|
4
|
-
const crypto = require(
|
|
5
|
-
const { execSync } = require(
|
|
6
|
-
const { pathToFileURL } = require(
|
|
7
|
-
const { Readable } = require(
|
|
8
|
-
const { pipeline } = require(
|
|
9
|
-
const
|
|
1
|
+
const path = require("path");
|
|
2
|
+
const fs = require("fs");
|
|
3
|
+
const os = require("os");
|
|
4
|
+
const crypto = require("crypto");
|
|
5
|
+
const { execSync } = require("child_process");
|
|
6
|
+
const { pathToFileURL } = require("url");
|
|
7
|
+
const { Readable } = require("stream");
|
|
8
|
+
const { pipeline } = require("stream/promises");
|
|
9
|
+
const fontCache = require("./font-cache");
|
|
10
|
+
const DEFAULT_CLIENT = `file:${path.join(__dirname, "dist", "index.html")}`;
|
|
10
11
|
|
|
11
12
|
// Walk Polotno nodes recursively (depth-first)
|
|
12
13
|
// cb(node) may return true to stop traversing the current branch early
|
|
@@ -29,12 +30,12 @@ const collectMediaSourcePointers = (json) => {
|
|
|
29
30
|
// videos in elements tree
|
|
30
31
|
for (const page of json?.pages || []) {
|
|
31
32
|
for (const el of page?.children || []) {
|
|
32
|
-
if (el?.type ===
|
|
33
|
-
pointers.push({ kind:
|
|
33
|
+
if (el?.type === "video" && typeof el.src === "string") {
|
|
34
|
+
pointers.push({ kind: "video", target: el, key: "src" });
|
|
34
35
|
}
|
|
35
36
|
forEveryChild(el, (child) => {
|
|
36
|
-
if (child?.type ===
|
|
37
|
-
pointers.push({ kind:
|
|
37
|
+
if (child?.type === "video" && typeof child.src === "string") {
|
|
38
|
+
pointers.push({ kind: "video", target: child, key: "src" });
|
|
38
39
|
}
|
|
39
40
|
});
|
|
40
41
|
}
|
|
@@ -42,8 +43,8 @@ const collectMediaSourcePointers = (json) => {
|
|
|
42
43
|
|
|
43
44
|
// audios array
|
|
44
45
|
for (const audio of json?.audios || []) {
|
|
45
|
-
if (audio && typeof audio.src ===
|
|
46
|
-
pointers.push({ kind:
|
|
46
|
+
if (audio && typeof audio.src === "string") {
|
|
47
|
+
pointers.push({ kind: "audio", target: audio, key: "src" });
|
|
47
48
|
}
|
|
48
49
|
}
|
|
49
50
|
|
|
@@ -51,45 +52,45 @@ const collectMediaSourcePointers = (json) => {
|
|
|
51
52
|
};
|
|
52
53
|
|
|
53
54
|
const isDataOrFileSrc = (src) => {
|
|
54
|
-
if (typeof src !==
|
|
55
|
+
if (typeof src !== "string") return true;
|
|
55
56
|
return (
|
|
56
|
-
src.startsWith(
|
|
57
|
-
src.startsWith(
|
|
58
|
-
src.startsWith(
|
|
57
|
+
src.startsWith("data:") ||
|
|
58
|
+
src.startsWith("file:") ||
|
|
59
|
+
src.startsWith("blob:")
|
|
59
60
|
);
|
|
60
61
|
};
|
|
61
62
|
|
|
62
63
|
const guessExtension = (urlString, contentType) => {
|
|
63
64
|
try {
|
|
64
65
|
const u = new URL(urlString);
|
|
65
|
-
const ext = path.extname(u.pathname ||
|
|
66
|
+
const ext = path.extname(u.pathname || "");
|
|
66
67
|
if (ext) return ext;
|
|
67
68
|
} catch (e) {
|
|
68
69
|
// ignore; fallback to content-type
|
|
69
70
|
}
|
|
70
71
|
|
|
71
|
-
const ct = String(contentType ||
|
|
72
|
-
.split(
|
|
72
|
+
const ct = String(contentType || "")
|
|
73
|
+
.split(";")[0]
|
|
73
74
|
.trim()
|
|
74
75
|
.toLowerCase();
|
|
75
76
|
const map = {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
77
|
+
"video/mp4": ".mp4",
|
|
78
|
+
"video/webm": ".webm",
|
|
79
|
+
"video/quicktime": ".mov",
|
|
80
|
+
"audio/mpeg": ".mp3",
|
|
81
|
+
"audio/mp4": ".m4a",
|
|
82
|
+
"audio/wav": ".wav",
|
|
83
|
+
"audio/x-wav": ".wav",
|
|
84
|
+
"audio/ogg": ".ogg",
|
|
85
|
+
"audio/webm": ".webm",
|
|
85
86
|
};
|
|
86
|
-
return map[ct] ||
|
|
87
|
+
return map[ct] || ".bin";
|
|
87
88
|
};
|
|
88
89
|
|
|
89
90
|
const downloadUrlToFile = async (urlString, destinationPath) => {
|
|
90
|
-
if (typeof fetch !==
|
|
91
|
+
if (typeof fetch !== "function") {
|
|
91
92
|
throw new Error(
|
|
92
|
-
|
|
93
|
+
"Global fetch API is not available in this Node.js runtime. Please use Node 18+.",
|
|
93
94
|
);
|
|
94
95
|
}
|
|
95
96
|
const MAX_ATTEMPTS = 3;
|
|
@@ -104,11 +105,11 @@ const downloadUrlToFile = async (urlString, destinationPath) => {
|
|
|
104
105
|
const res = await fetch(urlString);
|
|
105
106
|
if (!res.ok) {
|
|
106
107
|
// Best-effort read of error body (small) for debugging.
|
|
107
|
-
let bodyPreview =
|
|
108
|
+
let bodyPreview = "";
|
|
108
109
|
try {
|
|
109
110
|
bodyPreview = await res.text();
|
|
110
111
|
if (bodyPreview.length > 500) {
|
|
111
|
-
bodyPreview = bodyPreview.slice(0, 500) +
|
|
112
|
+
bodyPreview = bodyPreview.slice(0, 500) + "...";
|
|
112
113
|
}
|
|
113
114
|
} catch (e) {
|
|
114
115
|
// ignore
|
|
@@ -118,12 +119,12 @@ const downloadUrlToFile = async (urlString, destinationPath) => {
|
|
|
118
119
|
bodyPreview ? `body: ${JSON.stringify(bodyPreview)}` : null,
|
|
119
120
|
]
|
|
120
121
|
.filter(Boolean)
|
|
121
|
-
.join(
|
|
122
|
+
.join(", ");
|
|
122
123
|
|
|
123
124
|
throw new Error(`HTTP error while downloading: ${details}`);
|
|
124
125
|
}
|
|
125
126
|
if (!res.body) {
|
|
126
|
-
throw new Error(
|
|
127
|
+
throw new Error("Empty response body");
|
|
127
128
|
}
|
|
128
129
|
|
|
129
130
|
await pipeline(
|
|
@@ -131,7 +132,7 @@ const downloadUrlToFile = async (urlString, destinationPath) => {
|
|
|
131
132
|
fs.createWriteStream(destinationPath),
|
|
132
133
|
);
|
|
133
134
|
|
|
134
|
-
return res.headers.get(
|
|
135
|
+
return res.headers.get("content-type") || "";
|
|
135
136
|
} catch (e) {
|
|
136
137
|
lastError = e;
|
|
137
138
|
if (attempt < MAX_ATTEMPTS) {
|
|
@@ -155,19 +156,19 @@ const getVideoNormalizationReason = (filePath) => {
|
|
|
155
156
|
try {
|
|
156
157
|
const raw = execSync(
|
|
157
158
|
`ffprobe -v error -select_streams v:0 -show_entries stream=codec_name,codec_tag_string,pix_fmt -of json "${filePath}"`,
|
|
158
|
-
{ encoding:
|
|
159
|
+
{ encoding: "utf8" },
|
|
159
160
|
);
|
|
160
161
|
const info = JSON.parse(raw);
|
|
161
162
|
const vs = info?.streams?.[0];
|
|
162
163
|
if (!vs) return null;
|
|
163
164
|
|
|
164
|
-
const codec = (vs.codec_name ||
|
|
165
|
-
const tag = (vs.codec_tag_string ||
|
|
166
|
-
if ([
|
|
167
|
-
return
|
|
165
|
+
const codec = (vs.codec_name || "").toLowerCase();
|
|
166
|
+
const tag = (vs.codec_tag_string || "").toLowerCase();
|
|
167
|
+
if (["hevc", "h265"].includes(codec) || ["hvc1", "hev1"].includes(tag)) {
|
|
168
|
+
return "HEVC/H.265 is not supported by Chromium headless";
|
|
168
169
|
}
|
|
169
|
-
const pix = (vs.pix_fmt ||
|
|
170
|
-
if (pix && pix !==
|
|
170
|
+
const pix = (vs.pix_fmt || "").toLowerCase();
|
|
171
|
+
if (pix && pix !== "yuv420p") {
|
|
171
172
|
return `pixel format "${pix}" is not the preferred yuv420p`;
|
|
172
173
|
}
|
|
173
174
|
return null;
|
|
@@ -178,7 +179,7 @@ const getVideoNormalizationReason = (filePath) => {
|
|
|
178
179
|
|
|
179
180
|
const isFfmpegAvailable = () => {
|
|
180
181
|
try {
|
|
181
|
-
execSync(
|
|
182
|
+
execSync("ffmpeg -version", { stdio: "ignore" });
|
|
182
183
|
return true;
|
|
183
184
|
} catch {
|
|
184
185
|
return false;
|
|
@@ -189,7 +190,7 @@ const getAudioCodec = (filePath) => {
|
|
|
189
190
|
try {
|
|
190
191
|
const result = execSync(
|
|
191
192
|
`ffprobe -v error -select_streams a:0 -show_entries stream=codec_name -of csv=p=0 "${filePath}"`,
|
|
192
|
-
{ encoding:
|
|
193
|
+
{ encoding: "utf8" },
|
|
193
194
|
);
|
|
194
195
|
return result.trim();
|
|
195
196
|
} catch {
|
|
@@ -199,7 +200,7 @@ const getAudioCodec = (filePath) => {
|
|
|
199
200
|
|
|
200
201
|
const convertAudioToAac = (inputPath, outputPath) => {
|
|
201
202
|
execSync(`ffmpeg -y -i "${inputPath}" -c:v copy -c:a aac "${outputPath}"`, {
|
|
202
|
-
stdio:
|
|
203
|
+
stdio: "ignore",
|
|
203
204
|
});
|
|
204
205
|
};
|
|
205
206
|
|
|
@@ -216,7 +217,7 @@ const prepareLocalMediaForVideoExport = async (json) => {
|
|
|
216
217
|
if (isDataOrFileSrc(src)) continue;
|
|
217
218
|
try {
|
|
218
219
|
const u = new URL(src);
|
|
219
|
-
if (u.protocol !==
|
|
220
|
+
if (u.protocol !== "http:" && u.protocol !== "https:") continue;
|
|
220
221
|
} catch (e) {
|
|
221
222
|
continue;
|
|
222
223
|
}
|
|
@@ -224,7 +225,7 @@ const prepareLocalMediaForVideoExport = async (json) => {
|
|
|
224
225
|
rewriteMap.set(src, null);
|
|
225
226
|
urlsToDownload.push(src);
|
|
226
227
|
}
|
|
227
|
-
if (p.kind ===
|
|
228
|
+
if (p.kind === "video") videoUrls.add(src);
|
|
228
229
|
}
|
|
229
230
|
|
|
230
231
|
if (urlsToDownload.length === 0) {
|
|
@@ -232,7 +233,7 @@ const prepareLocalMediaForVideoExport = async (json) => {
|
|
|
232
233
|
}
|
|
233
234
|
|
|
234
235
|
const tempDir = await fs.promises.mkdtemp(
|
|
235
|
-
path.join(os.tmpdir(),
|
|
236
|
+
path.join(os.tmpdir(), "polotno-node-assets-"),
|
|
236
237
|
);
|
|
237
238
|
|
|
238
239
|
let didCleanup = false;
|
|
@@ -247,7 +248,7 @@ const prepareLocalMediaForVideoExport = async (json) => {
|
|
|
247
248
|
urlsToDownload.map(async (src) => {
|
|
248
249
|
const tmpBase = crypto.randomUUID
|
|
249
250
|
? crypto.randomUUID()
|
|
250
|
-
: crypto.randomBytes(16).toString(
|
|
251
|
+
: crypto.randomBytes(16).toString("hex");
|
|
251
252
|
const tmpPath = path.join(tempDir, tmpBase);
|
|
252
253
|
const contentType = await downloadUrlToFile(src, tmpPath);
|
|
253
254
|
const ext = guessExtension(src, contentType);
|
|
@@ -257,10 +258,10 @@ const prepareLocalMediaForVideoExport = async (json) => {
|
|
|
257
258
|
if (videoUrls.has(src)) {
|
|
258
259
|
const reason = getVideoNormalizationReason(finalPath);
|
|
259
260
|
if (reason) {
|
|
260
|
-
const normalizedPath = finalPath +
|
|
261
|
+
const normalizedPath = finalPath + ".normalized.mp4";
|
|
261
262
|
execSync(
|
|
262
263
|
`ffmpeg -y -i "${finalPath}" -c:v libx264 -pix_fmt yuv420p -c:a aac -movflags +faststart "${normalizedPath}"`,
|
|
263
|
-
{ stdio:
|
|
264
|
+
{ stdio: "ignore" },
|
|
264
265
|
);
|
|
265
266
|
finalPath = normalizedPath;
|
|
266
267
|
}
|
|
@@ -285,13 +286,30 @@ const prepareLocalMediaForVideoExport = async (json) => {
|
|
|
285
286
|
}
|
|
286
287
|
};
|
|
287
288
|
|
|
288
|
-
module.exports.createPage = async (
|
|
289
|
+
module.exports.createPage = async (
|
|
290
|
+
browser,
|
|
291
|
+
url,
|
|
292
|
+
requestInterceptor,
|
|
293
|
+
useFontCache = true,
|
|
294
|
+
) => {
|
|
289
295
|
const page = await browser.newPage();
|
|
290
296
|
|
|
291
297
|
// Attach request interceptor if provided
|
|
292
|
-
if (typeof requestInterceptor ===
|
|
298
|
+
if (useFontCache || typeof requestInterceptor === "function") {
|
|
293
299
|
await page.setRequestInterception(true);
|
|
294
|
-
page.on(
|
|
300
|
+
page.on("request", (request) => {
|
|
301
|
+
if (useFontCache && fontCache.tryRespond(request)) {
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
if (typeof requestInterceptor === "function") {
|
|
305
|
+
requestInterceptor(request);
|
|
306
|
+
} else {
|
|
307
|
+
request.continue().catch(() => {});
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
if (useFontCache) {
|
|
311
|
+
page.on("response", (response) => fontCache.capture(response));
|
|
312
|
+
}
|
|
295
313
|
}
|
|
296
314
|
|
|
297
315
|
// we need to make sure that headless is inside user agent
|
|
@@ -299,32 +317,32 @@ module.exports.createPage = async (browser, url, requestInterceptor) => {
|
|
|
299
317
|
// but also lets NOT just overwrite default one, because Google Fonts may send "reduced" version of font
|
|
300
318
|
// so some languages may not work
|
|
301
319
|
const userAgent = await page.evaluate(() => navigator.userAgent);
|
|
302
|
-
if (userAgent.indexOf(
|
|
303
|
-
await page.setUserAgent(userAgent +
|
|
320
|
+
if (userAgent.indexOf("Headless") === -1) {
|
|
321
|
+
await page.setUserAgent(userAgent + " Chrome Headless");
|
|
304
322
|
}
|
|
305
323
|
|
|
306
|
-
page.on(
|
|
324
|
+
page.on("console", (msg) => {
|
|
307
325
|
msg.args().forEach(async (message) => {
|
|
308
326
|
// skip style information
|
|
309
|
-
if (message.toString().indexOf(
|
|
327
|
+
if (message.toString().indexOf("margin") >= 0) {
|
|
310
328
|
return;
|
|
311
329
|
}
|
|
312
330
|
|
|
313
331
|
const { subtype, description } = message.remoteObject();
|
|
314
332
|
|
|
315
|
-
if (subtype ===
|
|
333
|
+
if (subtype === "error") {
|
|
316
334
|
console.error(description);
|
|
317
335
|
return;
|
|
318
336
|
}
|
|
319
337
|
|
|
320
338
|
const text = message
|
|
321
339
|
.toString()
|
|
322
|
-
.replace(
|
|
323
|
-
.replace(
|
|
340
|
+
.replace("JSHandle:", "")
|
|
341
|
+
.replace("%c", "");
|
|
324
342
|
console.log(text);
|
|
325
343
|
});
|
|
326
344
|
});
|
|
327
|
-
page.on(
|
|
345
|
+
page.on("pageerror", (msg) => {
|
|
328
346
|
console.error(msg);
|
|
329
347
|
});
|
|
330
348
|
await page.goto(url);
|
|
@@ -335,42 +353,81 @@ module.exports.run = async (page, func, args) => {
|
|
|
335
353
|
return await page.evaluate(func, ...args);
|
|
336
354
|
};
|
|
337
355
|
|
|
356
|
+
// In-page export recipes. These run inside the browser (via page.evaluate / run),
|
|
357
|
+
// so they may only reference page globals (`store`, `window`) — not module scope.
|
|
358
|
+
// Defining them once lets the instance methods and the module-level page-taking
|
|
359
|
+
// exports share a single recipe per export format.
|
|
360
|
+
const recipeToDataURL = async (json, attrs) => {
|
|
361
|
+
const pixelRatio = attrs.pixelRatio || 1;
|
|
362
|
+
store.loadJSON(json);
|
|
363
|
+
store.setElementsPixelRatio(pixelRatio);
|
|
364
|
+
await store.waitLoading();
|
|
365
|
+
return store.toDataURL({ ...attrs, pixelRatio });
|
|
366
|
+
};
|
|
367
|
+
const recipeToPDFDataURL = async (json, attrs) => {
|
|
368
|
+
store.loadJSON(json);
|
|
369
|
+
// Honor an `onProgress` callback exposed by the instance `run()`. When it is
|
|
370
|
+
// not exposed (e.g. the module-level page-taking export) this is a no-op.
|
|
371
|
+
if (window.onProgress) {
|
|
372
|
+
attrs.onProgress = (progress) => {
|
|
373
|
+
window.onProgress(progress);
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
await store.waitLoading();
|
|
377
|
+
return await store.toPDFDataURL(attrs);
|
|
378
|
+
};
|
|
379
|
+
const recipeToBlob = async (json, attrs) => {
|
|
380
|
+
store.loadJSON(json);
|
|
381
|
+
await store.waitLoading();
|
|
382
|
+
return await store.toBlob(attrs);
|
|
383
|
+
};
|
|
384
|
+
const recipeToGIFDataURL = async (json, attrs) => {
|
|
385
|
+
store.loadJSON(json);
|
|
386
|
+
return await store.toGIFDataURL(attrs);
|
|
387
|
+
};
|
|
388
|
+
|
|
338
389
|
module.exports.jsonToDataURL = async (page, json, attrs) => {
|
|
339
|
-
return await page.evaluate(
|
|
340
|
-
async (json, attrs) => {
|
|
341
|
-
const pixelRatio = attrs.pixelRatio || 1;
|
|
342
|
-
store.loadJSON(json);
|
|
343
|
-
store.setElementsPixelRatio(pixelRatio);
|
|
344
|
-
await store.waitLoading();
|
|
345
|
-
return store.toDataURL({ ...attrs, pixelRatio });
|
|
346
|
-
},
|
|
347
|
-
json,
|
|
348
|
-
attrs || {},
|
|
349
|
-
);
|
|
390
|
+
return await page.evaluate(recipeToDataURL, json, attrs || {});
|
|
350
391
|
};
|
|
351
392
|
|
|
352
393
|
module.exports.jsonToPDFDataURL = async (page, json, attrs) => {
|
|
353
|
-
return page.evaluate(
|
|
354
|
-
async (json, attrs) => {
|
|
355
|
-
store.loadJSON(json);
|
|
356
|
-
await store.waitLoading();
|
|
357
|
-
return await store.toPDFDataURL(attrs);
|
|
358
|
-
},
|
|
359
|
-
json,
|
|
360
|
-
attrs || {},
|
|
361
|
-
);
|
|
394
|
+
return page.evaluate(recipeToPDFDataURL, json, attrs || {});
|
|
362
395
|
};
|
|
363
396
|
|
|
364
397
|
module.exports.jsonToBlob = async (page, json, attrs) => {
|
|
365
|
-
return page.evaluate(
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
398
|
+
return page.evaluate(recipeToBlob, json, attrs || {});
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
// Render options that map directly to a `window.config.*` setter on the client.
|
|
402
|
+
// `arg: "value"` passes the option value through; `arg: true` calls the setter
|
|
403
|
+
// with a literal `true` (the option is only a boolean flag).
|
|
404
|
+
const CONFIG_OPTION_SETTERS = [
|
|
405
|
+
{ option: "assetLoadTimeout", method: "setAssetLoadTimeout", arg: "value" },
|
|
406
|
+
{ option: "fontLoadTimeout", method: "setFontLoadTimeout", arg: "value" },
|
|
407
|
+
{ option: "legacyRichTextEnabled", method: "setLegacyRichTextEnabled", arg: true },
|
|
408
|
+
{ option: "textVerticalResizeEnabled", method: "setTextVerticalResizeEnabled", arg: true },
|
|
409
|
+
{ option: "textSplitAllowed", method: "unstable_setTextSplitAllowed", arg: true },
|
|
410
|
+
{ option: "textOverflow", method: "setTextOverflow", arg: "value" },
|
|
411
|
+
];
|
|
412
|
+
|
|
413
|
+
// Apply the option→config setters above to a page in a single round-trip.
|
|
414
|
+
const applyConfigOptions = async (page, attrs = {}) => {
|
|
415
|
+
const calls = [];
|
|
416
|
+
for (const { option, method, arg } of CONFIG_OPTION_SETTERS) {
|
|
417
|
+
if (attrs[option]) {
|
|
418
|
+
calls.push({ method, value: arg === "value" ? attrs[option] : arg });
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
if (calls.length === 0) return;
|
|
422
|
+
await page.evaluate((calls) => {
|
|
423
|
+
for (const { method, value } of calls) {
|
|
424
|
+
if (window.config && window.config[method]) {
|
|
425
|
+
window.config[method](value);
|
|
426
|
+
} else {
|
|
427
|
+
console.error(`${method} function is not defined in the client.`);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}, calls);
|
|
374
431
|
};
|
|
375
432
|
|
|
376
433
|
const busyPages = [];
|
|
@@ -383,12 +440,18 @@ module.exports.createInstance = async ({
|
|
|
383
440
|
useParallelPages = true,
|
|
384
441
|
browser,
|
|
385
442
|
requestInterceptor,
|
|
443
|
+
useFontCache = true,
|
|
386
444
|
} = {}) => {
|
|
387
445
|
const visitPage = url || `${DEFAULT_CLIENT}?key=${key}`;
|
|
388
446
|
const hasCustomClientUrl = Boolean(url);
|
|
389
447
|
const firstPage = useParallelPages
|
|
390
448
|
? null
|
|
391
|
-
: await module.exports.createPage(
|
|
449
|
+
: await module.exports.createPage(
|
|
450
|
+
browser,
|
|
451
|
+
visitPage,
|
|
452
|
+
requestInterceptor,
|
|
453
|
+
useFontCache,
|
|
454
|
+
);
|
|
392
455
|
|
|
393
456
|
async function ensureAssetErrorHelpers(page) {
|
|
394
457
|
await page.evaluate(() => {
|
|
@@ -403,10 +466,10 @@ module.exports.createInstance = async ({
|
|
|
403
466
|
window._polotnoError = null;
|
|
404
467
|
|
|
405
468
|
const message = String(error);
|
|
406
|
-
const isFontError = message.indexOf(
|
|
469
|
+
const isFontError = message.indexOf("Timeout for loading font") >= 0;
|
|
407
470
|
const skipFontError = isFontError && attrs && attrs.skipFontError;
|
|
408
471
|
|
|
409
|
-
const isImageError = message.indexOf(
|
|
472
|
+
const isImageError = message.indexOf("image ") >= 0;
|
|
410
473
|
const skipImageError = isImageError && attrs && attrs.skipImageError;
|
|
411
474
|
|
|
412
475
|
if (skipFontError || skipImageError) {
|
|
@@ -443,12 +506,17 @@ module.exports.createInstance = async ({
|
|
|
443
506
|
|
|
444
507
|
const run = async (func, ...args) => {
|
|
445
508
|
const page = useParallelPages
|
|
446
|
-
? await module.exports.createPage(
|
|
509
|
+
? await module.exports.createPage(
|
|
510
|
+
browser,
|
|
511
|
+
visitPage,
|
|
512
|
+
requestInterceptor,
|
|
513
|
+
useFontCache,
|
|
514
|
+
)
|
|
447
515
|
: firstPage;
|
|
448
516
|
|
|
449
517
|
if (busyPages.indexOf(page) >= 0) {
|
|
450
518
|
throw new Error(
|
|
451
|
-
|
|
519
|
+
"Current rendering context is busy with another task. Please use `useParallelPages: true` option to run multiple tasks in parallel or make sure previous task is finished before starting a new one.",
|
|
452
520
|
);
|
|
453
521
|
}
|
|
454
522
|
|
|
@@ -459,69 +527,19 @@ module.exports.createInstance = async ({
|
|
|
459
527
|
const profilePath = args[1]?.profilePath;
|
|
460
528
|
if (profilePath) {
|
|
461
529
|
cdpSession = await page.target().createCDPSession();
|
|
462
|
-
await cdpSession.send(
|
|
463
|
-
await cdpSession.send(
|
|
530
|
+
await cdpSession.send("Profiler.enable");
|
|
531
|
+
await cdpSession.send("Profiler.start");
|
|
464
532
|
}
|
|
465
533
|
|
|
466
534
|
try {
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
console.error(
|
|
473
|
-
'setAssetLoadTimeout function is not defined in the client.',
|
|
474
|
-
);
|
|
475
|
-
}
|
|
476
|
-
}, args[1].assetLoadTimeout);
|
|
477
|
-
}
|
|
478
|
-
if (args[1]?.fontLoadTimeout) {
|
|
479
|
-
await page.evaluate((timeout) => {
|
|
480
|
-
if (window.config && window.config.setFontLoadTimeout) {
|
|
481
|
-
window.config.setFontLoadTimeout(timeout);
|
|
482
|
-
} else {
|
|
483
|
-
console.error(
|
|
484
|
-
'setFontLoadTimeout function is not defined in the client.',
|
|
485
|
-
);
|
|
486
|
-
}
|
|
487
|
-
}, args[1].fontLoadTimeout);
|
|
488
|
-
}
|
|
489
|
-
if (args[1]?.htmlTextRenderEnabled || args[1]?.richTextEnabled) {
|
|
490
|
-
await page.evaluate(() => {
|
|
491
|
-
if (window.config && window.config.setRichTextEnabled) {
|
|
492
|
-
window.config.setRichTextEnabled(true);
|
|
493
|
-
} else {
|
|
494
|
-
console.error(
|
|
495
|
-
'setRichTextEnabled function is not defined in the client.',
|
|
496
|
-
);
|
|
497
|
-
}
|
|
498
|
-
});
|
|
499
|
-
}
|
|
500
|
-
if (args[1]?.textVerticalResizeEnabled) {
|
|
501
|
-
await page.evaluate(() => {
|
|
502
|
-
if (window.config && window.config.setTextVerticalResizeEnabled) {
|
|
503
|
-
window.config.setTextVerticalResizeEnabled(true);
|
|
504
|
-
} else {
|
|
505
|
-
console.error(
|
|
506
|
-
'setTextVerticalResizeEnabled function is not defined in the client.',
|
|
507
|
-
);
|
|
508
|
-
}
|
|
509
|
-
});
|
|
510
|
-
}
|
|
511
|
-
if (args[1]?.textSplitAllowed) {
|
|
512
|
-
await page.evaluate(() => {
|
|
513
|
-
if (window.config && window.config.unstable_setTextSplitAllowed) {
|
|
514
|
-
window.config.unstable_setTextSplitAllowed(true);
|
|
515
|
-
} else {
|
|
516
|
-
console.error(
|
|
517
|
-
'unstable_setTextSplitAllowed function is not defined in the client.',
|
|
518
|
-
);
|
|
519
|
-
}
|
|
520
|
-
});
|
|
521
|
-
}
|
|
535
|
+
// `htmlTextRenderEnabled`, `richTextEnabled` and `renderTagTextEnabled` options
|
|
536
|
+
// are no-op since polotno 4 - the new text rendering engine is always on.
|
|
537
|
+
// They are still accepted for backwards compatibility.
|
|
538
|
+
// All direct `window.config.*` setters are applied from one table.
|
|
539
|
+
await applyConfigOptions(page, args[1]);
|
|
522
540
|
if (args[1]?.onProgress) {
|
|
523
541
|
const originalProgress = args[1].onProgress;
|
|
524
|
-
await page.exposeFunction(
|
|
542
|
+
await page.exposeFunction("onProgress", async (progress, frameTime) => {
|
|
525
543
|
originalProgress(progress, frameTime);
|
|
526
544
|
});
|
|
527
545
|
}
|
|
@@ -531,17 +549,6 @@ module.exports.createInstance = async ({
|
|
|
531
549
|
await page.exposeFunction(name, fn);
|
|
532
550
|
}
|
|
533
551
|
}
|
|
534
|
-
if (args[1]?.textOverflow) {
|
|
535
|
-
await page.evaluate((overflow) => {
|
|
536
|
-
if (window.config && window.config.setTextOverflow) {
|
|
537
|
-
window.config.setTextOverflow(overflow);
|
|
538
|
-
} else {
|
|
539
|
-
console.error(
|
|
540
|
-
'setTextOverflow function is not defined in the client.',
|
|
541
|
-
);
|
|
542
|
-
}
|
|
543
|
-
}, args[1].textOverflow);
|
|
544
|
-
}
|
|
545
552
|
await page.evaluate(() => {
|
|
546
553
|
if (window.config?.onLoadError) {
|
|
547
554
|
window.config.onLoadError((error) => {
|
|
@@ -549,7 +556,7 @@ module.exports.createInstance = async ({
|
|
|
549
556
|
});
|
|
550
557
|
} else {
|
|
551
558
|
console.error(
|
|
552
|
-
|
|
559
|
+
"onLoadError function is not defined in the client. Error handling will not work.",
|
|
553
560
|
);
|
|
554
561
|
}
|
|
555
562
|
});
|
|
@@ -558,7 +565,7 @@ module.exports.createInstance = async ({
|
|
|
558
565
|
|
|
559
566
|
// Stop profiler and save profile if enabled
|
|
560
567
|
if (cdpSession && profilePath) {
|
|
561
|
-
const { profile } = await cdpSession.send(
|
|
568
|
+
const { profile } = await cdpSession.send("Profiler.stop");
|
|
562
569
|
fs.writeFileSync(profilePath, JSON.stringify(profile));
|
|
563
570
|
console.log(`CPU profile saved to: ${profilePath}`);
|
|
564
571
|
}
|
|
@@ -579,13 +586,13 @@ module.exports.createInstance = async ({
|
|
|
579
586
|
} catch (e) {
|
|
580
587
|
// Stop profiler and save profile even on error
|
|
581
588
|
if (cdpSession && profilePath) {
|
|
582
|
-
console.log(
|
|
589
|
+
console.log("saving profile");
|
|
583
590
|
try {
|
|
584
|
-
const { profile } = await cdpSession.send(
|
|
591
|
+
const { profile } = await cdpSession.send("Profiler.stop");
|
|
585
592
|
fs.writeFileSync(profilePath, JSON.stringify(profile));
|
|
586
593
|
console.log(`CPU profile saved to: ${profilePath}`);
|
|
587
594
|
} catch (profileError) {
|
|
588
|
-
console.error(
|
|
595
|
+
console.error("Failed to save profile:", profileError.message);
|
|
589
596
|
}
|
|
590
597
|
}
|
|
591
598
|
|
|
@@ -608,65 +615,35 @@ module.exports.createInstance = async ({
|
|
|
608
615
|
};
|
|
609
616
|
|
|
610
617
|
const jsonToDataURL = async (json, attrs) => {
|
|
611
|
-
return await run(
|
|
612
|
-
async (json, attrs) => {
|
|
613
|
-
const pixelRatio = attrs.pixelRatio || 1;
|
|
614
|
-
store.loadJSON(json);
|
|
615
|
-
store.setElementsPixelRatio(pixelRatio);
|
|
616
|
-
await store.waitLoading();
|
|
617
|
-
return store.toDataURL({ ...attrs, pixelRatio });
|
|
618
|
-
},
|
|
619
|
-
json,
|
|
620
|
-
attrs || {},
|
|
621
|
-
);
|
|
618
|
+
return await run(recipeToDataURL, json, attrs || {});
|
|
622
619
|
};
|
|
623
620
|
|
|
624
621
|
const jsonToGIFDataURL = async (json, attrs) => {
|
|
625
|
-
return await run(
|
|
626
|
-
async (json, attrs) => {
|
|
627
|
-
store.loadJSON(json);
|
|
628
|
-
return await store.toGIFDataURL(attrs);
|
|
629
|
-
},
|
|
630
|
-
json,
|
|
631
|
-
attrs || {},
|
|
632
|
-
);
|
|
622
|
+
return await run(recipeToGIFDataURL, json, attrs || {});
|
|
633
623
|
};
|
|
634
624
|
|
|
635
625
|
const jsonToGIFBase64 = async (json, attrs) => {
|
|
636
626
|
const url = await jsonToGIFDataURL(json, attrs);
|
|
637
|
-
return url.split(
|
|
627
|
+
return url.split("base64,")[1];
|
|
638
628
|
};
|
|
639
629
|
|
|
640
630
|
const jsonToImageBase64 = async (json, attrs) => {
|
|
641
631
|
const url = await jsonToDataURL(json, attrs);
|
|
642
|
-
return url.split(
|
|
632
|
+
return url.split("base64,")[1];
|
|
643
633
|
};
|
|
644
634
|
|
|
645
635
|
const jsonToPDFDataURL = async (json, attrs) => {
|
|
646
|
-
return await run(
|
|
647
|
-
async (json, attrs) => {
|
|
648
|
-
store.loadJSON(json);
|
|
649
|
-
if (window.onProgress) {
|
|
650
|
-
attrs.onProgress = (progress) => {
|
|
651
|
-
window.onProgress(progress);
|
|
652
|
-
};
|
|
653
|
-
}
|
|
654
|
-
await store.waitLoading();
|
|
655
|
-
return await store.toPDFDataURL(attrs);
|
|
656
|
-
},
|
|
657
|
-
json,
|
|
658
|
-
attrs || {},
|
|
659
|
-
);
|
|
636
|
+
return await run(recipeToPDFDataURL, json, attrs || {});
|
|
660
637
|
};
|
|
661
638
|
|
|
662
639
|
const jsonToPDFBase64 = async (json, attrs) => {
|
|
663
640
|
const url = await jsonToPDFDataURL(json, attrs);
|
|
664
|
-
return url.split(
|
|
641
|
+
return url.split("base64,")[1];
|
|
665
642
|
};
|
|
666
643
|
|
|
667
644
|
const jsonToBlob = async (json, attrs) => {
|
|
668
645
|
const base64 = await jsonToImageBase64(json, attrs);
|
|
669
|
-
const blob = Buffer.from(base64,
|
|
646
|
+
const blob = Buffer.from(base64, "base64");
|
|
670
647
|
return blob;
|
|
671
648
|
};
|
|
672
649
|
|
|
@@ -705,11 +682,11 @@ module.exports.createInstance = async ({
|
|
|
705
682
|
|
|
706
683
|
// For video export, we always use 'resize' text overflow mode
|
|
707
684
|
// to ensure text fits properly in animations. User preferences are ignored for now.
|
|
708
|
-
window.config.setTextOverflow(
|
|
685
|
+
window.config.setTextOverflow("resize");
|
|
709
686
|
|
|
710
687
|
if (!window.__polotnoLoadVideoExport) {
|
|
711
688
|
throw new Error(
|
|
712
|
-
|
|
689
|
+
"Video export module loader is not defined in the client. Expected window.__polotnoLoadVideoExport().",
|
|
713
690
|
);
|
|
714
691
|
}
|
|
715
692
|
|
|
@@ -772,7 +749,7 @@ module.exports.createInstance = async ({
|
|
|
772
749
|
) {
|
|
773
750
|
const end = Math.min(offset + CHUNK_SIZE, uint8Array.length);
|
|
774
751
|
const chunk = uint8Array.slice(offset, end);
|
|
775
|
-
let binary =
|
|
752
|
+
let binary = "";
|
|
776
753
|
for (let i = 0; i < chunk.length; i++) {
|
|
777
754
|
binary += String.fromCharCode(chunk[i]);
|
|
778
755
|
}
|
|
@@ -786,7 +763,7 @@ module.exports.createInstance = async ({
|
|
|
786
763
|
...attrs,
|
|
787
764
|
exposeFunctions: {
|
|
788
765
|
__polotnoWriteChunk: (base64Chunk) => {
|
|
789
|
-
const buffer = Buffer.from(base64Chunk,
|
|
766
|
+
const buffer = Buffer.from(base64Chunk, "base64");
|
|
790
767
|
writeStream.write(buffer);
|
|
791
768
|
},
|
|
792
769
|
},
|
|
@@ -794,13 +771,13 @@ module.exports.createInstance = async ({
|
|
|
794
771
|
);
|
|
795
772
|
|
|
796
773
|
writeStream.end();
|
|
797
|
-
await new Promise((resolve) => writeStream.on(
|
|
774
|
+
await new Promise((resolve) => writeStream.on("finish", resolve));
|
|
798
775
|
|
|
799
776
|
// Convert audio to AAC if needed
|
|
800
777
|
if (isFfmpegAvailable()) {
|
|
801
778
|
const codec = getAudioCodec(outputPath);
|
|
802
|
-
if (codec && codec !==
|
|
803
|
-
const tempPath = outputPath +
|
|
779
|
+
if (codec && codec !== "aac") {
|
|
780
|
+
const tempPath = outputPath + ".tmp.mp4";
|
|
804
781
|
convertAudioToAac(outputPath, tempPath);
|
|
805
782
|
await fs.promises.rename(tempPath, outputPath);
|
|
806
783
|
}
|
|
@@ -820,8 +797,8 @@ module.exports.createInstance = async ({
|
|
|
820
797
|
try {
|
|
821
798
|
const mimeType = await jsonToVideoFile(json, tempPath, attrs);
|
|
822
799
|
const buffer = await fs.promises.readFile(tempPath);
|
|
823
|
-
return `data:${mimeType ||
|
|
824
|
-
|
|
800
|
+
return `data:${mimeType || "video/mp4"};base64,${buffer.toString(
|
|
801
|
+
"base64",
|
|
825
802
|
)}`;
|
|
826
803
|
} finally {
|
|
827
804
|
await fs.promises.rm(tempPath, { force: true });
|
|
@@ -836,7 +813,7 @@ module.exports.createInstance = async ({
|
|
|
836
813
|
try {
|
|
837
814
|
await jsonToVideoFile(json, tempPath, attrs);
|
|
838
815
|
const buffer = await fs.promises.readFile(tempPath);
|
|
839
|
-
return buffer.toString(
|
|
816
|
+
return buffer.toString("base64");
|
|
840
817
|
} finally {
|
|
841
818
|
await fs.promises.rm(tempPath, { force: true });
|
|
842
819
|
}
|
|
@@ -861,7 +838,12 @@ module.exports.createInstance = async ({
|
|
|
861
838
|
jsonToVideoBase64,
|
|
862
839
|
jsonToVideoFile,
|
|
863
840
|
createPage: async () =>
|
|
864
|
-
await module.exports.createPage(
|
|
841
|
+
await module.exports.createPage(
|
|
842
|
+
browser,
|
|
843
|
+
visitPage,
|
|
844
|
+
requestInterceptor,
|
|
845
|
+
useFontCache,
|
|
846
|
+
),
|
|
865
847
|
};
|
|
866
848
|
|
|
867
849
|
createdInstances.push(instance);
|
|
@@ -874,7 +856,7 @@ const closeAllInstances = async () => {
|
|
|
874
856
|
};
|
|
875
857
|
|
|
876
858
|
// Also handle other termination signals
|
|
877
|
-
[
|
|
859
|
+
["SIGINT", "SIGTERM", "SIGQUIT", "exit", "exit"].forEach((signal) => {
|
|
878
860
|
process.on(signal, () => {
|
|
879
861
|
closeAllInstances();
|
|
880
862
|
});
|