satoru-render 1.0.10 → 1.0.12
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/cli.js +0 -0
- package/dist/core.d.ts +8 -2
- package/dist/core.js +138 -50
- package/dist/satoru-single.js +0 -0
- package/dist/satoru.js +2 -2
- package/dist/satoru.wasm +0 -0
- package/dist/web-workers.js +369 -250
- package/dist/workers-parent.js +125 -34
- package/package.json +10 -10
package/dist/cli.js
CHANGED
|
File without changes
|
package/dist/core.d.ts
CHANGED
|
@@ -2,8 +2,10 @@ import { LogLevel } from "./log-level.js";
|
|
|
2
2
|
export interface SatoruModule {
|
|
3
3
|
create_instance: () => any;
|
|
4
4
|
destroy_instance: (inst: any) => void;
|
|
5
|
-
collect_resources: (inst: any, html: string, width: number, height: number) => void;
|
|
6
|
-
|
|
5
|
+
collect_resources: (inst: any, html: string, width: number, height: number, mediaType: number) => void;
|
|
6
|
+
get_collect_profile: (inst: any) => string;
|
|
7
|
+
set_collect_profile_enabled: (inst: any, enabled: boolean) => void;
|
|
8
|
+
get_pending_resources: (inst: any) => Uint8Array | null;
|
|
7
9
|
add_resource: (inst: any, url: string, type: number, data: Uint8Array) => void;
|
|
8
10
|
scan_css: (inst: any, css: string) => void;
|
|
9
11
|
load_font: (inst: any, name: string, data: Uint8Array) => void;
|
|
@@ -87,6 +89,10 @@ export interface RenderOptions {
|
|
|
87
89
|
onLog?: (level: LogLevel, message: string) => void;
|
|
88
90
|
/** Media type for CSS @media queries (default: "screen") */
|
|
89
91
|
mediaType?: "screen" | "print";
|
|
92
|
+
/** Collect coarse render timings for diagnostics */
|
|
93
|
+
profile?: boolean;
|
|
94
|
+
/** Receives coarse render timings when profile is enabled */
|
|
95
|
+
onProfile?: (profile: Record<string, number>) => void;
|
|
90
96
|
}
|
|
91
97
|
export declare const DEFAULT_FONT_MAP: Record<string, string>;
|
|
92
98
|
export declare function resolveGoogleFonts(resource: RequiredResource, userAgent?: string): Promise<ResolvedFontResult | Uint8Array | null>;
|
package/dist/core.js
CHANGED
|
@@ -312,6 +312,16 @@ export class SatoruBase {
|
|
|
312
312
|
}
|
|
313
313
|
async render(options) {
|
|
314
314
|
let { format = "svg", value, url, baseUrl } = options;
|
|
315
|
+
const profileEnabled = options.profile === true;
|
|
316
|
+
const profile = {};
|
|
317
|
+
const now = () => typeof performance !== "undefined" && performance.now
|
|
318
|
+
? performance.now()
|
|
319
|
+
: Date.now();
|
|
320
|
+
const addProfile = (name, elapsed) => {
|
|
321
|
+
if (!profileEnabled)
|
|
322
|
+
return;
|
|
323
|
+
profile[name] = (profile[name] ?? 0) + elapsed;
|
|
324
|
+
};
|
|
315
325
|
if (format === "pdf" && Array.isArray(value) && value.length > 1) {
|
|
316
326
|
const pagePdfs = [];
|
|
317
327
|
const SatoruClass = this.constructor;
|
|
@@ -360,6 +370,7 @@ export class SatoruBase {
|
|
|
360
370
|
this.currentFontMap = options.fontMap ?? DEFAULT_FONT_MAP;
|
|
361
371
|
const instancePtr = mod.create_instance();
|
|
362
372
|
mod.set_font_map(instancePtr, this.currentFontMap);
|
|
373
|
+
mod.set_collect_profile_enabled(instancePtr, profileEnabled);
|
|
363
374
|
try {
|
|
364
375
|
const defaultResolver = (r) => this.resolveDefaultResource(r, baseUrl, options.userAgent);
|
|
365
376
|
const resolver = options.resolveResource
|
|
@@ -370,9 +381,13 @@ export class SatoruBase {
|
|
|
370
381
|
const cachedResolver = async (r) => {
|
|
371
382
|
const cacheKey = `${r.type}:${r.url}:${r.characters ?? ""}`;
|
|
372
383
|
const cached = this.resourceCache.get(cacheKey);
|
|
373
|
-
if (cached)
|
|
384
|
+
if (cached) {
|
|
385
|
+
addProfile("resourceCacheHitsCount", 1);
|
|
374
386
|
return cached;
|
|
387
|
+
}
|
|
388
|
+
const resolveStart = now();
|
|
375
389
|
const result = await resolver(r);
|
|
390
|
+
addProfile("resolveResources", now() - resolveStart);
|
|
376
391
|
if (result) {
|
|
377
392
|
if (result instanceof Uint8Array) {
|
|
378
393
|
this.resourceCache.set(cacheKey, result);
|
|
@@ -418,33 +433,6 @@ export class SatoruBase {
|
|
|
418
433
|
mod.scan_css(instancePtr, css);
|
|
419
434
|
}
|
|
420
435
|
const loadResourceData = (r, uint8) => {
|
|
421
|
-
if (r.type === "image" &&
|
|
422
|
-
typeof createImageBitmap !== "undefined" &&
|
|
423
|
-
typeof OffscreenCanvas !== "undefined") {
|
|
424
|
-
return (async () => {
|
|
425
|
-
try {
|
|
426
|
-
const blob = new Blob([uint8.buffer]);
|
|
427
|
-
const bitmap = await createImageBitmap(blob);
|
|
428
|
-
const canvas = new OffscreenCanvas(bitmap.width, bitmap.height);
|
|
429
|
-
const ctx = canvas.getContext("2d");
|
|
430
|
-
if (ctx) {
|
|
431
|
-
ctx.drawImage(bitmap, 0, 0);
|
|
432
|
-
const imageData = ctx.getImageData(0, 0, bitmap.width, bitmap.height);
|
|
433
|
-
mod.load_image_pixels(instancePtr, r.url, bitmap.width, bitmap.height, new Uint8Array(imageData.data.buffer), r.url);
|
|
434
|
-
return;
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
catch (e) {
|
|
438
|
-
// fall through
|
|
439
|
-
}
|
|
440
|
-
let typeInt = 1; // Font
|
|
441
|
-
if (r.type === "image")
|
|
442
|
-
typeInt = 2;
|
|
443
|
-
if (r.type === "css")
|
|
444
|
-
typeInt = 3;
|
|
445
|
-
mod.add_resource(instancePtr, r.url, typeInt, uint8);
|
|
446
|
-
})();
|
|
447
|
-
}
|
|
448
436
|
let typeInt = 1; // Font
|
|
449
437
|
if (r.type === "image")
|
|
450
438
|
typeInt = 2;
|
|
@@ -454,21 +442,89 @@ export class SatoruBase {
|
|
|
454
442
|
};
|
|
455
443
|
const inputHtmls = Array.isArray(value) ? value : [value];
|
|
456
444
|
const processedHtmls = [];
|
|
445
|
+
const utf8Decoder = new TextDecoder();
|
|
446
|
+
const utf8Encoder = new TextEncoder();
|
|
457
447
|
const resolvedResources = new Set();
|
|
458
448
|
for (const rawHtml of inputHtmls) {
|
|
459
449
|
let processedHtml = rawHtml;
|
|
460
450
|
for (let i = 0; i < 10; i++) {
|
|
461
|
-
|
|
462
|
-
const
|
|
463
|
-
|
|
451
|
+
addProfile("collectResourcesCount", 1);
|
|
452
|
+
const collectStart = now();
|
|
453
|
+
mod.collect_resources(instancePtr, processedHtml, width, height, options.mediaType === "print" ? 1 : 0);
|
|
454
|
+
const collectElapsed = now() - collectStart;
|
|
455
|
+
addProfile("collectResources", collectElapsed);
|
|
456
|
+
addProfile(`collectResourcesRound${i + 1}`, collectElapsed);
|
|
457
|
+
if (profileEnabled) {
|
|
458
|
+
try {
|
|
459
|
+
const collectProfile = JSON.parse(mod.get_collect_profile(instancePtr));
|
|
460
|
+
for (const [key, value] of Object.entries(collectProfile)) {
|
|
461
|
+
addProfile(key, value);
|
|
462
|
+
addProfile(`${key}Round${i + 1}`, value);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
catch { }
|
|
466
|
+
}
|
|
467
|
+
const pendingStart = now();
|
|
468
|
+
const binary = mod.get_pending_resources(instancePtr);
|
|
469
|
+
addProfile("getPendingResources", now() - pendingStart);
|
|
470
|
+
if (!binary)
|
|
464
471
|
break;
|
|
465
|
-
const
|
|
472
|
+
const parseStart = now();
|
|
473
|
+
const view = new DataView(binary.buffer, binary.byteOffset, binary.byteLength);
|
|
474
|
+
let offset = 0;
|
|
475
|
+
const count = view.getUint32(offset, true);
|
|
476
|
+
offset += 4;
|
|
477
|
+
const resources = [];
|
|
478
|
+
for (let j = 0; j < count; j++) {
|
|
479
|
+
const typeInt = view.getUint8(offset++);
|
|
480
|
+
const redraw_on_ready = view.getUint8(offset++) !== 0;
|
|
481
|
+
const urlLen = view.getUint32(offset, true);
|
|
482
|
+
offset += 4;
|
|
483
|
+
const url = utf8Decoder.decode(new Uint8Array(binary.buffer, binary.byteOffset + offset, urlLen));
|
|
484
|
+
offset += urlLen;
|
|
485
|
+
const nameLen = view.getUint32(offset, true);
|
|
486
|
+
offset += 4;
|
|
487
|
+
const name = utf8Decoder.decode(new Uint8Array(binary.buffer, binary.byteOffset + offset, nameLen));
|
|
488
|
+
offset += nameLen;
|
|
489
|
+
const charsLen = view.getUint32(offset, true);
|
|
490
|
+
offset += 4;
|
|
491
|
+
const characters = utf8Decoder.decode(new Uint8Array(binary.buffer, binary.byteOffset + offset, charsLen));
|
|
492
|
+
offset += charsLen;
|
|
493
|
+
let type = "font";
|
|
494
|
+
if (typeInt === 2)
|
|
495
|
+
type = "image";
|
|
496
|
+
else if (typeInt === 3)
|
|
497
|
+
type = "css";
|
|
498
|
+
resources.push({ type, url, name, characters, redraw_on_ready });
|
|
499
|
+
}
|
|
466
500
|
const pending = resources.filter((r) => {
|
|
467
501
|
const key = `${r.type}:${r.url}:${r.characters ?? ""}`;
|
|
502
|
+
if (r.type === "font" && resolvedResources.has(`font:${r.url}:`)) {
|
|
503
|
+
return false;
|
|
504
|
+
}
|
|
468
505
|
return !resolvedResources.has(key);
|
|
469
506
|
});
|
|
507
|
+
addProfile("pendingResourcesCount", pending.length);
|
|
508
|
+
addProfile(`pendingResourcesRound${i + 1}Count`, pending.length);
|
|
509
|
+
for (const r of pending) {
|
|
510
|
+
if (r.type === "font") {
|
|
511
|
+
addProfile("pendingFontResourcesCount", 1);
|
|
512
|
+
addProfile(`pendingFontResourcesRound${i + 1}Count`, 1);
|
|
513
|
+
}
|
|
514
|
+
else if (r.type === "css") {
|
|
515
|
+
addProfile("pendingCssResourcesCount", 1);
|
|
516
|
+
addProfile(`pendingCssResourcesRound${i + 1}Count`, 1);
|
|
517
|
+
}
|
|
518
|
+
else if (r.type === "image") {
|
|
519
|
+
addProfile("pendingImageResourcesCount", 1);
|
|
520
|
+
addProfile(`pendingImageResourcesRound${i + 1}Count`, 1);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
addProfile("parsePendingResources", now() - parseStart);
|
|
470
524
|
if (pending.length === 0)
|
|
471
525
|
break;
|
|
526
|
+
addProfile("pendingResourceRoundsCount", 1);
|
|
527
|
+
const loadStart = now();
|
|
472
528
|
await Promise.all(pending.map(async (r) => {
|
|
473
529
|
try {
|
|
474
530
|
if (r.url.startsWith("data:")) {
|
|
@@ -476,6 +532,9 @@ export class SatoruBase {
|
|
|
476
532
|
}
|
|
477
533
|
const key = `${r.type}:${r.url}:${r.characters ?? ""}`;
|
|
478
534
|
resolvedResources.add(key);
|
|
535
|
+
if (r.type === "font") {
|
|
536
|
+
resolvedResources.add(`font:${r.url}:`);
|
|
537
|
+
}
|
|
479
538
|
const data = await cachedResolver({ ...r });
|
|
480
539
|
if (!data)
|
|
481
540
|
return;
|
|
@@ -502,7 +561,7 @@ export class SatoruBase {
|
|
|
502
561
|
? data
|
|
503
562
|
: new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
504
563
|
if (r.type === "css") {
|
|
505
|
-
const cssText =
|
|
564
|
+
const cssText = utf8Decoder.decode(finalUint8);
|
|
506
565
|
const isAbsolute = /^[a-z][a-z0-9+.-]*:/i.test(r.url) ||
|
|
507
566
|
r.url.startsWith("data:");
|
|
508
567
|
let cssBaseUrl = r.url;
|
|
@@ -530,7 +589,7 @@ export class SatoruBase {
|
|
|
530
589
|
return match;
|
|
531
590
|
}
|
|
532
591
|
});
|
|
533
|
-
finalUint8 =
|
|
592
|
+
finalUint8 = utf8Encoder.encode(rewrittenCss);
|
|
534
593
|
}
|
|
535
594
|
}
|
|
536
595
|
await loadResourceData(r, finalUint8);
|
|
@@ -540,7 +599,9 @@ export class SatoruBase {
|
|
|
540
599
|
console.warn(`Failed to resolve resource: ${r.url}`, e);
|
|
541
600
|
}
|
|
542
601
|
}));
|
|
602
|
+
addProfile("loadPendingResources", now() - loadStart);
|
|
543
603
|
}
|
|
604
|
+
const stripStart = now();
|
|
544
605
|
const resolvedUrls = new Set();
|
|
545
606
|
resolvedResources.forEach((key) => {
|
|
546
607
|
const parts = key.split(":");
|
|
@@ -554,6 +615,7 @@ export class SatoruBase {
|
|
|
554
615
|
processedHtml = processedHtml.replace(linkRegex, "");
|
|
555
616
|
});
|
|
556
617
|
processedHtmls.push(processedHtml);
|
|
618
|
+
addProfile("stripResolvedLinks", now() - stripStart);
|
|
557
619
|
}
|
|
558
620
|
const formatMap = {
|
|
559
621
|
svg: 0,
|
|
@@ -561,29 +623,55 @@ export class SatoruBase {
|
|
|
561
623
|
webp: 2,
|
|
562
624
|
pdf: 3,
|
|
563
625
|
};
|
|
564
|
-
const
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
626
|
+
const renderStart = now();
|
|
627
|
+
const result = (processedHtmls.length === 1)
|
|
628
|
+
? mod.render_from_state(instancePtr, width, height, formatMap[format] ?? 0, {
|
|
629
|
+
svgTextToPaths: options.textToPaths ?? true,
|
|
630
|
+
outputWidth: options.outputWidth ?? 0,
|
|
631
|
+
outputHeight: options.outputHeight ?? 0,
|
|
632
|
+
fitType: options.fit === "cover" ? 1 : options.fit === "fill" ? 2 : 0,
|
|
633
|
+
cropX: options.crop?.x ?? 0,
|
|
634
|
+
cropY: options.crop?.y ?? 0,
|
|
635
|
+
cropWidth: options.crop?.width ?? 0,
|
|
636
|
+
cropHeight: options.crop?.height ?? 0,
|
|
637
|
+
fitPositionX: options.fitPosition?.x ?? 0.5,
|
|
638
|
+
fitPositionY: options.fitPosition?.y ?? 0.5,
|
|
639
|
+
backgroundColor: this.parseColor(options.backgroundColor),
|
|
640
|
+
mediaType: options.mediaType === "print" ? 1 : 0,
|
|
641
|
+
})
|
|
642
|
+
: mod.render(instancePtr, processedHtmls, width, height, formatMap[format] ?? 0, {
|
|
643
|
+
svgTextToPaths: options.textToPaths ?? true,
|
|
644
|
+
outputWidth: options.outputWidth ?? 0,
|
|
645
|
+
outputHeight: options.outputHeight ?? 0,
|
|
646
|
+
fitType: options.fit === "cover" ? 1 : options.fit === "fill" ? 2 : 0,
|
|
647
|
+
cropX: options.crop?.x ?? 0,
|
|
648
|
+
cropY: options.crop?.y ?? 0,
|
|
649
|
+
cropWidth: options.crop?.width ?? 0,
|
|
650
|
+
cropHeight: options.crop?.height ?? 0,
|
|
651
|
+
fitPositionX: options.fitPosition?.x ?? 0.5,
|
|
652
|
+
fitPositionY: options.fitPosition?.y ?? 0.5,
|
|
653
|
+
backgroundColor: this.parseColor(options.backgroundColor),
|
|
654
|
+
mediaType: options.mediaType === "print" ? 1 : 0,
|
|
655
|
+
});
|
|
656
|
+
addProfile("wasmRender", now() - renderStart);
|
|
578
657
|
if (!result) {
|
|
658
|
+
options.onProfile?.(profile);
|
|
579
659
|
if (format === "svg")
|
|
580
660
|
return "";
|
|
581
661
|
return new Uint8Array();
|
|
582
662
|
}
|
|
583
663
|
if (format === "svg") {
|
|
584
|
-
|
|
664
|
+
const decodeStart = now();
|
|
665
|
+
const svg = utf8Decoder.decode(result);
|
|
666
|
+
addProfile("decodeResult", now() - decodeStart);
|
|
667
|
+
options.onProfile?.(profile);
|
|
668
|
+
return svg;
|
|
585
669
|
}
|
|
586
|
-
|
|
670
|
+
const copyStart = now();
|
|
671
|
+
const bytes = new Uint8Array(result);
|
|
672
|
+
addProfile("copyResult", now() - copyStart);
|
|
673
|
+
options.onProfile?.(profile);
|
|
674
|
+
return bytes;
|
|
587
675
|
}
|
|
588
676
|
finally {
|
|
589
677
|
mod.destroy_instance(instancePtr);
|
package/dist/satoru-single.js
CHANGED
|
Binary file
|