satoru-render 0.0.25 → 1.0.1
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 +4 -0
- package/dist/core.d.ts +31 -5
- package/dist/core.js +197 -31
- package/dist/index.d.ts +3 -3
- package/dist/node.d.ts +3 -3
- package/dist/satoru-single.js +0 -0
- package/dist/satoru.js +4727 -2
- package/dist/satoru.wasm +0 -0
- package/dist/web-workers.js +702 -433
- package/dist/workers-parent.js +149 -22
- package/package.json +1 -1
package/dist/workers-parent.js
CHANGED
|
@@ -259,6 +259,59 @@ const DEFAULT_FONT_MAP = {
|
|
|
259
259
|
emoji: "https://cdn.jsdelivr.net/npm/@fontsource/noto-color-emoji/files/noto-color-emoji-emoji-400-normal.woff2",
|
|
260
260
|
"Noto Color Emoji": "https://cdn.jsdelivr.net/npm/@fontsource/noto-color-emoji/files/noto-color-emoji-emoji-400-normal.woff2"
|
|
261
261
|
};
|
|
262
|
+
/**
|
|
263
|
+
* Parse unicode-range string into an array of [start, end] codepoint ranges.
|
|
264
|
+
* e.g. "U+0000-00FF, U+0131" → [[0x0000, 0x00FF], [0x0131, 0x0131]]
|
|
265
|
+
*/
|
|
266
|
+
function parseUnicodeRanges(rangeStr) {
|
|
267
|
+
const ranges = [];
|
|
268
|
+
for (const part of rangeStr.split(",")) {
|
|
269
|
+
const m = part.trim().match(/U\+([0-9A-Fa-f?]+)(?:-([0-9A-Fa-f]+))?/);
|
|
270
|
+
if (!m) continue;
|
|
271
|
+
if (m[1].includes("?")) {
|
|
272
|
+
const lo = parseInt(m[1].replace(/\?/g, "0"), 16);
|
|
273
|
+
const hi = parseInt(m[1].replace(/\?/g, "F"), 16);
|
|
274
|
+
ranges.push([lo, hi]);
|
|
275
|
+
} else {
|
|
276
|
+
const start = parseInt(m[1], 16);
|
|
277
|
+
const end = m[2] ? parseInt(m[2], 16) : start;
|
|
278
|
+
ranges.push([start, end]);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return ranges;
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Check if any character's codepoint falls within the given unicode ranges.
|
|
285
|
+
*/
|
|
286
|
+
function hasMatchingCodepoint(ranges, characters) {
|
|
287
|
+
for (let i = 0; i < characters.length; i++) {
|
|
288
|
+
const cp = characters.codePointAt(i);
|
|
289
|
+
if (cp > 65535) i++;
|
|
290
|
+
for (const [start, end] of ranges) if (cp >= start && cp <= end) return true;
|
|
291
|
+
}
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Parse @font-face blocks from CSS text, extracting src url and unicode-range.
|
|
296
|
+
*/
|
|
297
|
+
function parseFontFaceBlocks(cssText) {
|
|
298
|
+
const blocks = [];
|
|
299
|
+
const blockRegex = /@font-face\s*\{([^}]+)\}/g;
|
|
300
|
+
let blockMatch;
|
|
301
|
+
while ((blockMatch = blockRegex.exec(cssText)) !== null) {
|
|
302
|
+
const body = blockMatch[1];
|
|
303
|
+
const srcMatch = body.match(/src:\s*url\(([^)]+)\)/);
|
|
304
|
+
const rangeMatch = body.match(/unicode-range:\s*([^;]+)/);
|
|
305
|
+
if (srcMatch) {
|
|
306
|
+
const url = srcMatch[1].replace(/['"]/g, "").trim();
|
|
307
|
+
blocks.push({
|
|
308
|
+
url,
|
|
309
|
+
unicodeRange: rangeMatch ? rangeMatch[1].trim() : null
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return blocks;
|
|
314
|
+
}
|
|
262
315
|
async function resolveGoogleFonts(resource, userAgent) {
|
|
263
316
|
if (!resource.url.startsWith("provider:google-fonts")) return null;
|
|
264
317
|
const urlObj = new URL(resource.url);
|
|
@@ -279,8 +332,35 @@ async function resolveGoogleFonts(resource, userAgent) {
|
|
|
279
332
|
try {
|
|
280
333
|
const resp = await fetch(googleFontUrl, { headers });
|
|
281
334
|
if (!resp.ok) return null;
|
|
282
|
-
const
|
|
283
|
-
|
|
335
|
+
const cssText = await resp.text();
|
|
336
|
+
const cssBuf = new TextEncoder().encode(cssText);
|
|
337
|
+
const blocks = parseFontFaceBlocks(cssText);
|
|
338
|
+
if (blocks.length === 0) return cssBuf;
|
|
339
|
+
let filteredBlocks = blocks;
|
|
340
|
+
if (text && text.length > 0) filteredBlocks = blocks.filter((block) => {
|
|
341
|
+
if (!block.unicodeRange) return true;
|
|
342
|
+
return hasMatchingCodepoint(parseUnicodeRanges(block.unicodeRange), text);
|
|
343
|
+
});
|
|
344
|
+
if (filteredBlocks.length === 0) return cssBuf;
|
|
345
|
+
const fontResults = await Promise.all(filteredBlocks.map(async (block) => {
|
|
346
|
+
try {
|
|
347
|
+
const fontResp = await fetch(block.url, { headers });
|
|
348
|
+
if (!fontResp.ok) return null;
|
|
349
|
+
const buf = await fontResp.arrayBuffer();
|
|
350
|
+
return {
|
|
351
|
+
url: block.url,
|
|
352
|
+
data: new Uint8Array(buf)
|
|
353
|
+
};
|
|
354
|
+
} catch {
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
}));
|
|
358
|
+
const fonts = [];
|
|
359
|
+
for (const f of fontResults) if (f !== null) fonts.push(f);
|
|
360
|
+
return {
|
|
361
|
+
css: cssBuf,
|
|
362
|
+
fonts
|
|
363
|
+
};
|
|
284
364
|
} catch {
|
|
285
365
|
return null;
|
|
286
366
|
}
|
|
@@ -289,6 +369,7 @@ var SatoruBase = class {
|
|
|
289
369
|
factory;
|
|
290
370
|
modPromise;
|
|
291
371
|
currentFontMap = DEFAULT_FONT_MAP;
|
|
372
|
+
resourceCache = /* @__PURE__ */ new Map();
|
|
292
373
|
constructor(factory) {
|
|
293
374
|
this.factory = factory;
|
|
294
375
|
}
|
|
@@ -350,7 +431,16 @@ var SatoruBase = class {
|
|
|
350
431
|
webp: 2,
|
|
351
432
|
pdf: 3
|
|
352
433
|
}[options.format ?? "svg"] ?? 0;
|
|
353
|
-
const result = mod.render_from_state(inst, options.width, options.height ?? 0, format,
|
|
434
|
+
const result = mod.render_from_state(inst, options.width, options.height ?? 0, format, {
|
|
435
|
+
svgTextToPaths: options.textToPaths ?? true,
|
|
436
|
+
outputWidth: options.outputWidth ?? 0,
|
|
437
|
+
outputHeight: options.outputHeight ?? 0,
|
|
438
|
+
fitType: options.fit === "cover" ? 1 : options.fit === "fill" ? 2 : 0,
|
|
439
|
+
cropX: options.crop?.x ?? 0,
|
|
440
|
+
cropY: options.crop?.y ?? 0,
|
|
441
|
+
cropWidth: options.crop?.width ?? 0,
|
|
442
|
+
cropHeight: options.crop?.height ?? 0
|
|
443
|
+
});
|
|
354
444
|
if (!result) {
|
|
355
445
|
if (options.format === "svg") return "";
|
|
356
446
|
return new Uint8Array();
|
|
@@ -419,6 +509,40 @@ var SatoruBase = class {
|
|
|
419
509
|
const resolver = options.resolveResource ? async (r) => {
|
|
420
510
|
return await options.resolveResource(r, defaultResolver);
|
|
421
511
|
} : defaultResolver;
|
|
512
|
+
const cachedResolver = async (r) => {
|
|
513
|
+
const cacheKey = `${r.type}:${r.url}:${r.characters ?? ""}`;
|
|
514
|
+
const cached = this.resourceCache.get(cacheKey);
|
|
515
|
+
if (cached) return cached;
|
|
516
|
+
const result = await resolver(r);
|
|
517
|
+
if (result) {
|
|
518
|
+
if (result instanceof Uint8Array) this.resourceCache.set(cacheKey, result);
|
|
519
|
+
else if ("css" in result && "fonts" in result) this.resourceCache.set(cacheKey, result);
|
|
520
|
+
}
|
|
521
|
+
return result;
|
|
522
|
+
};
|
|
523
|
+
const loadResourceData = (r, uint8) => {
|
|
524
|
+
if (r.type === "image" && typeof createImageBitmap !== "undefined" && typeof OffscreenCanvas !== "undefined") return (async () => {
|
|
525
|
+
try {
|
|
526
|
+
const blob = new Blob([uint8.buffer]);
|
|
527
|
+
const bitmap = await createImageBitmap(blob);
|
|
528
|
+
const ctx = new OffscreenCanvas(bitmap.width, bitmap.height).getContext("2d");
|
|
529
|
+
if (ctx) {
|
|
530
|
+
ctx.drawImage(bitmap, 0, 0);
|
|
531
|
+
const imageData = ctx.getImageData(0, 0, bitmap.width, bitmap.height);
|
|
532
|
+
mod.load_image_pixels(instancePtr, r.url, bitmap.width, bitmap.height, new Uint8Array(imageData.data.buffer), r.url);
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
} catch (e) {}
|
|
536
|
+
let typeInt = 1;
|
|
537
|
+
if (r.type === "image") typeInt = 2;
|
|
538
|
+
if (r.type === "css") typeInt = 3;
|
|
539
|
+
mod.add_resource(instancePtr, r.url, typeInt, uint8);
|
|
540
|
+
})();
|
|
541
|
+
let typeInt = 1;
|
|
542
|
+
if (r.type === "image") typeInt = 2;
|
|
543
|
+
if (r.type === "css") typeInt = 3;
|
|
544
|
+
mod.add_resource(instancePtr, r.url, typeInt, uint8);
|
|
545
|
+
};
|
|
422
546
|
const inputHtmls = Array.isArray(value) ? value : [value];
|
|
423
547
|
const processedHtmls = [];
|
|
424
548
|
const resolvedResources = /* @__PURE__ */ new Set();
|
|
@@ -438,25 +562,19 @@ var SatoruBase = class {
|
|
|
438
562
|
if (r.url.startsWith("data:")) return;
|
|
439
563
|
const key = `${r.type}:${r.url}:${r.characters ?? ""}`;
|
|
440
564
|
resolvedResources.add(key);
|
|
441
|
-
const data = await
|
|
442
|
-
if (data
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
const
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
return;
|
|
453
|
-
}
|
|
454
|
-
} catch (e) {}
|
|
455
|
-
let typeInt = 1;
|
|
456
|
-
if (r.type === "image") typeInt = 2;
|
|
457
|
-
if (r.type === "css") typeInt = 3;
|
|
458
|
-
mod.add_resource(instancePtr, r.url, typeInt, uint8);
|
|
565
|
+
const data = await cachedResolver({ ...r });
|
|
566
|
+
if (!data) return;
|
|
567
|
+
if (typeof data === "object" && "css" in data && "fonts" in data) {
|
|
568
|
+
const fontResult = data;
|
|
569
|
+
mod.add_resource(instancePtr, r.url, 1, fontResult.css);
|
|
570
|
+
for (const font of fontResult.fonts) {
|
|
571
|
+
const fontKey = `font:${font.url}:`;
|
|
572
|
+
resolvedResources.add(fontKey);
|
|
573
|
+
mod.add_resource(instancePtr, font.url, 1, font.data);
|
|
574
|
+
}
|
|
575
|
+
return;
|
|
459
576
|
}
|
|
577
|
+
if (data instanceof Uint8Array || ArrayBuffer.isView(data)) await loadResourceData(r, data instanceof Uint8Array ? data : new Uint8Array(data.buffer, data.byteOffset, data.byteLength));
|
|
460
578
|
} catch (e) {
|
|
461
579
|
console.warn(`Failed to resolve resource: ${r.url}`, e);
|
|
462
580
|
}
|
|
@@ -479,7 +597,16 @@ var SatoruBase = class {
|
|
|
479
597
|
png: 1,
|
|
480
598
|
webp: 2,
|
|
481
599
|
pdf: 3
|
|
482
|
-
}[format] ?? 0,
|
|
600
|
+
}[format] ?? 0, {
|
|
601
|
+
svgTextToPaths: options.textToPaths ?? true,
|
|
602
|
+
outputWidth: options.outputWidth ?? 0,
|
|
603
|
+
outputHeight: options.outputHeight ?? 0,
|
|
604
|
+
fitType: options.fit === "cover" ? 1 : options.fit === "fill" ? 2 : 0,
|
|
605
|
+
cropX: options.crop?.x ?? 0,
|
|
606
|
+
cropY: options.crop?.y ?? 0,
|
|
607
|
+
cropWidth: options.crop?.width ?? 0,
|
|
608
|
+
cropHeight: options.crop?.height ?? 0
|
|
609
|
+
});
|
|
483
610
|
if (!result) {
|
|
484
611
|
if (format === "svg") return "";
|
|
485
612
|
return new Uint8Array();
|