smooth-screenshot 1.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/dist/index.mjs ADDED
@@ -0,0 +1,1285 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
+ var __publicField = (obj, key, value) => __defNormalProp(obj, key + "" , value);
4
+
5
+ // src/options.ts
6
+ var DEFAULT_QUALITY = 0.92;
7
+ var DEFAULT_FRAME_BUDGET_MS = 5;
8
+ var DEFAULT_WORKER_COUNT = 2;
9
+ var DEFAULT_FETCH_TIMEOUT_MS = 3e4;
10
+ var DEFAULT_FETCH_RETRIES = 2;
11
+ function resolveWorkerCount(worker) {
12
+ if (worker === false) return 0;
13
+ if (worker === true || worker === void 0) return DEFAULT_WORKER_COUNT;
14
+ return Math.max(0, Math.floor(worker.count));
15
+ }
16
+ function resolveOptions(options, win) {
17
+ const scale = options.scale ?? options.pixelRatio ?? win.devicePixelRatio ?? 1;
18
+ return {
19
+ type: options.type ?? "png",
20
+ quality: options.quality ?? DEFAULT_QUALITY,
21
+ scale: scale > 0 ? scale : 1,
22
+ backgroundColor: options.backgroundColor ?? null,
23
+ width: options.width ?? null,
24
+ height: options.height ?? null,
25
+ crop: options.crop ?? null,
26
+ signal: options.signal,
27
+ onProgress: options.onProgress ?? null,
28
+ frameBudgetMs: options.frameBudgetMs ?? DEFAULT_FRAME_BUDGET_MS,
29
+ workerCount: resolveWorkerCount(options.worker),
30
+ maxTileSize: options.maxTileSize ?? 0,
31
+ filter: options.filter ?? null,
32
+ fontsSkip: options.fonts?.skip ?? false,
33
+ fontsPreferWoff2: options.fonts?.preferWoff2 ?? true,
34
+ fetchRequestInit: options.fetch?.requestInit,
35
+ fetchPlaceholder: options.fetch?.placeholder ?? null,
36
+ fetchTimeoutMs: options.fetch?.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS,
37
+ fetchRetries: options.fetch?.retries ?? DEFAULT_FETCH_RETRIES,
38
+ onResourceError: options.onResourceError ?? null
39
+ };
40
+ }
41
+
42
+ // src/errors.ts
43
+ var SmoothScreenshotError = class _SmoothScreenshotError extends Error {
44
+ constructor(code, message, options) {
45
+ super(message, options);
46
+ __publicField(this, "code");
47
+ this.name = "SmoothScreenshotError";
48
+ this.code = code;
49
+ Object.setPrototypeOf(this, _SmoothScreenshotError.prototype);
50
+ }
51
+ };
52
+ function createAbortError() {
53
+ return new DOMException("The screenshot was aborted.", "AbortError");
54
+ }
55
+ function isAbortError(error) {
56
+ return error instanceof DOMException && error.name === "AbortError";
57
+ }
58
+
59
+ // src/scheduler.ts
60
+ function createScheduler(budgetMs, signal) {
61
+ let sliceStart = performance.now();
62
+ const channel = new MessageChannel();
63
+ const pending = [];
64
+ channel.port1.onmessage = () => {
65
+ pending.shift()?.();
66
+ };
67
+ function macrotask() {
68
+ return new Promise((resolve) => {
69
+ pending.push(resolve);
70
+ channel.port2.postMessage(void 0);
71
+ });
72
+ }
73
+ function abortGuard() {
74
+ if (signal?.aborted) throw createAbortError();
75
+ }
76
+ const scheduler = {
77
+ async yieldNow() {
78
+ await macrotask();
79
+ sliceStart = performance.now();
80
+ abortGuard();
81
+ },
82
+ async yieldIfNeeded() {
83
+ if (performance.now() - sliceStart >= budgetMs) {
84
+ await scheduler.yieldNow();
85
+ } else {
86
+ abortGuard();
87
+ }
88
+ },
89
+ reset() {
90
+ sliceStart = performance.now();
91
+ }
92
+ };
93
+ return scheduler;
94
+ }
95
+
96
+ // src/worker/source.ts
97
+ var WORKER_SOURCE = `
98
+ self.onmessage = function (e) {
99
+ var id = e.data.id
100
+ var url = e.data.url
101
+ var init = e.data.init
102
+ fetch(url, init)
103
+ .then(function (res) {
104
+ if (!res.ok) throw new Error('HTTP ' + res.status)
105
+ return res.blob()
106
+ })
107
+ .then(function (blob) {
108
+ var reader = new FileReader()
109
+ reader.onload = function () { self.postMessage({ id: id, result: reader.result }) }
110
+ reader.onerror = function () { self.postMessage({ id: id, error: 'read failed' }) }
111
+ reader.readAsDataURL(blob)
112
+ })
113
+ .catch(function (err) {
114
+ self.postMessage({ id: id, error: String((err && err.message) || err) })
115
+ })
116
+ }
117
+ `;
118
+
119
+ // src/worker/pool.ts
120
+ function sanitizeInit(init) {
121
+ if (!init) return void 0;
122
+ const headers = init.headers instanceof Headers ? Object.fromEntries(init.headers.entries()) : init.headers;
123
+ return {
124
+ method: init.method,
125
+ headers,
126
+ credentials: init.credentials,
127
+ mode: init.mode,
128
+ cache: init.cache,
129
+ referrer: init.referrer,
130
+ integrity: init.integrity
131
+ };
132
+ }
133
+ function createWorkerPool(count) {
134
+ if (count <= 0 || typeof Worker === "undefined") return null;
135
+ let blobUrl;
136
+ let workers;
137
+ try {
138
+ const blob = new Blob([WORKER_SOURCE], { type: "text/javascript" });
139
+ blobUrl = URL.createObjectURL(blob);
140
+ workers = Array.from({ length: count }, () => new Worker(blobUrl));
141
+ } catch {
142
+ return null;
143
+ }
144
+ const pending = /* @__PURE__ */ new Map();
145
+ let nextId = 0;
146
+ let roundRobin = 0;
147
+ let destroyed = false;
148
+ for (const worker of workers) {
149
+ worker.onmessage = (event) => {
150
+ const { id, result, error } = event.data;
151
+ const request = pending.get(id);
152
+ if (!request) return;
153
+ pending.delete(id);
154
+ if (error !== void 0) request.reject(new Error(error));
155
+ else request.resolve(result);
156
+ };
157
+ }
158
+ return {
159
+ fetchAsDataUrl(url, requestInit) {
160
+ if (destroyed) return Promise.reject(new Error("worker pool destroyed"));
161
+ const id = nextId++;
162
+ const worker = workers[roundRobin++ % workers.length];
163
+ return new Promise((resolve, reject) => {
164
+ pending.set(id, { resolve, reject });
165
+ worker.postMessage({ id, url, init: sanitizeInit(requestInit) });
166
+ });
167
+ },
168
+ destroy() {
169
+ if (destroyed) return;
170
+ destroyed = true;
171
+ for (const worker of workers) worker.terminate();
172
+ URL.revokeObjectURL(blobUrl);
173
+ for (const request of pending.values()) {
174
+ request.reject(new Error("worker pool destroyed"));
175
+ }
176
+ pending.clear();
177
+ }
178
+ };
179
+ }
180
+
181
+ // src/context.ts
182
+ var PHASE_WEIGHTS = {
183
+ clone: 0.35,
184
+ embed: 0.3,
185
+ rasterize: 0.2,
186
+ encode: 0.15
187
+ };
188
+ var PHASE_ORDER = ["clone", "embed", "rasterize", "encode"];
189
+ function cumulativeWeightBefore(phase) {
190
+ let sum = 0;
191
+ for (const candidate of PHASE_ORDER) {
192
+ if (candidate === phase) break;
193
+ sum += PHASE_WEIGHTS[candidate];
194
+ }
195
+ return sum;
196
+ }
197
+ function createContext(target, options) {
198
+ const ownerDocument = target.ownerDocument;
199
+ const ownerWindow = ownerDocument.defaultView ?? window;
200
+ const scheduler = createScheduler(options.frameBudgetMs, options.signal);
201
+ const workerPool = createWorkerPool(options.workerCount);
202
+ const cleanups = [];
203
+ let destroyed = false;
204
+ let lastOverall = -1;
205
+ const context = {
206
+ options,
207
+ signal: options.signal,
208
+ ownerDocument,
209
+ ownerWindow,
210
+ scheduler,
211
+ workerPool,
212
+ defaultStyleCache: /* @__PURE__ */ new Map(),
213
+ resourceCache: /* @__PURE__ */ new Map(),
214
+ fontCache: /* @__PURE__ */ new Map(),
215
+ sandbox: null,
216
+ throwIfAborted() {
217
+ if (options.signal?.aborted) throw createAbortError();
218
+ },
219
+ reportPhase(phase, ratio) {
220
+ if (!options.onProgress) return;
221
+ const clamped = ratio < 0 ? 0 : ratio > 1 ? 1 : ratio;
222
+ const overall = cumulativeWeightBefore(phase) + PHASE_WEIGHTS[phase] * clamped;
223
+ if (overall === lastOverall) return;
224
+ lastOverall = overall;
225
+ options.onProgress({ phase, phaseRatio: clamped, overall });
226
+ },
227
+ addCleanup(fn) {
228
+ cleanups.push(fn);
229
+ },
230
+ destroy() {
231
+ if (destroyed) return;
232
+ destroyed = true;
233
+ while (cleanups.length) {
234
+ const fn = cleanups.pop();
235
+ try {
236
+ fn();
237
+ } catch {
238
+ }
239
+ }
240
+ if (context.sandbox) {
241
+ context.sandbox.remove();
242
+ context.sandbox = null;
243
+ }
244
+ workerPool?.destroy();
245
+ }
246
+ };
247
+ return context;
248
+ }
249
+
250
+ // src/clone/default-style.ts
251
+ var XHTML_NS = "http://www.w3.org/1999/xhtml";
252
+ function ensureSandbox(context) {
253
+ if (context.sandbox?.contentDocument) {
254
+ return context.sandbox.contentDocument;
255
+ }
256
+ const iframe = context.ownerDocument.createElement("iframe");
257
+ iframe.setAttribute("aria-hidden", "true");
258
+ iframe.setAttribute("tabindex", "-1");
259
+ iframe.style.cssText = "position:fixed;left:-9999px;top:0;width:0;height:0;border:0;visibility:hidden;";
260
+ context.ownerDocument.body.appendChild(iframe);
261
+ context.sandbox = iframe;
262
+ const doc = iframe.contentDocument;
263
+ if (!doc) throw new Error("Unable to create style sandbox.");
264
+ return doc;
265
+ }
266
+ function getDefaultStyle(context, source) {
267
+ const namespace = source.namespaceURI ?? XHTML_NS;
268
+ const key = `${namespace}|${source.localName}`;
269
+ const cached = context.defaultStyleCache.get(key);
270
+ if (cached) return cached;
271
+ const doc = ensureSandbox(context);
272
+ const sandboxWindow = doc.defaultView;
273
+ if (!sandboxWindow || !doc.body) {
274
+ const empty = {};
275
+ context.defaultStyleCache.set(key, empty);
276
+ return empty;
277
+ }
278
+ const element = namespace === XHTML_NS ? doc.createElement(source.localName) : doc.createElementNS(namespace, source.localName);
279
+ doc.body.appendChild(element);
280
+ const computed = sandboxWindow.getComputedStyle(element);
281
+ const record = {};
282
+ for (let i = 0; i < computed.length; i++) {
283
+ const name = computed[i];
284
+ if (name) record[name] = computed.getPropertyValue(name);
285
+ }
286
+ doc.body.removeChild(element);
287
+ context.defaultStyleCache.set(key, record);
288
+ return record;
289
+ }
290
+
291
+ // src/clone/copy-styles.ts
292
+ function copyStyle(source, target, context) {
293
+ const computed = context.ownerWindow.getComputedStyle(source);
294
+ const defaults = getDefaultStyle(context, source);
295
+ const targetStyle = target.style;
296
+ for (let i = 0; i < computed.length; i++) {
297
+ const name = computed[i];
298
+ if (!name) continue;
299
+ const value = computed.getPropertyValue(name);
300
+ if (value === "" || defaults[name] === value) continue;
301
+ targetStyle.setProperty(name, value, computed.getPropertyPriority(name));
302
+ }
303
+ }
304
+
305
+ // src/clone/pseudo.ts
306
+ var PSEUDOS = ["::before", "::after"];
307
+ var CLASS_PREFIX = "ss-pseudo-";
308
+ function clonePseudoElements(source, target, state, context) {
309
+ let className = null;
310
+ for (const pseudo of PSEUDOS) {
311
+ const computed = context.ownerWindow.getComputedStyle(source, pseudo);
312
+ const content = computed.getPropertyValue("content");
313
+ if (!content || content === "none" || content === "normal") continue;
314
+ if (!className) {
315
+ className = `${CLASS_PREFIX}${state.uid++}`;
316
+ target.classList.add(className);
317
+ }
318
+ let body = "";
319
+ for (let i = 0; i < computed.length; i++) {
320
+ const name = computed[i];
321
+ if (!name) continue;
322
+ const value = computed.getPropertyValue(name);
323
+ if (value === "") continue;
324
+ const priority = computed.getPropertyPriority(name);
325
+ body += `${name}:${value}${priority ? " !important" : ""};`;
326
+ }
327
+ state.pseudoRules.push(`.${className}${pseudo}{${body}}`);
328
+ }
329
+ }
330
+
331
+ // src/clone/special.ts
332
+ function snapshotCanvas(canvas) {
333
+ try {
334
+ return canvas.toDataURL();
335
+ } catch {
336
+ return null;
337
+ }
338
+ }
339
+ function copyInputValue(source, target) {
340
+ if (source instanceof HTMLTextAreaElement) {
341
+ target.textContent = source.value;
342
+ return;
343
+ }
344
+ if (source instanceof HTMLInputElement) {
345
+ const input = target;
346
+ if (source.type === "checkbox" || source.type === "radio") {
347
+ if (source.checked) input.setAttribute("checked", "");
348
+ else input.removeAttribute("checked");
349
+ } else {
350
+ input.setAttribute("value", source.value);
351
+ }
352
+ return;
353
+ }
354
+ if (source instanceof HTMLSelectElement) {
355
+ target.setAttribute("data-ss-selected", source.value);
356
+ }
357
+ }
358
+
359
+ // src/clone/state.ts
360
+ function createCloneState(total) {
361
+ return { cloned: 0, total: Math.max(1, total), pseudoRules: [], uid: 0 };
362
+ }
363
+
364
+ // src/clone/clone-node.ts
365
+ var SKIP_TAGS = /* @__PURE__ */ new Set(["SCRIPT", "NOSCRIPT", "TEMPLATE"]);
366
+ var PROGRESS_EVERY = 32;
367
+ var ELEMENT_NODE = 1;
368
+ var TEXT_NODE = 3;
369
+ function passesFilter(node, context, isRoot) {
370
+ if (isRoot || !context.options.filter) return true;
371
+ if (node.nodeType !== ELEMENT_NODE) return true;
372
+ return context.options.filter(node);
373
+ }
374
+ async function cloneNode(node, context, state, isRoot) {
375
+ if (!passesFilter(node, context, isRoot)) return null;
376
+ if (node.nodeType === TEXT_NODE) {
377
+ return node.cloneNode(false);
378
+ }
379
+ if (node.nodeType !== ELEMENT_NODE) return null;
380
+ const element = node;
381
+ if (SKIP_TAGS.has(element.tagName)) return null;
382
+ await context.scheduler.yieldIfNeeded();
383
+ state.cloned++;
384
+ if (state.cloned % PROGRESS_EVERY === 0) {
385
+ context.reportPhase("clone", state.cloned / state.total);
386
+ }
387
+ if (element instanceof HTMLCanvasElement) {
388
+ const dataUrl = snapshotCanvas(element);
389
+ if (dataUrl) {
390
+ const image = context.ownerDocument.createElement("img");
391
+ image.src = dataUrl;
392
+ copyStyle(element, image, context);
393
+ return image;
394
+ }
395
+ }
396
+ const clone = element.cloneNode(false);
397
+ copyStyle(element, clone, context);
398
+ clonePseudoElements(element, clone, state, context);
399
+ copyInputValue(element, clone);
400
+ for (const child of Array.from(element.childNodes)) {
401
+ const clonedChild = await cloneNode(child, context, state, false);
402
+ if (clonedChild) clone.appendChild(clonedChild);
403
+ }
404
+ return clone;
405
+ }
406
+ async function cloneTree(target, context) {
407
+ const total = target.getElementsByTagName("*").length + 1;
408
+ const state = createCloneState(total);
409
+ context.scheduler.reset();
410
+ const clone = await cloneNode(target, context, state, true);
411
+ context.reportPhase("clone", 1);
412
+ return { clone, pseudoRules: state.pseudoRules };
413
+ }
414
+
415
+ // src/util/concurrency.ts
416
+ async function runPool(items, limit, worker) {
417
+ if (items.length === 0) return;
418
+ let cursor = 0;
419
+ const runner = async () => {
420
+ while (cursor < items.length) {
421
+ const index = cursor++;
422
+ await worker(items[index], index);
423
+ }
424
+ };
425
+ const size = Math.max(1, Math.min(limit, items.length));
426
+ await Promise.all(Array.from({ length: size }, runner));
427
+ }
428
+
429
+ // src/util/css.ts
430
+ var URL_RE = /url\((['"]?)([^'")]+)\1\)/g;
431
+ function extractUrls(cssText) {
432
+ const urls = [];
433
+ let match;
434
+ URL_RE.lastIndex = 0;
435
+ while ((match = URL_RE.exec(cssText)) !== null) {
436
+ const url = match[2];
437
+ if (url && !url.startsWith("data:")) urls.push(url);
438
+ }
439
+ return urls;
440
+ }
441
+ function replaceUrl(cssText, from, to) {
442
+ return cssText.replace(URL_RE, (whole, _quote, url) => {
443
+ return url === from ? `url("${to}")` : whole;
444
+ });
445
+ }
446
+ function resolveUrl(url, baseUrl) {
447
+ if (url.startsWith("data:") || /^[a-z][a-z0-9+.-]*:/i.test(url)) return url;
448
+ try {
449
+ return new URL(url, baseUrl).href;
450
+ } catch {
451
+ return url;
452
+ }
453
+ }
454
+
455
+ // src/util/dataurl.ts
456
+ function blobToDataUrl(blob) {
457
+ return new Promise((resolve, reject) => {
458
+ const reader = new FileReader();
459
+ reader.onloadend = () => resolve(reader.result);
460
+ reader.onerror = () => reject(new Error("Failed to read blob."));
461
+ reader.readAsDataURL(blob);
462
+ });
463
+ }
464
+ function isDataUrl(url) {
465
+ return url.startsWith("data:");
466
+ }
467
+
468
+ // src/embed/fetch.ts
469
+ function delay(ms, signal) {
470
+ return new Promise((resolve, reject) => {
471
+ if (signal?.aborted) {
472
+ reject(createAbortError());
473
+ return;
474
+ }
475
+ const timer = setTimeout(() => {
476
+ signal?.removeEventListener("abort", onAbort);
477
+ resolve();
478
+ }, ms);
479
+ const onAbort = () => {
480
+ clearTimeout(timer);
481
+ reject(createAbortError());
482
+ };
483
+ signal?.addEventListener("abort", onAbort, { once: true });
484
+ });
485
+ }
486
+ async function mainThreadFetch(url, context) {
487
+ const controller = new AbortController();
488
+ const timer = setTimeout(
489
+ () => controller.abort(),
490
+ context.options.fetchTimeoutMs
491
+ );
492
+ const onParentAbort = () => controller.abort();
493
+ context.signal?.addEventListener("abort", onParentAbort, { once: true });
494
+ try {
495
+ const response = await fetch(url, {
496
+ ...context.options.fetchRequestInit,
497
+ signal: controller.signal
498
+ });
499
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
500
+ const blob = await response.blob();
501
+ return await blobToDataUrl(blob);
502
+ } finally {
503
+ clearTimeout(timer);
504
+ context.signal?.removeEventListener("abort", onParentAbort);
505
+ }
506
+ }
507
+ async function workerFetch(url, context) {
508
+ const pool = context.workerPool;
509
+ let timer;
510
+ const timeout = new Promise((_, reject) => {
511
+ timer = setTimeout(
512
+ () => reject(new Error("timeout")),
513
+ context.options.fetchTimeoutMs
514
+ );
515
+ });
516
+ try {
517
+ return await Promise.race([
518
+ pool.fetchAsDataUrl(url, context.options.fetchRequestInit),
519
+ timeout
520
+ ]);
521
+ } finally {
522
+ clearTimeout(timer);
523
+ }
524
+ }
525
+ async function withRetry(url, context) {
526
+ const retries = context.options.fetchRetries;
527
+ let lastError;
528
+ for (let attempt = 0; attempt <= retries; attempt++) {
529
+ context.throwIfAborted();
530
+ try {
531
+ return context.workerPool ? await workerFetch(url, context) : await mainThreadFetch(url, context);
532
+ } catch (error) {
533
+ if (isAbortError(error)) throw error;
534
+ lastError = error;
535
+ if (attempt < retries) await delay(100 * 2 ** attempt, context.signal);
536
+ }
537
+ }
538
+ throw lastError instanceof Error ? lastError : new Error("Resource fetch failed.");
539
+ }
540
+ function fetchResourceAsDataUrl(url, context) {
541
+ if (isDataUrl(url)) return Promise.resolve(url);
542
+ const cached = context.resourceCache.get(url);
543
+ if (cached) return cached;
544
+ const promise = withRetry(url, context);
545
+ context.resourceCache.set(url, promise);
546
+ return promise;
547
+ }
548
+
549
+ // src/embed/images.ts
550
+ var TRANSPARENT_PIXEL = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=";
551
+ function handleFailure(url, error, context) {
552
+ if (context.options.onResourceError) {
553
+ context.options.onResourceError(
554
+ url,
555
+ error instanceof Error ? error : new Error(String(error))
556
+ );
557
+ }
558
+ return context.options.fetchPlaceholder ?? TRANSPARENT_PIXEL;
559
+ }
560
+ async function embedImageElement(image, context) {
561
+ const source = image.currentSrc || image.src;
562
+ if (!source || source.startsWith("data:")) return;
563
+ try {
564
+ image.src = await fetchResourceAsDataUrl(source, context);
565
+ } catch (error) {
566
+ if (isAbortError(error)) throw error;
567
+ image.src = handleFailure(source, error, context);
568
+ }
569
+ image.removeAttribute("srcset");
570
+ image.removeAttribute("loading");
571
+ image.removeAttribute("crossorigin");
572
+ }
573
+ async function embedBackground(element, context) {
574
+ let value = element.style.backgroundImage;
575
+ if (!value || value === "none" || !value.includes("url(")) return;
576
+ const urls = extractUrls(value);
577
+ for (const url of urls) {
578
+ try {
579
+ const dataUrl = await fetchResourceAsDataUrl(url, context);
580
+ value = replaceUrl(value, url, dataUrl);
581
+ } catch (error) {
582
+ if (isAbortError(error)) throw error;
583
+ value = replaceUrl(value, url, handleFailure(url, error, context));
584
+ }
585
+ }
586
+ element.style.backgroundImage = value;
587
+ }
588
+
589
+ // src/embed/fonts.ts
590
+ var FONT_FACE_RULE = 5;
591
+ var FONT_FACE_RE = /@font-face\s*{[^}]*}/gi;
592
+ var URL_FORMAT_RE = /url\((['"]?)([^'")]+)\1\)\s*format\((['"]?)([^'")]+)\3\)/gi;
593
+ async function gatherFontFaces(context) {
594
+ const entries = [];
595
+ const crossOriginHrefs = [];
596
+ for (const sheet of Array.from(context.ownerDocument.styleSheets)) {
597
+ const baseUrl = sheet.href ?? context.ownerDocument.baseURI;
598
+ let rules = null;
599
+ try {
600
+ rules = sheet.cssRules;
601
+ } catch {
602
+ rules = null;
603
+ }
604
+ if (rules) {
605
+ for (const rule of Array.from(rules)) {
606
+ if (rule.type === FONT_FACE_RULE) {
607
+ entries.push({ cssText: rule.cssText, baseUrl });
608
+ }
609
+ }
610
+ } else if (sheet.href) {
611
+ crossOriginHrefs.push(sheet.href);
612
+ }
613
+ }
614
+ for (const href of crossOriginHrefs) {
615
+ context.throwIfAborted();
616
+ try {
617
+ const response = await fetch(href, context.options.fetchRequestInit);
618
+ if (!response.ok) continue;
619
+ const text = await response.text();
620
+ for (const match of text.match(FONT_FACE_RE) ?? []) {
621
+ entries.push({ cssText: match, baseUrl: href });
622
+ }
623
+ } catch {
624
+ }
625
+ }
626
+ return entries;
627
+ }
628
+ function pickPreferredUrl(cssText, urls, preferWoff2) {
629
+ if (preferWoff2) {
630
+ URL_FORMAT_RE.lastIndex = 0;
631
+ let match;
632
+ while ((match = URL_FORMAT_RE.exec(cssText)) !== null) {
633
+ if (/woff2/i.test(match[4] ?? "")) return match[2];
634
+ }
635
+ }
636
+ return urls[0];
637
+ }
638
+ async function embedFontFace(entry, context) {
639
+ const cached = context.fontCache.get(entry.cssText);
640
+ if (cached !== void 0) return cached;
641
+ const urls = extractUrls(entry.cssText);
642
+ let css = entry.cssText;
643
+ if (urls.length > 0) {
644
+ const chosen = pickPreferredUrl(entry.cssText, urls, context.options.fontsPreferWoff2);
645
+ try {
646
+ const dataUrl = await fetchResourceAsDataUrl(
647
+ resolveUrl(chosen, entry.baseUrl),
648
+ context
649
+ );
650
+ css = entry.cssText.replace(/src\s*:[^;]+;?/i, `src:url("${dataUrl}");`);
651
+ } catch {
652
+ }
653
+ }
654
+ context.fontCache.set(entry.cssText, css);
655
+ return css;
656
+ }
657
+ async function embedWebFonts(context) {
658
+ if (context.options.fontsSkip) return "";
659
+ const faces = await gatherFontFaces(context);
660
+ if (faces.length === 0) return "";
661
+ const parts = [];
662
+ await runPool(faces, 4, async (face) => {
663
+ parts.push(await embedFontFace(face, context));
664
+ await context.scheduler.yieldIfNeeded();
665
+ });
666
+ return parts.join("\n");
667
+ }
668
+
669
+ // src/embed/embed.ts
670
+ var EMBED_CONCURRENCY = 6;
671
+ function hasBackgroundImage(element) {
672
+ const value = element.style?.backgroundImage;
673
+ return Boolean(value) && value !== "none" && value.includes("url(");
674
+ }
675
+ async function embedResources(clone, context) {
676
+ context.scheduler.reset();
677
+ const images = Array.from(clone.querySelectorAll("img"));
678
+ if (clone instanceof HTMLImageElement) images.unshift(clone);
679
+ const backgrounds = Array.from(
680
+ clone.querySelectorAll("*")
681
+ ).filter(hasBackgroundImage);
682
+ if (hasBackgroundImage(clone)) backgrounds.unshift(clone);
683
+ const total = images.length + backgrounds.length;
684
+ let done = 0;
685
+ const tick = () => {
686
+ done++;
687
+ if (total > 0) context.reportPhase("embed", done / total);
688
+ };
689
+ const imageTasks = images.map(
690
+ (image) => async () => {
691
+ await embedImageElement(image, context);
692
+ tick();
693
+ await context.scheduler.yieldIfNeeded();
694
+ }
695
+ );
696
+ const backgroundTasks = backgrounds.map(
697
+ (element) => async () => {
698
+ await embedBackground(element, context);
699
+ tick();
700
+ await context.scheduler.yieldIfNeeded();
701
+ }
702
+ );
703
+ const tasks = [...imageTasks, ...backgroundTasks];
704
+ const [fontCss] = await Promise.all([
705
+ embedWebFonts(context),
706
+ runPool(tasks, EMBED_CONCURRENCY, (task) => task())
707
+ ]);
708
+ context.reportPhase("embed", 1);
709
+ return fontCss;
710
+ }
711
+
712
+ // src/serialize/foreign-object-svg.ts
713
+ var SVG_NS = "http://www.w3.org/2000/svg";
714
+ var XHTML_NS2 = "http://www.w3.org/1999/xhtml";
715
+ function buildSvgString(params) {
716
+ const {
717
+ clone,
718
+ doc,
719
+ fullWidth,
720
+ fullHeight,
721
+ pseudoRules,
722
+ fontCss,
723
+ backgroundColor,
724
+ crop
725
+ } = params;
726
+ const outW = crop ? crop.width : fullWidth;
727
+ const outH = crop ? crop.height : fullHeight;
728
+ const svg = doc.createElementNS(SVG_NS, "svg");
729
+ svg.setAttribute("xmlns", SVG_NS);
730
+ svg.setAttribute("width", String(outW));
731
+ svg.setAttribute("height", String(outH));
732
+ svg.setAttribute("viewBox", `0 0 ${outW} ${outH}`);
733
+ if (backgroundColor) {
734
+ const rect = doc.createElementNS(SVG_NS, "rect");
735
+ rect.setAttribute("width", "100%");
736
+ rect.setAttribute("height", "100%");
737
+ rect.setAttribute("fill", backgroundColor);
738
+ svg.appendChild(rect);
739
+ }
740
+ const foreignObject = doc.createElementNS(SVG_NS, "foreignObject");
741
+ foreignObject.setAttribute("x", String(crop ? -crop.x : 0));
742
+ foreignObject.setAttribute("y", String(crop ? -crop.y : 0));
743
+ foreignObject.setAttribute("width", String(fullWidth));
744
+ foreignObject.setAttribute("height", String(fullHeight));
745
+ const container = doc.createElement("div");
746
+ container.setAttribute("xmlns", XHTML_NS2);
747
+ if (fontCss || pseudoRules.length > 0) {
748
+ const style = doc.createElement("style");
749
+ style.textContent = `${fontCss}
750
+ ${pseudoRules.join("")}`;
751
+ container.appendChild(style);
752
+ }
753
+ clone.style.margin = "0";
754
+ container.appendChild(clone);
755
+ foreignObject.appendChild(container);
756
+ svg.appendChild(foreignObject);
757
+ return new XMLSerializer().serializeToString(svg);
758
+ }
759
+ function svgStringToDataUrl(svg) {
760
+ return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
761
+ }
762
+
763
+ // src/raster/svg-to-image.ts
764
+ async function svgToImage(dataUrl, doc) {
765
+ const view = doc.defaultView ?? window;
766
+ const image = new view.Image();
767
+ image.decoding = "sync";
768
+ image.src = dataUrl;
769
+ if (typeof image.decode === "function") {
770
+ await image.decode();
771
+ return image;
772
+ }
773
+ await new Promise((resolve, reject) => {
774
+ image.onload = () => resolve();
775
+ image.onerror = () => reject(new Error("Failed to rasterize SVG."));
776
+ });
777
+ return image;
778
+ }
779
+
780
+ // src/raster/tiler.ts
781
+ var DEFAULT_MAX_TILE = 4096;
782
+ var MAX_BAND_BYTES = 8 * 1024 * 1024;
783
+ function createCanvas(doc, width, height) {
784
+ const canvas = doc.createElement("canvas");
785
+ canvas.width = width;
786
+ canvas.height = height;
787
+ return canvas;
788
+ }
789
+ function clampScale(outWidth, outHeight, scale, maxTile) {
790
+ const limit = Math.min(
791
+ maxTile / Math.max(1, outWidth),
792
+ maxTile / Math.max(1, outHeight)
793
+ );
794
+ return Math.min(scale, limit);
795
+ }
796
+ function drawSvgToCanvas(svgImage, deviceWidth, deviceHeight, context) {
797
+ const canvas = createCanvas(context.ownerDocument, deviceWidth, deviceHeight);
798
+ const ctx = canvas.getContext("2d");
799
+ if (!ctx) throw new Error("2D context unavailable.");
800
+ ctx.drawImage(svgImage, 0, 0, deviceWidth, deviceHeight);
801
+ context.reportPhase("rasterize", 1);
802
+ return canvas;
803
+ }
804
+ async function* rgbaBands(svgImage, geometry2, maxTile, context) {
805
+ const { outWidth, outHeight, deviceWidth, deviceHeight } = geometry2;
806
+ const rx = outWidth / deviceWidth;
807
+ const ry = outHeight / deviceHeight;
808
+ const bandRows = Math.max(
809
+ 1,
810
+ Math.min(maxTile, Math.floor(MAX_BAND_BYTES / (deviceWidth * 4)) || 1)
811
+ );
812
+ const tile = createCanvas(
813
+ context.ownerDocument,
814
+ Math.min(maxTile, deviceWidth),
815
+ bandRows
816
+ );
817
+ const tileCtx = tile.getContext("2d", { willReadFrequently: true });
818
+ if (!tileCtx) throw new Error("2D context unavailable.");
819
+ for (let y = 0; y < deviceHeight; y += bandRows) {
820
+ const bandHeight = Math.min(bandRows, deviceHeight - y);
821
+ const band = new Uint8Array(deviceWidth * bandHeight * 4);
822
+ for (let x = 0; x < deviceWidth; x += maxTile) {
823
+ const tileWidth = Math.min(maxTile, deviceWidth - x);
824
+ if (tile.width !== tileWidth || tile.height !== bandHeight) {
825
+ tile.width = tileWidth;
826
+ tile.height = bandHeight;
827
+ }
828
+ tileCtx.clearRect(0, 0, tileWidth, bandHeight);
829
+ tileCtx.drawImage(
830
+ svgImage,
831
+ x * rx,
832
+ y * ry,
833
+ tileWidth * rx,
834
+ bandHeight * ry,
835
+ 0,
836
+ 0,
837
+ tileWidth,
838
+ bandHeight
839
+ );
840
+ const pixels = tileCtx.getImageData(0, 0, tileWidth, bandHeight).data;
841
+ const rowBytes = tileWidth * 4;
842
+ for (let row = 0; row < bandHeight; row++) {
843
+ band.set(
844
+ pixels.subarray(row * rowBytes, (row + 1) * rowBytes),
845
+ (row * deviceWidth + x) * 4
846
+ );
847
+ }
848
+ await context.scheduler.yieldIfNeeded();
849
+ }
850
+ context.reportPhase("rasterize", Math.min(1, (y + bandHeight) / deviceHeight));
851
+ yield band;
852
+ }
853
+ }
854
+
855
+ // src/util/compress.ts
856
+ function mergeChunks(chunks, total) {
857
+ const merged = new Uint8Array(total);
858
+ let offset = 0;
859
+ for (const part of chunks) {
860
+ merged.set(part, offset);
861
+ offset += part.length;
862
+ }
863
+ return merged;
864
+ }
865
+ function supportsCompressionStream() {
866
+ return typeof CompressionStream !== "undefined";
867
+ }
868
+ async function deflate(source) {
869
+ const cs = new CompressionStream("deflate");
870
+ const writer = cs.writable.getWriter();
871
+ const pump = (async () => {
872
+ for await (const part of source) {
873
+ await writer.write(part);
874
+ }
875
+ await writer.close();
876
+ })();
877
+ const reader = cs.readable.getReader();
878
+ const chunks = [];
879
+ let total = 0;
880
+ for (; ; ) {
881
+ const { value, done } = await reader.read();
882
+ if (done) break;
883
+ chunks.push(value);
884
+ total += value.length;
885
+ }
886
+ await pump;
887
+ return mergeChunks(chunks, total);
888
+ }
889
+
890
+ // src/encode/png-encoder.ts
891
+ var PNG_SIGNATURE = new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]);
892
+ var CRC_TABLE = (() => {
893
+ const table = new Uint32Array(256);
894
+ for (let n = 0; n < 256; n++) {
895
+ let c = n;
896
+ for (let k = 0; k < 8; k++) {
897
+ c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
898
+ }
899
+ table[n] = c >>> 0;
900
+ }
901
+ return table;
902
+ })();
903
+ function crc32(bytes) {
904
+ let crc = 4294967295;
905
+ for (let i = 0; i < bytes.length; i++) {
906
+ crc = CRC_TABLE[(crc ^ bytes[i]) & 255] ^ crc >>> 8;
907
+ }
908
+ return (crc ^ 4294967295) >>> 0;
909
+ }
910
+ function chunk(type, data) {
911
+ const out = new Uint8Array(12 + data.length);
912
+ const view = new DataView(out.buffer);
913
+ view.setUint32(0, data.length);
914
+ for (let i = 0; i < 4; i++) out[4 + i] = type.charCodeAt(i);
915
+ out.set(data, 8);
916
+ view.setUint32(8 + data.length, crc32(out.subarray(4, 8 + data.length)));
917
+ return out;
918
+ }
919
+ function ihdr(width, height) {
920
+ const data = new Uint8Array(13);
921
+ const view = new DataView(data.buffer);
922
+ view.setUint32(0, width);
923
+ view.setUint32(4, height);
924
+ data[8] = 8;
925
+ data[9] = 6;
926
+ return data;
927
+ }
928
+ async function encodePng(width, height, rgbaBands2) {
929
+ const stride = width * 4;
930
+ async function* framedScanlines() {
931
+ for await (const band of rgbaBands2) {
932
+ const rows = band.length / stride;
933
+ const framed = new Uint8Array(band.length + rows);
934
+ for (let r = 0; r < rows; r++) {
935
+ const dst = r * (stride + 1);
936
+ framed[dst] = 0;
937
+ framed.set(band.subarray(r * stride, (r + 1) * stride), dst + 1);
938
+ }
939
+ yield framed;
940
+ }
941
+ }
942
+ const idat = await deflate(framedScanlines());
943
+ return new Blob(
944
+ [
945
+ PNG_SIGNATURE,
946
+ chunk("IHDR", ihdr(width, height)),
947
+ chunk("IDAT", idat),
948
+ chunk("IEND", new Uint8Array(0))
949
+ ],
950
+ { type: "image/png" }
951
+ );
952
+ }
953
+
954
+ // src/encode/pdf.ts
955
+ var encoder = new TextEncoder();
956
+ async function* rgbaToRgb(bands) {
957
+ for await (const rgba of bands) {
958
+ const pixels = rgba.length / 4;
959
+ const rgb = new Uint8Array(pixels * 3);
960
+ for (let i = 0, j = 0; i < rgba.length; i += 4, j += 3) {
961
+ const alpha = rgba[i + 3] / 255;
962
+ const inv = 255 * (1 - alpha);
963
+ rgb[j] = Math.round(rgba[i] * alpha + inv);
964
+ rgb[j + 1] = Math.round(rgba[i + 1] * alpha + inv);
965
+ rgb[j + 2] = Math.round(rgba[i + 2] * alpha + inv);
966
+ }
967
+ yield rgb;
968
+ }
969
+ }
970
+ function fmt(n) {
971
+ return (Math.round(n * 1e3) / 1e3).toString();
972
+ }
973
+ async function encodePdfFromRgbaBands(bands, pixelWidth, pixelHeight, pointWidth, pointHeight) {
974
+ const imageData = await deflate(rgbaToRgb(bands));
975
+ const parts = [];
976
+ const offsets = new Array(6).fill(0);
977
+ let offset = 0;
978
+ const pushBytes = (bytes) => {
979
+ parts.push(bytes);
980
+ offset += bytes.length;
981
+ };
982
+ const pushText = (text) => pushBytes(encoder.encode(text));
983
+ const startObject = (n) => {
984
+ offsets[n] = offset;
985
+ };
986
+ pushText("%PDF-1.4\n");
987
+ pushBytes(new Uint8Array([37, 255, 255, 255, 255, 10]));
988
+ startObject(1);
989
+ pushText("1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n");
990
+ startObject(2);
991
+ pushText("2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n");
992
+ startObject(3);
993
+ pushText(
994
+ `3 0 obj
995
+ << /Type /Page /Parent 2 0 R /MediaBox [0 0 ${fmt(pointWidth)} ${fmt(
996
+ pointHeight
997
+ )}] /Resources << /XObject << /Im0 4 0 R >> >> /Contents 5 0 R >>
998
+ endobj
999
+ `
1000
+ );
1001
+ startObject(4);
1002
+ pushText(
1003
+ `4 0 obj
1004
+ << /Type /XObject /Subtype /Image /Width ${pixelWidth} /Height ${pixelHeight} /ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /FlateDecode /Length ${imageData.length} >>
1005
+ stream
1006
+ `
1007
+ );
1008
+ pushBytes(imageData);
1009
+ pushText("\nendstream\nendobj\n");
1010
+ const content = `q
1011
+ ${fmt(pointWidth)} 0 0 ${fmt(pointHeight)} 0 0 cm
1012
+ /Im0 Do
1013
+ Q
1014
+ `;
1015
+ startObject(5);
1016
+ pushText(
1017
+ `5 0 obj
1018
+ << /Length ${content.length} >>
1019
+ stream
1020
+ ${content}endstream
1021
+ endobj
1022
+ `
1023
+ );
1024
+ const xrefOffset = offset;
1025
+ let xref = `xref
1026
+ 0 6
1027
+ 0000000000 65535 f
1028
+ `;
1029
+ for (let i = 1; i < 6; i++) {
1030
+ xref += `${String(offsets[i]).padStart(10, "0")} 00000 n
1031
+ `;
1032
+ }
1033
+ pushText(xref);
1034
+ pushText(`trailer
1035
+ << /Size 6 /Root 1 0 R >>
1036
+ startxref
1037
+ ${xrefOffset}
1038
+ %%EOF`);
1039
+ return new Blob(parts, { type: "application/pdf" });
1040
+ }
1041
+
1042
+ // src/encode/image.ts
1043
+ var MIME = {
1044
+ png: "image/png",
1045
+ jpeg: "image/jpeg",
1046
+ webp: "image/webp"
1047
+ };
1048
+ function canvasToBlob(canvas, type, quality) {
1049
+ return new Promise((resolve, reject) => {
1050
+ try {
1051
+ canvas.toBlob(
1052
+ (blob) => {
1053
+ if (blob) resolve(blob);
1054
+ else reject(new SmoothScreenshotError("ENCODE_FAILED", "toBlob returned null."));
1055
+ },
1056
+ MIME[type],
1057
+ quality
1058
+ );
1059
+ } catch (error) {
1060
+ reject(
1061
+ new SmoothScreenshotError(
1062
+ "TAINTED_CANVAS",
1063
+ "Canvas is tainted by cross-origin content and cannot be exported.",
1064
+ { cause: error }
1065
+ )
1066
+ );
1067
+ }
1068
+ });
1069
+ }
1070
+ function canvasToDataUrl(canvas, type, quality) {
1071
+ try {
1072
+ return canvas.toDataURL(MIME[type], quality);
1073
+ } catch (error) {
1074
+ throw new SmoothScreenshotError(
1075
+ "TAINTED_CANVAS",
1076
+ "Canvas is tainted by cross-origin content and cannot be exported.",
1077
+ { cause: error }
1078
+ );
1079
+ }
1080
+ }
1081
+
1082
+ // src/capture.ts
1083
+ var CSS_PX_TO_PT = 0.75;
1084
+ function assertElement(target) {
1085
+ if (!target || typeof target.getBoundingClientRect !== "function" || !target.ownerDocument) {
1086
+ throw new SmoothScreenshotError(
1087
+ "INVALID_TARGET",
1088
+ "Capture target must be an HTMLElement attached to a document."
1089
+ );
1090
+ }
1091
+ }
1092
+ async function prepare(target, context, backgroundColor) {
1093
+ context.throwIfAborted();
1094
+ const rect = target.getBoundingClientRect();
1095
+ const fullWidth = context.options.width ?? rect.width;
1096
+ const fullHeight = context.options.height ?? rect.height;
1097
+ if (fullWidth <= 0 || fullHeight <= 0) {
1098
+ throw new SmoothScreenshotError(
1099
+ "EMPTY_TARGET",
1100
+ "Capture target has no rendered size."
1101
+ );
1102
+ }
1103
+ const crop = context.options.crop;
1104
+ const outWidth = crop ? crop.width : fullWidth;
1105
+ const outHeight = crop ? crop.height : fullHeight;
1106
+ const { clone, pseudoRules } = await cloneTree(target, context);
1107
+ const fontCss = await embedResources(clone, context);
1108
+ const svgString = buildSvgString({
1109
+ clone,
1110
+ doc: context.ownerDocument,
1111
+ fullWidth,
1112
+ fullHeight,
1113
+ pseudoRules,
1114
+ fontCss,
1115
+ backgroundColor,
1116
+ crop
1117
+ });
1118
+ return { svgString, fullWidth, fullHeight, outWidth, outHeight };
1119
+ }
1120
+ function withContext(target, options, backgroundColor, run) {
1121
+ assertElement(target);
1122
+ const ownerWindow = target.ownerDocument.defaultView ?? window;
1123
+ const context = createContext(target, resolveOptions(options, ownerWindow));
1124
+ return prepare(target, context, backgroundColor).then((prepared) => run(context, prepared)).finally(() => context.destroy());
1125
+ }
1126
+ function geometry(prepared, scale) {
1127
+ return {
1128
+ outWidth: prepared.outWidth,
1129
+ outHeight: prepared.outHeight,
1130
+ deviceWidth: Math.max(1, Math.round(prepared.outWidth * scale)),
1131
+ deviceHeight: Math.max(1, Math.round(prepared.outHeight * scale))
1132
+ };
1133
+ }
1134
+ function captureSvg(target, options = {}) {
1135
+ return withContext(target, options, options.backgroundColor ?? null, async (context, prepared) => {
1136
+ context.reportPhase("rasterize", 1);
1137
+ context.reportPhase("encode", 1);
1138
+ return svgStringToDataUrl(prepared.svgString);
1139
+ });
1140
+ }
1141
+ async function renderClampedCanvas(context, prepared) {
1142
+ const maxTile = context.options.maxTileSize || DEFAULT_MAX_TILE;
1143
+ const scale = clampScale(
1144
+ prepared.outWidth,
1145
+ prepared.outHeight,
1146
+ context.options.scale,
1147
+ maxTile
1148
+ );
1149
+ const geo = geometry(prepared, scale);
1150
+ const svgImage = await svgToImage(
1151
+ svgStringToDataUrl(prepared.svgString),
1152
+ context.ownerDocument
1153
+ );
1154
+ return drawSvgToCanvas(svgImage, geo.deviceWidth, geo.deviceHeight, context);
1155
+ }
1156
+ function captureCanvas(target, options = {}) {
1157
+ return withContext(
1158
+ target,
1159
+ options,
1160
+ options.backgroundColor ?? null,
1161
+ (context, prepared) => renderClampedCanvas(context, prepared)
1162
+ );
1163
+ }
1164
+ async function encodeImage(context, prepared, type, toResult, toPngBeyondCap) {
1165
+ const maxTile = context.options.maxTileSize || DEFAULT_MAX_TILE;
1166
+ const fullGeo = geometry(prepared, context.options.scale);
1167
+ const exceeds = fullGeo.deviceWidth > maxTile || fullGeo.deviceHeight > maxTile;
1168
+ const svgImage = await svgToImage(
1169
+ svgStringToDataUrl(prepared.svgString),
1170
+ context.ownerDocument
1171
+ );
1172
+ if (type === "png" && exceeds && supportsCompressionStream()) {
1173
+ const blob = await encodePng(
1174
+ fullGeo.deviceWidth,
1175
+ fullGeo.deviceHeight,
1176
+ rgbaBands(svgImage, fullGeo, maxTile, context)
1177
+ );
1178
+ context.reportPhase("encode", 1);
1179
+ return toPngBeyondCap(blob);
1180
+ }
1181
+ let geo = fullGeo;
1182
+ if (exceeds) {
1183
+ const scale = clampScale(
1184
+ prepared.outWidth,
1185
+ prepared.outHeight,
1186
+ context.options.scale,
1187
+ maxTile
1188
+ );
1189
+ geo = geometry(prepared, scale);
1190
+ }
1191
+ const canvas = drawSvgToCanvas(svgImage, geo.deviceWidth, geo.deviceHeight, context);
1192
+ const result = await toResult(canvas);
1193
+ context.reportPhase("encode", 1);
1194
+ return result;
1195
+ }
1196
+ function captureImageDataUrl(target, options, type) {
1197
+ const backgroundColor = options.backgroundColor ?? (type === "jpeg" ? "#ffffff" : null);
1198
+ return withContext(
1199
+ target,
1200
+ options,
1201
+ backgroundColor,
1202
+ (context, prepared) => encodeImage(
1203
+ context,
1204
+ prepared,
1205
+ type,
1206
+ (canvas) => canvasToDataUrl(canvas, type, context.options.quality),
1207
+ (blob) => new Promise((resolve, reject) => {
1208
+ const reader = new FileReader();
1209
+ reader.onloadend = () => resolve(reader.result);
1210
+ reader.onerror = () => reject(new Error("Failed to read PNG blob."));
1211
+ reader.readAsDataURL(blob);
1212
+ })
1213
+ )
1214
+ );
1215
+ }
1216
+ function captureImageBlob(target, options, type) {
1217
+ const backgroundColor = options.backgroundColor ?? (type === "jpeg" ? "#ffffff" : null);
1218
+ return withContext(
1219
+ target,
1220
+ options,
1221
+ backgroundColor,
1222
+ (context, prepared) => encodeImage(
1223
+ context,
1224
+ prepared,
1225
+ type,
1226
+ (canvas) => canvasToBlob(canvas, type, context.options.quality),
1227
+ (blob) => blob
1228
+ )
1229
+ );
1230
+ }
1231
+ function capturePdf(target, options = {}) {
1232
+ if (!supportsCompressionStream()) {
1233
+ return Promise.reject(
1234
+ new SmoothScreenshotError(
1235
+ "ENCODE_FAILED",
1236
+ "PDF export requires CompressionStream (Chrome 80+, Firefox 113+, Safari 16.4+)."
1237
+ )
1238
+ );
1239
+ }
1240
+ const backgroundColor = options.backgroundColor ?? "#ffffff";
1241
+ return withContext(target, options, backgroundColor, async (context, prepared) => {
1242
+ const maxTile = context.options.maxTileSize || DEFAULT_MAX_TILE;
1243
+ const geo = geometry(prepared, context.options.scale);
1244
+ const svgImage = await svgToImage(
1245
+ svgStringToDataUrl(prepared.svgString),
1246
+ context.ownerDocument
1247
+ );
1248
+ const blob = await encodePdfFromRgbaBands(
1249
+ rgbaBands(svgImage, geo, maxTile, context),
1250
+ geo.deviceWidth,
1251
+ geo.deviceHeight,
1252
+ prepared.outWidth * CSS_PX_TO_PT,
1253
+ prepared.outHeight * CSS_PX_TO_PT
1254
+ );
1255
+ context.reportPhase("encode", 1);
1256
+ return blob;
1257
+ });
1258
+ }
1259
+
1260
+ // src/api.ts
1261
+ function toPng(target, options = {}) {
1262
+ return captureImageDataUrl(target, options, "png");
1263
+ }
1264
+ function toJpeg(target, options = {}) {
1265
+ return captureImageDataUrl(target, options, "jpeg");
1266
+ }
1267
+ function toWebp(target, options = {}) {
1268
+ return captureImageDataUrl(target, options, "webp");
1269
+ }
1270
+ function toSvg(target, options = {}) {
1271
+ return captureSvg(target, options);
1272
+ }
1273
+ function toCanvas(target, options = {}) {
1274
+ return captureCanvas(target, options);
1275
+ }
1276
+ function toPdf(target, options = {}) {
1277
+ return capturePdf(target, options);
1278
+ }
1279
+ function toBlob(target, options = {}) {
1280
+ return captureImageBlob(target, options, options.type ?? "png");
1281
+ }
1282
+
1283
+ export { SmoothScreenshotError, toBlob, toCanvas, toJpeg, toPdf, toPng, toSvg, toWebp };
1284
+ //# sourceMappingURL=index.mjs.map
1285
+ //# sourceMappingURL=index.mjs.map