mnfst 0.5.73 → 0.5.75
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 +93 -8
- package/package.json +1 -1
package/lib/manifest.export.js
CHANGED
|
@@ -150,11 +150,33 @@ function initializeExportPlugin() {
|
|
|
150
150
|
return `export-${ts}.${ext}`;
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
+
// Transparent 1×1 PNG, used as a fallback when an inline image fails to fetch.
|
|
154
|
+
// html-to-image would otherwise reject the whole snapshot on a single CORS or 404.
|
|
155
|
+
const TRANSPARENT_PIXEL = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkAAIAAAoAAv/lxKUAAAAASUVORK5CYII=';
|
|
156
|
+
|
|
157
|
+
// html-to-image rejects with the raw `image.onerror` Event when the assembled
|
|
158
|
+
// SVG fails to decode (oversized clones, malformed inline assets, etc.).
|
|
159
|
+
// Translate those into something readable before logging.
|
|
160
|
+
function describeExportError(err) {
|
|
161
|
+
if (err && err.message) return err;
|
|
162
|
+
if (err && typeof Event !== 'undefined' && err instanceof Event) {
|
|
163
|
+
const tag = err.target && err.target.tagName ? err.target.tagName.toLowerCase() : 'image';
|
|
164
|
+
return new Error(
|
|
165
|
+
`failed to render ${tag} during export. ` +
|
|
166
|
+
`Common causes: cross-origin images without CORS headers, ` +
|
|
167
|
+
`an oversized target element, or a no-target snapshot of a complex page. ` +
|
|
168
|
+
`Pass a "target" option to scope the snapshot.`
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
return new Error(String(err));
|
|
172
|
+
}
|
|
173
|
+
|
|
153
174
|
function emitError(format, err) {
|
|
154
|
-
|
|
175
|
+
const e = describeExportError(err);
|
|
176
|
+
console.error('[x-export] export failed:', e.message);
|
|
155
177
|
try {
|
|
156
178
|
window.dispatchEvent(new CustomEvent('manifest:export-error', {
|
|
157
|
-
detail: { format, error:
|
|
179
|
+
detail: { format, error: e.message }
|
|
158
180
|
}));
|
|
159
181
|
} catch { /* ignore */ }
|
|
160
182
|
}
|
|
@@ -194,6 +216,10 @@ function initializeExportPlugin() {
|
|
|
194
216
|
const out = {
|
|
195
217
|
pixelRatio: Number(opts.resolution) > 0 ? Number(opts.resolution) : 2,
|
|
196
218
|
cacheBust: true,
|
|
219
|
+
// A single cross-origin image without CORS headers would otherwise
|
|
220
|
+
// reject the whole snapshot. Substitute a transparent pixel so the
|
|
221
|
+
// export still produces a usable file.
|
|
222
|
+
imagePlaceholder: TRANSPARENT_PIXEL,
|
|
197
223
|
filter
|
|
198
224
|
};
|
|
199
225
|
if (opts.backgroundColor) out.backgroundColor = opts.backgroundColor;
|
|
@@ -202,31 +228,76 @@ function initializeExportPlugin() {
|
|
|
202
228
|
return out;
|
|
203
229
|
}
|
|
204
230
|
|
|
231
|
+
// Run an html-to-image call once. If it rejects with a raw Event (the SVG
|
|
232
|
+
// failed to decode — usually a font or oversized clone issue), retry once
|
|
233
|
+
// with safer options before propagating the error. Most real-world failures
|
|
234
|
+
// recover on the second attempt.
|
|
235
|
+
async function snapshotWithFallback(lib, fn, target, so) {
|
|
236
|
+
try {
|
|
237
|
+
return await lib[fn](target, so);
|
|
238
|
+
} catch (err) {
|
|
239
|
+
if (err && err.message) throw err; // a real Error — don't mask it
|
|
240
|
+
const safer = { ...so, skipFonts: true, pixelRatio: Math.min(so.pixelRatio || 2, 1) };
|
|
241
|
+
return await lib[fn](target, safer);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Chromium and Safari reject SVG-image decoding past roughly 16,000px on
|
|
246
|
+
// either axis. A long article at pixelRatio: 2 easily exceeds that. When
|
|
247
|
+
// the target is the whole body we cap dimensions so the assembled SVG
|
|
248
|
+
// stays decodable; explicit targets keep their natural size.
|
|
249
|
+
const SAFE_MAX_DIMENSION = 14000;
|
|
250
|
+
|
|
251
|
+
function applySafeDimensions(target, so) {
|
|
252
|
+
const isWholeBody = target === document.body || target === document.documentElement;
|
|
253
|
+
if (!isWholeBody) return so;
|
|
254
|
+
const rect = target.getBoundingClientRect();
|
|
255
|
+
const width = Math.max(rect.width, target.scrollWidth || 0);
|
|
256
|
+
const height = Math.max(rect.height, target.scrollHeight || 0);
|
|
257
|
+
const ratio = so.pixelRatio || 2;
|
|
258
|
+
const limit = SAFE_MAX_DIMENSION / ratio;
|
|
259
|
+
const out = { ...so };
|
|
260
|
+
if (width > limit) out.canvasWidth = Math.floor(limit);
|
|
261
|
+
if (height > limit) out.canvasHeight = Math.floor(limit);
|
|
262
|
+
return out;
|
|
263
|
+
}
|
|
264
|
+
|
|
205
265
|
async function exportImage(opts, filename, ext) {
|
|
206
266
|
const lib = await loadHtmlToImage();
|
|
207
267
|
const target = resolveTarget(opts);
|
|
208
|
-
const so = snapshotOptions(opts);
|
|
268
|
+
const so = applySafeDimensions(target, snapshotOptions(opts));
|
|
209
269
|
let dataUrl;
|
|
210
|
-
if (ext === 'png') dataUrl = await lib
|
|
270
|
+
if (ext === 'png') dataUrl = await snapshotWithFallback(lib, 'toPng', target, so);
|
|
211
271
|
else if (ext === 'jpeg') {
|
|
212
272
|
if (!so.backgroundColor) so.backgroundColor = effectivePageBackground();
|
|
213
273
|
so.quality = Number(opts.quality) > 0 && Number(opts.quality) <= 1
|
|
214
274
|
? Number(opts.quality)
|
|
215
275
|
: 0.95;
|
|
216
|
-
dataUrl = await lib
|
|
276
|
+
dataUrl = await snapshotWithFallback(lib, 'toJpeg', target, so);
|
|
217
277
|
} else if (ext === 'webp') {
|
|
218
|
-
const
|
|
278
|
+
const webpOpts = { ...so, type: 'image/webp', quality: opts.quality || 0.95 };
|
|
279
|
+
const blob = await snapshotWithFallback(lib, 'toBlob', target, webpOpts);
|
|
219
280
|
dataUrl = URL.createObjectURL(blob);
|
|
220
281
|
}
|
|
221
282
|
triggerDownload(dataUrl, filename);
|
|
222
283
|
}
|
|
223
284
|
|
|
285
|
+
// Whole-page PDF: route through the browser's native print pipeline.
|
|
286
|
+
// It handles multi-page layout, page breaks, vector text, and the page's
|
|
287
|
+
// own @media print CSS — far more reliable than rasterizing a long page
|
|
288
|
+
// and embedding it as a single image. The user sees the print dialog and
|
|
289
|
+
// picks "Save as PDF" (or any installed PDF printer). Element-scoped PDFs
|
|
290
|
+
// continue to use html-to-image + jsPDF, which is well-behaved at small
|
|
291
|
+
// target sizes.
|
|
224
292
|
async function exportPdf(opts, filename) {
|
|
293
|
+
if (!opts.target) {
|
|
294
|
+
return printToPdf(filename);
|
|
295
|
+
}
|
|
225
296
|
const [imgLib, jsPDFCtor] = await Promise.all([loadHtmlToImage(), loadJsPDF()]);
|
|
226
297
|
const target = resolveTarget(opts);
|
|
227
|
-
const so = snapshotOptions(opts);
|
|
298
|
+
const so = applySafeDimensions(target, snapshotOptions(opts));
|
|
228
299
|
if (!so.backgroundColor) so.backgroundColor = effectivePageBackground();
|
|
229
|
-
const dataUrl = await imgLib
|
|
300
|
+
const dataUrl = await snapshotWithFallback(imgLib, 'toPng', target, so);
|
|
230
301
|
const img = await loadImage(dataUrl);
|
|
231
302
|
const orientation = img.width > img.height ? 'landscape' : 'portrait';
|
|
232
303
|
const pdf = new jsPDFCtor({ orientation, unit: 'pt', format: opts.pageSize || 'a4' });
|
|
@@ -241,6 +312,20 @@ function initializeExportPlugin() {
|
|
|
241
312
|
pdf.save(filename);
|
|
242
313
|
}
|
|
243
314
|
|
|
315
|
+
function printToPdf(filename) {
|
|
316
|
+
// The browser's "Save as PDF" dialog seeds its default filename from
|
|
317
|
+
// document.title. Swap it briefly so the suggested name matches the
|
|
318
|
+
// user's intent, then restore after the dialog closes.
|
|
319
|
+
const original = document.title;
|
|
320
|
+
const cleaned = String(filename || '').replace(/\.pdf$/i, '') || original;
|
|
321
|
+
document.title = cleaned;
|
|
322
|
+
try { window.print(); }
|
|
323
|
+
finally {
|
|
324
|
+
// Wait one frame so the print dialog reads the swapped title first.
|
|
325
|
+
setTimeout(() => { document.title = original; }, 0);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
244
329
|
function effectivePageBackground() {
|
|
245
330
|
let el = document.body;
|
|
246
331
|
while (el && el !== document.documentElement.parentElement) {
|