mnfst 0.5.74 → 0.5.77
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/lib/manifest.export.js +92 -37
- package/package.json +1 -1
package/lib/manifest.export.js
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
*
|
|
27
27
|
* Supported formats: pdf (default), png, jpeg, webp, csv, json.
|
|
28
28
|
*
|
|
29
|
-
* Library dependencies (
|
|
29
|
+
* Library dependencies (modern-screenshot, jsPDF) are loaded lazily on first
|
|
30
30
|
* use from jsDelivr; pages that never export pay nothing.
|
|
31
31
|
*
|
|
32
32
|
* Elements with `data-no-export` are excluded from visual snapshots.
|
|
@@ -151,12 +151,12 @@ function initializeExportPlugin() {
|
|
|
151
151
|
}
|
|
152
152
|
|
|
153
153
|
// Transparent 1×1 PNG, used as a fallback when an inline image fails to fetch.
|
|
154
|
-
//
|
|
154
|
+
// Without it, a single CORS or 404 image would reject the entire snapshot.
|
|
155
155
|
const TRANSPARENT_PIXEL = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkAAIAAAoAAv/lxKUAAAAASUVORK5CYII=';
|
|
156
156
|
|
|
157
|
-
//
|
|
158
|
-
// SVG fails to decode (oversized clones, malformed inline assets,
|
|
159
|
-
// Translate those into something readable before logging.
|
|
157
|
+
// The snapshot library rejects with the raw `image.onerror` Event when the
|
|
158
|
+
// assembled SVG fails to decode (oversized clones, malformed inline assets,
|
|
159
|
+
// etc.). Translate those into something readable before logging.
|
|
160
160
|
function describeExportError(err) {
|
|
161
161
|
if (err && err.message) return err;
|
|
162
162
|
if (err && typeof Event !== 'undefined' && err instanceof Event) {
|
|
@@ -208,66 +208,102 @@ function initializeExportPlugin() {
|
|
|
208
208
|
}
|
|
209
209
|
|
|
210
210
|
function snapshotOptions(opts) {
|
|
211
|
-
// Filter callback for
|
|
211
|
+
// Filter callback for modern-screenshot — exclude opt-out elements.
|
|
212
212
|
const filter = (node) => {
|
|
213
213
|
if (!node || node.nodeType !== 1) return true;
|
|
214
214
|
return !(node.hasAttribute && node.hasAttribute('data-no-export'));
|
|
215
215
|
};
|
|
216
216
|
const out = {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
217
|
+
scale: Number(opts.resolution) > 0 ? Number(opts.resolution) : 2,
|
|
218
|
+
filter,
|
|
219
|
+
fetch: {
|
|
220
|
+
// Cross-origin assets without CORS headers fall back to this
|
|
221
|
+
// transparent pixel instead of rejecting the entire snapshot.
|
|
222
|
+
placeholderImage: TRANSPARENT_PIXEL,
|
|
223
|
+
// Always bypass the cache so we get fresh, CORS-correct copies.
|
|
224
|
+
bypassingCache: true,
|
|
225
|
+
},
|
|
224
226
|
};
|
|
225
227
|
if (opts.backgroundColor) out.backgroundColor = opts.backgroundColor;
|
|
226
|
-
if (opts.width) out.
|
|
227
|
-
if (opts.height) out.
|
|
228
|
+
if (opts.width) out.width = Number(opts.width);
|
|
229
|
+
if (opts.height) out.height = Number(opts.height);
|
|
228
230
|
return out;
|
|
229
231
|
}
|
|
230
232
|
|
|
231
|
-
// Run
|
|
232
|
-
// failed to decode
|
|
233
|
-
//
|
|
234
|
-
// recover on the second attempt.
|
|
233
|
+
// Run a modern-screenshot call once. If it rejects with a raw Event (the
|
|
234
|
+
// assembled SVG failed to decode), retry once with safer options before
|
|
235
|
+
// propagating. Most real-world failures recover on the second attempt.
|
|
235
236
|
async function snapshotWithFallback(lib, fn, target, so) {
|
|
236
237
|
try {
|
|
237
238
|
return await lib[fn](target, so);
|
|
238
239
|
} catch (err) {
|
|
239
240
|
if (err && err.message) throw err; // a real Error — don't mask it
|
|
240
|
-
const safer = {
|
|
241
|
+
const safer = {
|
|
242
|
+
...so,
|
|
243
|
+
scale: Math.min(so.scale || 2, 1),
|
|
244
|
+
font: { skip: true },
|
|
245
|
+
};
|
|
241
246
|
return await lib[fn](target, safer);
|
|
242
247
|
}
|
|
243
248
|
}
|
|
244
249
|
|
|
250
|
+
// Chromium and Safari reject SVG-image decoding past roughly 16,000px on
|
|
251
|
+
// either axis. A long article at pixelRatio: 2 easily exceeds that. When
|
|
252
|
+
// the target is the whole body we cap dimensions so the assembled SVG
|
|
253
|
+
// stays decodable; explicit targets keep their natural size.
|
|
254
|
+
const SAFE_MAX_DIMENSION = 14000;
|
|
255
|
+
|
|
256
|
+
function applySafeDimensions(target, so) {
|
|
257
|
+
const isWholeBody = target === document.body || target === document.documentElement;
|
|
258
|
+
if (!isWholeBody) return so;
|
|
259
|
+
const rect = target.getBoundingClientRect();
|
|
260
|
+
const width = Math.max(rect.width, target.scrollWidth || 0);
|
|
261
|
+
const height = Math.max(rect.height, target.scrollHeight || 0);
|
|
262
|
+
const ratio = so.pixelRatio || 2;
|
|
263
|
+
const limit = SAFE_MAX_DIMENSION / ratio;
|
|
264
|
+
const out = { ...so };
|
|
265
|
+
if (width > limit) out.canvasWidth = Math.floor(limit);
|
|
266
|
+
if (height > limit) out.canvasHeight = Math.floor(limit);
|
|
267
|
+
return out;
|
|
268
|
+
}
|
|
269
|
+
|
|
245
270
|
async function exportImage(opts, filename, ext) {
|
|
246
|
-
const lib = await
|
|
271
|
+
const lib = await loadSnapshotLib();
|
|
247
272
|
const target = resolveTarget(opts);
|
|
248
|
-
const so = snapshotOptions(opts);
|
|
273
|
+
const so = applySafeDimensions(target, snapshotOptions(opts));
|
|
249
274
|
let dataUrl;
|
|
250
|
-
if (ext === 'png') dataUrl = await snapshotWithFallback(lib, '
|
|
275
|
+
if (ext === 'png') dataUrl = await snapshotWithFallback(lib, 'domToPng', target, so);
|
|
251
276
|
else if (ext === 'jpeg') {
|
|
252
277
|
if (!so.backgroundColor) so.backgroundColor = effectivePageBackground();
|
|
253
278
|
so.quality = Number(opts.quality) > 0 && Number(opts.quality) <= 1
|
|
254
279
|
? Number(opts.quality)
|
|
255
280
|
: 0.95;
|
|
256
|
-
dataUrl = await snapshotWithFallback(lib, '
|
|
281
|
+
dataUrl = await snapshotWithFallback(lib, 'domToJpeg', target, so);
|
|
257
282
|
} else if (ext === 'webp') {
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
283
|
+
so.quality = Number(opts.quality) > 0 && Number(opts.quality) <= 1
|
|
284
|
+
? Number(opts.quality)
|
|
285
|
+
: 0.95;
|
|
286
|
+
dataUrl = await snapshotWithFallback(lib, 'domToWebp', target, so);
|
|
261
287
|
}
|
|
262
288
|
triggerDownload(dataUrl, filename);
|
|
263
289
|
}
|
|
264
290
|
|
|
291
|
+
// Whole-page PDF: route through the browser's native print pipeline.
|
|
292
|
+
// It handles multi-page layout, page breaks, vector text, and the page's
|
|
293
|
+
// own @media print CSS — far more reliable than rasterizing a long page
|
|
294
|
+
// and embedding it as a single image. The user sees the print dialog and
|
|
295
|
+
// picks "Save as PDF" (or any installed PDF printer). Element-scoped PDFs
|
|
296
|
+
// continue to use modern-screenshot + jsPDF, which is well-behaved at small
|
|
297
|
+
// target sizes.
|
|
265
298
|
async function exportPdf(opts, filename) {
|
|
266
|
-
|
|
299
|
+
if (!opts.target) {
|
|
300
|
+
return printToPdf(filename);
|
|
301
|
+
}
|
|
302
|
+
const [imgLib, jsPDFCtor] = await Promise.all([loadSnapshotLib(), loadJsPDF()]);
|
|
267
303
|
const target = resolveTarget(opts);
|
|
268
|
-
const so = snapshotOptions(opts);
|
|
304
|
+
const so = applySafeDimensions(target, snapshotOptions(opts));
|
|
269
305
|
if (!so.backgroundColor) so.backgroundColor = effectivePageBackground();
|
|
270
|
-
const dataUrl = await snapshotWithFallback(imgLib, '
|
|
306
|
+
const dataUrl = await snapshotWithFallback(imgLib, 'domToPng', target, so);
|
|
271
307
|
const img = await loadImage(dataUrl);
|
|
272
308
|
const orientation = img.width > img.height ? 'landscape' : 'portrait';
|
|
273
309
|
const pdf = new jsPDFCtor({ orientation, unit: 'pt', format: opts.pageSize || 'a4' });
|
|
@@ -282,6 +318,20 @@ function initializeExportPlugin() {
|
|
|
282
318
|
pdf.save(filename);
|
|
283
319
|
}
|
|
284
320
|
|
|
321
|
+
function printToPdf(filename) {
|
|
322
|
+
// The browser's "Save as PDF" dialog seeds its default filename from
|
|
323
|
+
// document.title. Swap it briefly so the suggested name matches the
|
|
324
|
+
// user's intent, then restore after the dialog closes.
|
|
325
|
+
const original = document.title;
|
|
326
|
+
const cleaned = String(filename || '').replace(/\.pdf$/i, '') || original;
|
|
327
|
+
document.title = cleaned;
|
|
328
|
+
try { window.print(); }
|
|
329
|
+
finally {
|
|
330
|
+
// Wait one frame so the print dialog reads the swapped title first.
|
|
331
|
+
setTimeout(() => { document.title = original; }, 0);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
285
335
|
function effectivePageBackground() {
|
|
286
336
|
let el = document.body;
|
|
287
337
|
while (el && el !== document.documentElement.parentElement) {
|
|
@@ -379,15 +429,20 @@ function initializeExportPlugin() {
|
|
|
379
429
|
|
|
380
430
|
// Lazy library loaders — cached promises.
|
|
381
431
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
432
|
+
// modern-screenshot is an actively maintained TypeScript rewrite of
|
|
433
|
+
// html-to-image with proper CSS AST parsing. It handles @layer rules,
|
|
434
|
+
// CSS custom properties, modern color functions, and url(data:...) values
|
|
435
|
+
// inside cross-origin stylesheets — all of which html-to-image mishandles.
|
|
436
|
+
let snapshotLibPromise = null;
|
|
437
|
+
function loadSnapshotLib() {
|
|
438
|
+
if (snapshotLibPromise) return snapshotLibPromise;
|
|
439
|
+
snapshotLibPromise = loadScript('https://cdn.jsdelivr.net/npm/modern-screenshot/dist/index.iife.min.js')
|
|
386
440
|
.then(() => {
|
|
387
|
-
|
|
388
|
-
|
|
441
|
+
const lib = window.modernScreenshot;
|
|
442
|
+
if (!lib) throw new Error('modern-screenshot failed to load');
|
|
443
|
+
return lib;
|
|
389
444
|
});
|
|
390
|
-
return
|
|
445
|
+
return snapshotLibPromise;
|
|
391
446
|
}
|
|
392
447
|
|
|
393
448
|
let jsPDFPromise = null;
|