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/video-parallel.js
DELETED
|
@@ -1,414 +0,0 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const ffmpeg = require('fluent-ffmpeg');
|
|
3
|
-
const axios = require('axios');
|
|
4
|
-
const path = require('path');
|
|
5
|
-
|
|
6
|
-
const tmp = require('tmp');
|
|
7
|
-
tmp.setGracefulCleanup();
|
|
8
|
-
|
|
9
|
-
const downloadVideo = async (url, destination) => {
|
|
10
|
-
const response = await axios({
|
|
11
|
-
method: 'GET',
|
|
12
|
-
url: url,
|
|
13
|
-
responseType: 'stream',
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
const writer = response.data.pipe(fs.createWriteStream(destination));
|
|
17
|
-
|
|
18
|
-
return new Promise((resolve, reject) => {
|
|
19
|
-
writer.on('finish', resolve);
|
|
20
|
-
writer.on('error', reject);
|
|
21
|
-
});
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
// 2. Convert it into webm format
|
|
25
|
-
const convertToWebM = (input, output) => {
|
|
26
|
-
return new Promise((resolve, reject) => {
|
|
27
|
-
ffmpeg(input)
|
|
28
|
-
.outputOptions([
|
|
29
|
-
'-c:v libvpx-vp9',
|
|
30
|
-
'-crf 30',
|
|
31
|
-
'-b:v 0',
|
|
32
|
-
'-b:a 128k',
|
|
33
|
-
'-c:a libopus',
|
|
34
|
-
'-cpu-used 4', // Faster encoding with slight quality loss
|
|
35
|
-
'-deadline realtime', // Fastest encoding mode
|
|
36
|
-
'-threads 0', // Use all available CPU cores
|
|
37
|
-
])
|
|
38
|
-
.output(output)
|
|
39
|
-
.on('end', () => {
|
|
40
|
-
resolve();
|
|
41
|
-
})
|
|
42
|
-
.on('error', (err) => {
|
|
43
|
-
console.error('Error occurred: ' + err.message);
|
|
44
|
-
reject(err);
|
|
45
|
-
})
|
|
46
|
-
.run();
|
|
47
|
-
});
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
// 3. Serialize file into dataurl string
|
|
51
|
-
const fileToDataUrl = (filename) => {
|
|
52
|
-
return new Promise((resolve, reject) => {
|
|
53
|
-
fs.readFile(filename, 'base64', (err, data) => {
|
|
54
|
-
if (err) reject(err);
|
|
55
|
-
resolve(`data:video/webm;base64,${data}`);
|
|
56
|
-
});
|
|
57
|
-
});
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
const convertVideo = async (url, tempFolder, skipWebm) => {
|
|
61
|
-
const shortUrl = url.slice(0, 50) + '...';
|
|
62
|
-
console.log('Downloading video: ' + shortUrl);
|
|
63
|
-
const filename = Math.random().toString(36).substring(7);
|
|
64
|
-
const mp4Destination = path.join(tempFolder.name, filename + '.mp4');
|
|
65
|
-
await downloadVideo(url, mp4Destination);
|
|
66
|
-
let dataURL = '';
|
|
67
|
-
if (!skipWebm) {
|
|
68
|
-
console.log('Converting video to webm: ' + filename);
|
|
69
|
-
const webmDestination = mp4Destination.replace('.mp4', '.webm');
|
|
70
|
-
await convertToWebM(mp4Destination, webmDestination);
|
|
71
|
-
dataURL = await fileToDataUrl(webmDestination);
|
|
72
|
-
console.log('Converting video finished: ' + filename);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
return {
|
|
76
|
-
dataURL,
|
|
77
|
-
mp4File: mp4Destination,
|
|
78
|
-
};
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
module.exports.jsonToVideo = async function jsonToVideo(
|
|
82
|
-
inst,
|
|
83
|
-
json,
|
|
84
|
-
attrs = {}
|
|
85
|
-
) {
|
|
86
|
-
const tempFolder = tmp.dirSync({
|
|
87
|
-
tmpdir: attrs.tmpdir,
|
|
88
|
-
});
|
|
89
|
-
// const tempFolder = {
|
|
90
|
-
// name: './temp',
|
|
91
|
-
// };
|
|
92
|
-
|
|
93
|
-
try {
|
|
94
|
-
const videoEls = [];
|
|
95
|
-
|
|
96
|
-
for (const page of json.pages) {
|
|
97
|
-
for (const el of page.children) {
|
|
98
|
-
if (el.type === 'video') {
|
|
99
|
-
videoEls.push(el);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
await Promise.all(
|
|
105
|
-
videoEls.map(async (el) => {
|
|
106
|
-
const { dataURL, file, mp4File } = await convertVideo(
|
|
107
|
-
el.src,
|
|
108
|
-
tempFolder,
|
|
109
|
-
attrs.skipWebm
|
|
110
|
-
);
|
|
111
|
-
if (!attrs.skipWebm) {
|
|
112
|
-
el.src = dataURL;
|
|
113
|
-
}
|
|
114
|
-
el.file = file;
|
|
115
|
-
el.mp4File = mp4File;
|
|
116
|
-
})
|
|
117
|
-
);
|
|
118
|
-
|
|
119
|
-
const audios = [];
|
|
120
|
-
// download all audio tracks
|
|
121
|
-
await Promise.all(
|
|
122
|
-
(json.audios || []).map(async (audio) => {
|
|
123
|
-
const filename = Math.random().toString(36).substring(7);
|
|
124
|
-
const destination = path.join(tempFolder.name, filename);
|
|
125
|
-
await downloadVideo(audio.src, destination);
|
|
126
|
-
audios.push({
|
|
127
|
-
...audio,
|
|
128
|
-
file: destination,
|
|
129
|
-
});
|
|
130
|
-
})
|
|
131
|
-
);
|
|
132
|
-
|
|
133
|
-
const duration = json.pages.reduce((acc, page) => {
|
|
134
|
-
return acc + (page.duration ?? 5000);
|
|
135
|
-
}, 0);
|
|
136
|
-
|
|
137
|
-
if (duration === 0) {
|
|
138
|
-
throw new Error('Video duration is 0');
|
|
139
|
-
}
|
|
140
|
-
const parallel = attrs.parallel || 5;
|
|
141
|
-
|
|
142
|
-
const fps = attrs.fps || 30;
|
|
143
|
-
const timePerFrame = 1000 / fps;
|
|
144
|
-
|
|
145
|
-
// loop through the images and add each to the animation
|
|
146
|
-
const framesNumber = Math.floor((duration / 1000) * fps) || 1;
|
|
147
|
-
const frames = Array.from(Array(framesNumber).keys());
|
|
148
|
-
|
|
149
|
-
// split frames into parallel chunks
|
|
150
|
-
const chunkSize = Math.ceil(framesNumber / parallel);
|
|
151
|
-
const chunks = [];
|
|
152
|
-
for (let i = 0; i < framesNumber; i += chunkSize) {
|
|
153
|
-
chunks.push(frames.slice(i, i + chunkSize));
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
let lastTime = 0;
|
|
157
|
-
const pages = json.pages.map((p, index) => {
|
|
158
|
-
const startTime = lastTime;
|
|
159
|
-
lastTime = startTime + p.duration;
|
|
160
|
-
return {
|
|
161
|
-
id: p.id,
|
|
162
|
-
startTime,
|
|
163
|
-
duration: p.duration,
|
|
164
|
-
};
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
let finishedFramesNumber = 0;
|
|
168
|
-
|
|
169
|
-
await Promise.all(
|
|
170
|
-
chunks.map(async (chunk, chunkIndex) => {
|
|
171
|
-
const instance = typeof inst === 'function' ? await inst() : inst;
|
|
172
|
-
const page = await instance.createPage();
|
|
173
|
-
await page.evaluate(() => {
|
|
174
|
-
if (window.config?.onLoadError) {
|
|
175
|
-
window.config.onLoadError((error) => {
|
|
176
|
-
window._polotnoError = error;
|
|
177
|
-
});
|
|
178
|
-
} else {
|
|
179
|
-
console.error(
|
|
180
|
-
'onLoadError function is not defined in the client. Error handling will not work.'
|
|
181
|
-
);
|
|
182
|
-
}
|
|
183
|
-
});
|
|
184
|
-
page.setDefaultNavigationTimeout(60000); // Increase timeout to 60 seconds
|
|
185
|
-
|
|
186
|
-
let lastPageIndex = -1;
|
|
187
|
-
for (const frameIndex of chunk) {
|
|
188
|
-
let currentTime = frameIndex * timePerFrame;
|
|
189
|
-
if (frameIndex === 0) {
|
|
190
|
-
// offset the very first frame to enable animation start
|
|
191
|
-
currentTime = 1;
|
|
192
|
-
}
|
|
193
|
-
if (frameIndex === framesNumber - 1) {
|
|
194
|
-
// offset the very last frame to enable animation end
|
|
195
|
-
currentTime = duration - 1;
|
|
196
|
-
}
|
|
197
|
-
const storePageIndex = pages.findIndex((p) => {
|
|
198
|
-
return (
|
|
199
|
-
currentTime >= p.startTime &&
|
|
200
|
-
currentTime < p.startTime + p.duration
|
|
201
|
-
);
|
|
202
|
-
});
|
|
203
|
-
if (storePageIndex !== lastPageIndex) {
|
|
204
|
-
await page.evaluate(async (json) => {
|
|
205
|
-
store.loadJSON(json);
|
|
206
|
-
await store.waitLoading();
|
|
207
|
-
await new Promise((resolve) => {
|
|
208
|
-
setTimeout(resolve, 100);
|
|
209
|
-
});
|
|
210
|
-
}, json);
|
|
211
|
-
await page.evaluate(async (currentTime) => {
|
|
212
|
-
store.setCurrentTime(currentTime);
|
|
213
|
-
store.checkActivePage();
|
|
214
|
-
store.activePage.set({
|
|
215
|
-
_rendering: true,
|
|
216
|
-
});
|
|
217
|
-
await store.waitLoading();
|
|
218
|
-
await new Promise((resolve) => {
|
|
219
|
-
setTimeout(resolve, 100);
|
|
220
|
-
});
|
|
221
|
-
if (window.config.unstable_setTextOverflow) {
|
|
222
|
-
window.config.unstable_setTextOverflow('resize');
|
|
223
|
-
} else {
|
|
224
|
-
// console.error(
|
|
225
|
-
// 'Can not set text overflow mode. Define window.config.unstable_setTextOverflow function'
|
|
226
|
-
// );
|
|
227
|
-
}
|
|
228
|
-
}, currentTime);
|
|
229
|
-
lastPageIndex = storePageIndex;
|
|
230
|
-
}
|
|
231
|
-
const startTime = Date.now();
|
|
232
|
-
const dataURL = await page.evaluate(
|
|
233
|
-
async (json, attrs, currentTime) => {
|
|
234
|
-
store.setCurrentTime(currentTime);
|
|
235
|
-
const currentPage = store.pages.find((p) => {
|
|
236
|
-
return (
|
|
237
|
-
store.currentTime >= p.startTime &&
|
|
238
|
-
store.currentTime < p.startTime + p.duration
|
|
239
|
-
);
|
|
240
|
-
});
|
|
241
|
-
const url = await store.toDataURL({
|
|
242
|
-
...attrs,
|
|
243
|
-
_skipTimeout: true,
|
|
244
|
-
mimeType: 'image/jpeg',
|
|
245
|
-
pageId: currentPage?.id,
|
|
246
|
-
});
|
|
247
|
-
return url;
|
|
248
|
-
},
|
|
249
|
-
json,
|
|
250
|
-
attrs || {},
|
|
251
|
-
currentTime
|
|
252
|
-
);
|
|
253
|
-
const error = await page.evaluate(() => window._polotnoError);
|
|
254
|
-
if (error) {
|
|
255
|
-
// clear error
|
|
256
|
-
await page.evaluate(() => {
|
|
257
|
-
window._polotnoError = null;
|
|
258
|
-
});
|
|
259
|
-
const message = error.toString();
|
|
260
|
-
const isFontError =
|
|
261
|
-
message.indexOf('Timeout for loading font') >= 0;
|
|
262
|
-
const skipError = isFontError && attrs.skipFontError;
|
|
263
|
-
|
|
264
|
-
const isImageError = message.indexOf('image ') >= 0;
|
|
265
|
-
const skipImageError = isImageError && attrs.skipImageError;
|
|
266
|
-
|
|
267
|
-
if (!skipError && !skipImageError) {
|
|
268
|
-
throw new Error('Asset loading error: ' + error);
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
// TODO: add error handling!!!!
|
|
272
|
-
fs.writeFileSync(
|
|
273
|
-
`${tempFolder.name}/${frameIndex}.jpeg`,
|
|
274
|
-
dataURL.split(',')[1],
|
|
275
|
-
'base64'
|
|
276
|
-
);
|
|
277
|
-
finishedFramesNumber += 1;
|
|
278
|
-
if (attrs.onProgress) {
|
|
279
|
-
const frameTime = Date.now() - startTime;
|
|
280
|
-
const progress = (finishedFramesNumber / framesNumber) * 0.95;
|
|
281
|
-
attrs.onProgress(progress, frameTime);
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
await page.close();
|
|
285
|
-
if (typeof inst === 'function') {
|
|
286
|
-
await instance.close();
|
|
287
|
-
}
|
|
288
|
-
})
|
|
289
|
-
);
|
|
290
|
-
|
|
291
|
-
const inputs = [];
|
|
292
|
-
let pageStartTime = 0;
|
|
293
|
-
for (const page of json.pages) {
|
|
294
|
-
const pageDuration = page.duration || 5000;
|
|
295
|
-
for (const el of page.children) {
|
|
296
|
-
if (el.type === 'video') {
|
|
297
|
-
const startTime = el.startTime || 0;
|
|
298
|
-
const endTime = el.endTime || 1;
|
|
299
|
-
const dur = el.duration || 5000;
|
|
300
|
-
const elStartTime = startTime * dur;
|
|
301
|
-
|
|
302
|
-
const elDuration = Math.min(
|
|
303
|
-
pageDuration,
|
|
304
|
-
dur * (endTime - startTime)
|
|
305
|
-
);
|
|
306
|
-
const elEndTime = elStartTime + elDuration;
|
|
307
|
-
inputs.push({
|
|
308
|
-
file: el.file,
|
|
309
|
-
mp4File: el.mp4File,
|
|
310
|
-
inputStartTime: elStartTime,
|
|
311
|
-
inputEndTime: elEndTime,
|
|
312
|
-
outputStartTime: pageStartTime,
|
|
313
|
-
outputEndTime: pageStartTime + pageDuration,
|
|
314
|
-
volume: el.volume,
|
|
315
|
-
});
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
pageStartTime += pageDuration;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
const format = attrs.out.split('.').pop() || 'mp4';
|
|
322
|
-
|
|
323
|
-
await new Promise((resolve, reject) => {
|
|
324
|
-
const ffmpegCmd = ffmpeg()
|
|
325
|
-
.input(`${tempFolder.name}/%d.jpeg`)
|
|
326
|
-
.inputFPS(fps)
|
|
327
|
-
.videoCodec('libx264')
|
|
328
|
-
.outputOptions('-pix_fmt yuv420p')
|
|
329
|
-
.videoFilters('scale=trunc(iw/2)*2:trunc(ih/2)*2');
|
|
330
|
-
|
|
331
|
-
const audioInputs = [];
|
|
332
|
-
|
|
333
|
-
// Use Promise.all to handle async ffprobe calls
|
|
334
|
-
Promise.all(
|
|
335
|
-
inputs.map((input, index) => {
|
|
336
|
-
return new Promise((resolveProbe) => {
|
|
337
|
-
const inputStartSec = (input.inputStartTime / 1000).toFixed(3);
|
|
338
|
-
const inputDurationSec = (
|
|
339
|
-
(input.inputEndTime - input.inputStartTime) /
|
|
340
|
-
1000
|
|
341
|
-
).toFixed(3);
|
|
342
|
-
const outputOffsetMs = Math.round(input.outputStartTime);
|
|
343
|
-
const volume = input.volume !== undefined ? input.volume : 1;
|
|
344
|
-
|
|
345
|
-
ffmpegCmd
|
|
346
|
-
.input(input.mp4File)
|
|
347
|
-
.inputOptions([`-ss ${inputStartSec}`, `-t ${inputDurationSec}`]);
|
|
348
|
-
|
|
349
|
-
ffmpeg.ffprobe(input.mp4File, (err, metadata) => {
|
|
350
|
-
if (
|
|
351
|
-
!err &&
|
|
352
|
-
metadata.streams.some((stream) => stream.codec_type === 'audio')
|
|
353
|
-
) {
|
|
354
|
-
audioInputs.push({
|
|
355
|
-
index: index + 1,
|
|
356
|
-
delay: outputOffsetMs,
|
|
357
|
-
volume: volume,
|
|
358
|
-
});
|
|
359
|
-
}
|
|
360
|
-
resolveProbe();
|
|
361
|
-
});
|
|
362
|
-
});
|
|
363
|
-
})
|
|
364
|
-
).then(() => {
|
|
365
|
-
// Map the video stream from the image sequence
|
|
366
|
-
ffmpegCmd.outputOptions(['-map 0:v']);
|
|
367
|
-
|
|
368
|
-
// Prepare audio filter complex for both video and pure audio inputs
|
|
369
|
-
const allAudioInputs = [...audioInputs, ...audios];
|
|
370
|
-
if (allAudioInputs.length > 0) {
|
|
371
|
-
const audioFilters = allAudioInputs.map((input, i) => {
|
|
372
|
-
const inputIndex = audioInputs.includes(input)
|
|
373
|
-
? input.index
|
|
374
|
-
: audioInputs.length + i + 1;
|
|
375
|
-
return `[${inputIndex}:a]adelay=${input.delay}|${input.delay},volume=${input.volume},apad[a${i}]`;
|
|
376
|
-
});
|
|
377
|
-
const audioMerge = allAudioInputs.map((_, i) => `[a${i}]`).join('');
|
|
378
|
-
const filterComplex = `${audioFilters.join(
|
|
379
|
-
';'
|
|
380
|
-
)};${audioMerge}amix=inputs=${
|
|
381
|
-
allAudioInputs.length
|
|
382
|
-
}:dropout_transition=0,aformat=sample_fmts=fltp:sample_rates=44100:channel_layouts=stereo,apad,atrim=0:${
|
|
383
|
-
duration / 1000
|
|
384
|
-
}[aout]`;
|
|
385
|
-
|
|
386
|
-
ffmpegCmd.complexFilter(filterComplex);
|
|
387
|
-
ffmpegCmd.outputOptions(['-map [aout]']);
|
|
388
|
-
|
|
389
|
-
// Add pure audio inputs
|
|
390
|
-
audios.forEach((audio) => {
|
|
391
|
-
ffmpegCmd.input(audio.file);
|
|
392
|
-
});
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
ffmpegCmd
|
|
396
|
-
.format(format)
|
|
397
|
-
.audioCodec('aac')
|
|
398
|
-
.on('start', (command) => console.log('FFmpeg command:', command))
|
|
399
|
-
.on('end', () => resolve())
|
|
400
|
-
.on('error', (err, stdout, stderr) => {
|
|
401
|
-
console.error('Error:', err.message);
|
|
402
|
-
console.error('stdout:', stdout);
|
|
403
|
-
console.error('stderr:', stderr);
|
|
404
|
-
reject(err);
|
|
405
|
-
})
|
|
406
|
-
.save(attrs.out);
|
|
407
|
-
});
|
|
408
|
-
});
|
|
409
|
-
} catch (e) {
|
|
410
|
-
throw e;
|
|
411
|
-
} finally {
|
|
412
|
-
fs.rmSync(tempFolder.name, { recursive: true });
|
|
413
|
-
}
|
|
414
|
-
};
|
|
File without changes
|