@openpresentation/opf-render 0.11.9 → 0.12.2
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 +142 -12
- package/dist/annotations.js +45 -0
- package/dist/chart-options.js +177 -0
- package/dist/charts.js +194 -36
- package/dist/color-fonts.js +32 -0
- package/dist/deck-runtime.js +343 -0
- package/dist/element-define.d.ts +1 -0
- package/dist/element-define.js +5 -0
- package/dist/element.d.ts +86 -0
- package/dist/element.js +521 -0
- package/dist/export-browser.d.ts +26 -0
- package/dist/export-browser.js +228 -0
- package/dist/font-manifest.js +158 -0
- package/dist/font-policy.js +232 -16
- package/dist/fonts.d.ts +30 -0
- package/dist/fonts.js +48 -22
- package/dist/index.d.ts +100 -2
- package/dist/pdf-fonts.js +333 -0
- package/dist/pdf-style.js +429 -0
- package/dist/pdf-subset.js +176 -0
- package/dist/pdf-text.js +338 -0
- package/dist/pdf-vector.js +1249 -0
- package/dist/pdf-writer.js +154 -0
- package/dist/pdf-xml.js +78 -0
- package/dist/player.d.ts +79 -0
- package/dist/player.js +706 -0
- package/dist/preview-fonts-cli.js +15 -0
- package/dist/preview-fonts-node.d.ts +14 -0
- package/dist/preview-fonts-node.js +86 -0
- package/dist/preview-fonts.d.ts +23 -0
- package/dist/preview-fonts.js +78 -0
- package/dist/raster-images.js +19 -6
- package/dist/raster-text.js +468 -0
- package/dist/raster.js +81 -12
- package/dist/script-font-pack.js +19 -1
- package/dist/script-fonts.js +214 -23
- package/dist/sha256.js +47 -0
- package/dist/svg.d.ts +9 -1
- package/dist/svg.js +360 -53
- package/dist/symbol-encodings.js +7 -0
- package/dist/symbol-fonts.js +96 -0
- package/package.json +90 -8
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// RR-28: `opf-preview-fonts <outDir> [--scripts all|Jpan,Arab]` fills a self-hosted font root for <opf-deck>.
|
|
3
|
+
import { copyPreviewFonts } from "./preview-fonts-node.js";
|
|
4
|
+
|
|
5
|
+
const args = process.argv.slice(2);
|
|
6
|
+
const flag = (name) => { const at = args.indexOf(name); return at === -1 ? undefined : args[at + 1]; };
|
|
7
|
+
const outDir = args.find((arg, index) => !arg.startsWith("--") && args[index - 1] !== "--scripts");
|
|
8
|
+
if (!outDir || args.includes("--help")) {
|
|
9
|
+
console.error("Usage: opf-preview-fonts <outDir> [--scripts all|Jpan,Arab,...]\nCopies the pinned, license-verified preview fonts to <outDir> (serve it as <opf-deck fonts=\"/<outDir>/\">).");
|
|
10
|
+
process.exit(outDir ? 0 : 2);
|
|
11
|
+
}
|
|
12
|
+
const scripts = flag("--scripts");
|
|
13
|
+
const result = copyPreviewFonts({ outDir, scripts: scripts === undefined ? false : scripts === "all" ? "all" : scripts.split(",").map((code) => code.trim()).filter(Boolean) });
|
|
14
|
+
console.log(`Preview fonts: ${result.packages} packages, ${result.copied} copied, ${result.verified} already verified, ${(result.bytes / 1048576).toFixed(1)} MiB, all SHA-256 verified.`);
|
|
15
|
+
if (result.missing.length) console.warn(`Not installed (script faces skipped): ${result.missing.join(", ")}`);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface CopyPreviewFontsOptions {
|
|
2
|
+
/** The directory to fill (served as the `fonts` root of `<opf-deck>`). */
|
|
3
|
+
outDir: string;
|
|
4
|
+
/** Noto script faces to include (they are large): `false` (default), `"all"`, or ISO 15924 codes such as `["Jpan", "Arab"]`. */
|
|
5
|
+
scripts?: false | "all" | readonly string[];
|
|
6
|
+
/** Where optional script packages are looked up when they are not installed beside this package. */
|
|
7
|
+
cwd?: string;
|
|
8
|
+
manifest?: unknown;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface CopyPreviewFontsResult { copied: number; verified: number; bytes: number; files: string[]; packages: number; missing: string[] }
|
|
12
|
+
|
|
13
|
+
/** Copy the pinned preview fonts into `outDir`, verifying every file's SHA-256 and writing every license notice to `LICENSES.txt`. */
|
|
14
|
+
export declare function copyPreviewFonts(options: CopyPreviewFontsOptions): CopyPreviewFontsResult;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// RR-28: fill a self-hosted font root (the layout of `preview-fonts.js`) from the installed packages, for hosts that serve
|
|
2
|
+
// `<opf-deck fonts="/opf-fonts/">`. Node only. Every file is copied unmodified and checked against its pinned SHA-256 and
|
|
3
|
+
// every license notice travels with it (LICENSES.txt). Nothing is downloaded: the faces come from this package's `fonts`
|
|
4
|
+
// directory and the installed `@expo-google-fonts/*` packages.
|
|
5
|
+
import crypto from "node:crypto";
|
|
6
|
+
import fs from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { createRequire } from "node:module";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
import { BUNDLED_FONT_MANIFEST } from "./font-manifest.js";
|
|
11
|
+
import { previewFontLayout } from "./preview-fonts.js";
|
|
12
|
+
import { scriptFontPackages } from "./script-font-pack.js";
|
|
13
|
+
|
|
14
|
+
const ALLOWED_LICENSES = ["OFL-1.1", "Apache-2.0", "MIT", "UFL-1.0"];
|
|
15
|
+
const sha256 = (bytes) => crypto.createHash("sha256").update(bytes).digest("hex");
|
|
16
|
+
const safeFile = /^[A-Za-z0-9_./-]+$/;
|
|
17
|
+
const rendererRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
18
|
+
|
|
19
|
+
function within(directory, file) {
|
|
20
|
+
const resolved = path.resolve(directory, file);
|
|
21
|
+
if (file.includes("..") || !safeFile.test(file) || !resolved.startsWith(path.resolve(directory) + path.sep)) throw new Error(`${file} escapes its package directory.`);
|
|
22
|
+
return resolved;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function resolvePackageDirectory(name, from) {
|
|
26
|
+
const searched = [createRequire(path.join(rendererRoot, "package.json")), createRequire(path.join(from ?? process.cwd(), "package.json"))];
|
|
27
|
+
for (const requireFrom of searched) {
|
|
28
|
+
try { return path.dirname(requireFrom.resolve(`${name}/package.json`)); } catch { /* try the next location */ }
|
|
29
|
+
}
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Copy the preview fonts into `outDir`.
|
|
35
|
+
*
|
|
36
|
+
* - `scripts`: which Noto script faces to include (they are large): `false` (default), `"all"`, or ISO 15924 codes such as
|
|
37
|
+
* `["Jpan", "Arab"]`. A deck that draws a script nobody copied still renders; its text falls back to a system font.
|
|
38
|
+
* - `cwd`: where the optional script packages are looked up when they are not installed beside this package.
|
|
39
|
+
*
|
|
40
|
+
* Returns `{ copied, verified, bytes, files, packages, missing }`; `missing` names script packages that are not installed.
|
|
41
|
+
*/
|
|
42
|
+
export function copyPreviewFonts({ outDir, scripts = false, cwd, manifest = BUNDLED_FONT_MANIFEST } = {}) {
|
|
43
|
+
if (typeof outDir !== "string" || !outDir) throw new TypeError("copyPreviewFonts needs outDir.");
|
|
44
|
+
const wantedScripts = scripts === false ? new Set() : new Set(scriptFontPackages(scripts === true ? "all" : scripts).map((pkg) => pkg.name));
|
|
45
|
+
let copied = 0, verified = 0, bytes = 0;
|
|
46
|
+
const notices = [], missing = [], files = [];
|
|
47
|
+
const layout = previewFontLayout(manifest).filter((entry) => entry.kind !== "scripts" || wantedScripts.has(entry.pkg.name));
|
|
48
|
+
for (const { pkg, target } of layout) {
|
|
49
|
+
if (!ALLOWED_LICENSES.includes(pkg.license)) throw new Error(`${pkg.name} license ${pkg.license} is not allowed.`);
|
|
50
|
+
const sourceDirectory = pkg.vendored ? within(rendererRoot, pkg.vendored) : resolvePackageDirectory(pkg.name, cwd);
|
|
51
|
+
if (!sourceDirectory || !fs.existsSync(sourceDirectory)) {
|
|
52
|
+
if (pkg.pack === "scripts") { missing.push(pkg.name); continue; }
|
|
53
|
+
throw new Error(`${pkg.name} is not installed: reinstall @openpresentation/opf-render.`);
|
|
54
|
+
}
|
|
55
|
+
if (!pkg.vendored) {
|
|
56
|
+
const installed = JSON.parse(fs.readFileSync(path.join(sourceDirectory, "package.json"), "utf8"));
|
|
57
|
+
if (installed.version !== pkg.version) throw new Error(`Expected ${pkg.name}@${pkg.version}, found ${installed.version}: install the pinned version.`);
|
|
58
|
+
}
|
|
59
|
+
const destinationDirectory = path.join(outDir, target);
|
|
60
|
+
let licenseText = "";
|
|
61
|
+
for (const item of [{ file: pkg.licenseFile, sha256: pkg.licenseSha256 }, ...(pkg.noticeFile ? [{ file: pkg.noticeFile, sha256: pkg.noticeSha256 }] : [])]) {
|
|
62
|
+
const data = fs.readFileSync(within(sourceDirectory, item.file));
|
|
63
|
+
if (sha256(data) !== item.sha256) throw new Error(`${pkg.name} ${item.file} differs from the manifest hash.`);
|
|
64
|
+
fs.mkdirSync(destinationDirectory, { recursive: true });
|
|
65
|
+
fs.writeFileSync(within(destinationDirectory, item.file), data);
|
|
66
|
+
licenseText += `${licenseText ? "\n\n" : ""}${data.toString("utf8").replace(/\r\n/g, "\n").trim()}`;
|
|
67
|
+
}
|
|
68
|
+
notices.push(`${pkg.name}@${pkg.version} (${pkg.license}), ${pkg.source}\n\n${licenseText}`);
|
|
69
|
+
for (const face of pkg.faces) {
|
|
70
|
+
const destination = within(destinationDirectory, face.file);
|
|
71
|
+
files.push(path.posix.join(target, face.file));
|
|
72
|
+
if (fs.existsSync(destination)) {
|
|
73
|
+
const existing = fs.readFileSync(destination);
|
|
74
|
+
if (sha256(existing) === face.sha256) { verified++; bytes += existing.length; continue; }
|
|
75
|
+
}
|
|
76
|
+
const data = fs.readFileSync(within(sourceDirectory, face.file));
|
|
77
|
+
if (sha256(data) !== face.sha256) throw new Error(`${pkg.name}/${face.file} differs from the manifest hash: reinstall the pinned package.`);
|
|
78
|
+
fs.mkdirSync(path.dirname(destination), { recursive: true });
|
|
79
|
+
fs.writeFileSync(destination, data);
|
|
80
|
+
copied++; bytes += data.length;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
84
|
+
fs.writeFileSync(path.join(outDir, "LICENSES.txt"), `Preview fonts served with <opf-deck>.\nEvery face is a pinned, unmodified copy, verified against its SHA-256.\n\n${notices.join("\n\n---\n\n")}\n`);
|
|
85
|
+
return { copied, verified, bytes, files, packages: layout.length - missing.length, missing };
|
|
86
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { BrowserFontRegistry } from "./fonts-browser.js";
|
|
2
|
+
|
|
3
|
+
/** One face of the self-hosted font root: where it sits under the root, and its pinned hash. */
|
|
4
|
+
export interface PreviewBaseFace { family: string; weight: number; italic: boolean; file: string; sha256: string; license: string }
|
|
5
|
+
|
|
6
|
+
/** Where a manifest package sits under a font root: `kind` is `base`, `lazy` or `scripts`, `target` its root-relative directory. */
|
|
7
|
+
export interface PreviewFontPlacement {
|
|
8
|
+
pkg: { name: string; version: string; pack: string; vendored?: string; license: string; faces: { file: string; family: string; weight: number; italic: boolean; sha256: string }[] };
|
|
9
|
+
kind: "base" | "lazy" | "scripts";
|
|
10
|
+
directory: string;
|
|
11
|
+
target: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export declare function previewFontDirectory(pkg: { name: string; vendored?: string }): string;
|
|
15
|
+
export declare function previewFontLayout(manifest?: unknown, lazyFiles?: readonly string[]): PreviewFontPlacement[];
|
|
16
|
+
export declare function previewBaseFaces(manifest?: unknown): PreviewBaseFace[];
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* A browser font registry over a self-hosted font root: `<root>/base/<package>/<file>`, `<root>/lazy/fonts/<family>/<file>` and
|
|
20
|
+
* `<root>/scripts/<package>/<file>`. It starts with Roboto Regular and loads other faces on demand, hash-verified, only when a
|
|
21
|
+
* deck draws them. Registries are shared per root on a page.
|
|
22
|
+
*/
|
|
23
|
+
export declare function loadPreviewFonts(root: string, options?: { document?: Document; fetch?: typeof fetch; signal?: AbortSignal; registryOptions?: Record<string, unknown> }): Promise<BrowserFontRegistry>;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// RR-28: the self-hosted font root <opf-deck> and the player load from. One directory a host serves from its own origin,
|
|
2
|
+
// laid out as the editor playground and the OpenPresentation sites serve it (nothing is ever requested from a font CDN):
|
|
3
|
+
//
|
|
4
|
+
// <root>/base/<package>/<file> the eager Office and base faces (Roboto Regular loads at startup, the rest on demand)
|
|
5
|
+
// <root>/lazy/fonts/<family>/<file> the vendored faces (Intos for the default Aptos scheme, the open families)
|
|
6
|
+
// <root>/scripts/<package>/<file> the Noto script faces (Japanese, Arabic, ...)
|
|
7
|
+
//
|
|
8
|
+
// `copyPreviewFonts` (the `/fonts-node` side of this layout, `preview-fonts-node.js`) fills it from the installed packages.
|
|
9
|
+
// Every file is verified against its pinned SHA-256 by the registry before use, and only the faces a deck draws are fetched.
|
|
10
|
+
import { BUNDLED_FONT_MANIFEST } from "./font-manifest.js";
|
|
11
|
+
import { lazyFontList, loadBrowserFontRegistry, splitStartupFaces } from "./fonts-browser.js";
|
|
12
|
+
|
|
13
|
+
/** The directory a package's files sit in under its kind: a vendored family's own name, or the npm package name without its scope. */
|
|
14
|
+
export function previewFontDirectory(pkg) {
|
|
15
|
+
return pkg.vendored ? pkg.vendored.replace(/^fonts\//, "") : pkg.name.split("/").pop();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Where every package of the manifest sits under a font root, relative to it: `kind` is `base`, `lazy` or `scripts`, and
|
|
20
|
+
* `target` the package's directory (`lazy` keeps the renderer's package-relative `fonts/<family>` path, which is what
|
|
21
|
+
* `lazyFontsBaseUrl` joins).
|
|
22
|
+
*/
|
|
23
|
+
export function previewFontLayout(manifest = BUNDLED_FONT_MANIFEST, lazyFiles = lazyFontList().map((face) => face.file)) {
|
|
24
|
+
const lazy = new Set(lazyFiles);
|
|
25
|
+
return manifest.packages.map((pkg) => {
|
|
26
|
+
const directory = previewFontDirectory(pkg);
|
|
27
|
+
const isLazy = pkg.faces.length > 0 && pkg.faces.every((face) => lazy.has(`${pkg.vendored}/${face.file}`));
|
|
28
|
+
const kind = pkg.pack === "scripts" ? "scripts" : isLazy ? "lazy" : "base";
|
|
29
|
+
return { pkg, kind, directory, target: kind === "lazy" ? `lazy/${pkg.vendored}` : `${kind}/${directory}` };
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** The eager faces (everything the registry could hold at startup) with their root-relative file and pinned hash. */
|
|
34
|
+
export function previewBaseFaces(manifest = BUNDLED_FONT_MANIFEST) {
|
|
35
|
+
return previewFontLayout(manifest).filter((entry) => entry.kind === "base").flatMap(({ pkg, target }) => pkg.faces.map((face) => ({
|
|
36
|
+
family: face.family, weight: face.weight, italic: face.italic, file: `${target}/${face.file}`, sha256: face.sha256, license: pkg.license,
|
|
37
|
+
})));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const absolute = (root, document) => {
|
|
41
|
+
const text = String(root);
|
|
42
|
+
const base = document?.baseURI ?? globalThis.document?.baseURI ?? globalThis.location?.href;
|
|
43
|
+
const url = base ? new URL(text, base).href : text;
|
|
44
|
+
return url.endsWith("/") ? url : `${url}/`;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const shared = new Map();
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* A browser font registry over a font root. It starts with Roboto Regular (the renderer's fallback family) and loads the rest
|
|
51
|
+
* only when a deck draws it, face by face: vendored faces from `<root>/lazy/`, script faces from `<root>/scripts/`, the other
|
|
52
|
+
* eager faces as the registry's extra lazy faces. One registry is shared by every deck on the page that names the same root.
|
|
53
|
+
*/
|
|
54
|
+
export function loadPreviewFonts(root, options = {}) {
|
|
55
|
+
if (typeof root !== "string" || !root) return Promise.reject(new TypeError("loadPreviewFonts needs the URL of the font root."));
|
|
56
|
+
const base = absolute(root, options.document);
|
|
57
|
+
const shareable = !options.document && !options.fetch && !options.registryOptions;
|
|
58
|
+
if (shareable && shared.has(base)) return shared.get(base);
|
|
59
|
+
const faces = previewBaseFaces();
|
|
60
|
+
const { startup, rest } = splitStartupFaces(faces, { startup: (face) => face.family === "Roboto" && face.weight === 400 && !face.italic });
|
|
61
|
+
const entry = (face) => ({ url: base + face.file, family: face.family, weight: face.weight, italic: face.italic, license: face.license, sha256: face.sha256 });
|
|
62
|
+
const promise = loadBrowserFontRegistry(startup.map(entry), {
|
|
63
|
+
substitutionPolicy: "visual",
|
|
64
|
+
fallbackFamily: "Roboto",
|
|
65
|
+
scriptBaseUrl: `${base}scripts/`,
|
|
66
|
+
lazyFontsBaseUrl: `${base}lazy/`,
|
|
67
|
+
extraLazyFonts: rest.map(entry),
|
|
68
|
+
...(options.document ? { document: options.document } : {}),
|
|
69
|
+
...(options.fetch ? { fetch: options.fetch } : {}),
|
|
70
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
71
|
+
...options.registryOptions,
|
|
72
|
+
});
|
|
73
|
+
if (shareable) {
|
|
74
|
+
shared.set(base, promise);
|
|
75
|
+
promise.catch(() => { if (shared.get(base) === promise) shared.delete(base); });
|
|
76
|
+
}
|
|
77
|
+
return promise;
|
|
78
|
+
}
|
package/dist/raster-images.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import {OPFRenderError} from './svg.js';
|
|
2
2
|
|
|
3
|
-
// Resvg does not decode WebP or apply JPEG EXIF orientation
|
|
4
|
-
// affected embedded image hrefs in the private
|
|
3
|
+
// Resvg does not decode WebP or apply JPEG EXIF orientation, and draws no text of an SVG used as an image (its nested
|
|
4
|
+
// document has no fonts). Replace only affected embedded image hrefs in the private
|
|
5
5
|
// rasterization copy; preserve SVG text, attributes, comments and source bytes.
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
// `nestedSvg(text)` renders an SVG picture that draws text to PNG bytes with the caller's fonts.
|
|
7
|
+
export async function prepareRasterImages(svg, {nestedSvg} = {}) {
|
|
8
|
+
if (!/webp|jpeg|svg\+xml|&#/i.test(svg)) return svg;
|
|
8
9
|
const cache = new Map();
|
|
9
10
|
let output = '', end = 0, imageIndex = 0;
|
|
10
11
|
const tokens = /<!--[\s\S]*?-->|<!\[CDATA\[[\s\S]*?\]\]>|<(?:"[^"]*"|'[^']*'|[^'">])*>/g;
|
|
@@ -16,11 +17,11 @@ export async function prepareRasterImages(svg) {
|
|
|
16
17
|
?? /\sxlink:href\s*=\s*(["'])([\s\S]*?)\1/.exec(tag);
|
|
17
18
|
if (!href) continue;
|
|
18
19
|
const uri = xmlText(href[2]);
|
|
19
|
-
if (!/^data:image\/(?:webp|jpeg)(?:;|,)/i.test(uri)) continue;
|
|
20
|
+
if (!/^data:image\/(?:webp|jpeg|svg\+xml)(?:;|,)/i.test(uri)) continue;
|
|
20
21
|
const trace = /\sdata-opf-path\s*=\s*(["'])([\s\S]*?)\1/.exec(tag);
|
|
21
22
|
const path = trace ? xmlText(trace[2]) : `svg.images.${index}`;
|
|
22
23
|
try {
|
|
23
|
-
if (!cache.has(uri)) cache.set(uri, prepareRasterImage(uri));
|
|
24
|
+
if (!cache.has(uri)) cache.set(uri, /^data:image\/svg\+xml/i.test(uri) ? prepareSvgImage(uri, nestedSvg) : prepareRasterImage(uri));
|
|
24
25
|
const png = await cache.get(uri);
|
|
25
26
|
if (png === null) continue;
|
|
26
27
|
const replacement = href[0].slice(0, href[0].indexOf(href[1]) + 1) + png + href[1];
|
|
@@ -34,6 +35,18 @@ export async function prepareRasterImages(svg) {
|
|
|
34
35
|
return output + svg.slice(end);
|
|
35
36
|
}
|
|
36
37
|
|
|
38
|
+
// An SVG picture that draws text is rendered to PNG here (transparent, up to 4x its size) so its text uses the render's fonts;
|
|
39
|
+
// one without text stays vector for resvg.
|
|
40
|
+
async function prepareSvgImage(uri, nestedSvg) {
|
|
41
|
+
if (!nestedSvg) return null;
|
|
42
|
+
const comma = uri.indexOf(',');
|
|
43
|
+
const header = uri.slice(0, comma), data = uri.slice(comma + 1);
|
|
44
|
+
const text = /;base64$/i.test(header) ? Buffer.from(decodeURIComponent(data).replace(/\s/g, ''), 'base64').toString('utf8') : decodeURIComponent(data);
|
|
45
|
+
if (!/<(?:[A-Za-z_][\w.-]*:)?text[\s>/]/.test(text)) return null;
|
|
46
|
+
// A document resvg cannot read stays as it is: resvg then draws nothing for it, as for any broken picture.
|
|
47
|
+
try { return 'data:image/png;base64,' + Buffer.from(await nestedSvg(text)).toString('base64'); } catch { return null; }
|
|
48
|
+
}
|
|
49
|
+
|
|
37
50
|
async function prepareRasterImage(uri) {
|
|
38
51
|
const comma = uri.indexOf(',');
|
|
39
52
|
const header = uri.slice(0, comma), data = uri.slice(comma + 1);
|