@broadpaper/forme 0.3.0 → 0.4.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/{chunk-2C3MAW2U.js → chunk-QA4APTWM.js} +172 -18
- package/dist/chunk-QA4APTWM.js.map +1 -0
- package/dist/index.browser.d.ts +1 -1
- package/dist/index.browser.js +11 -1
- package/dist/index.browser.js.map +1 -1
- package/dist/index.d.ts +89 -14
- package/dist/index.js +10 -1
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/dist/chunk-2C3MAW2U.js.map +0 -1
|
@@ -902,6 +902,146 @@ var FormeMeasurer = class {
|
|
|
902
902
|
}
|
|
903
903
|
};
|
|
904
904
|
|
|
905
|
+
// src/fonts.ts
|
|
906
|
+
var localReader = null;
|
|
907
|
+
function setFontFileReader(reader) {
|
|
908
|
+
localReader = reader;
|
|
909
|
+
}
|
|
910
|
+
var MAX_FONT_BYTES = 32 * 1024 * 1024;
|
|
911
|
+
var FETCH_TIMEOUT_MS = 15e3;
|
|
912
|
+
var MAX_CACHED = 64;
|
|
913
|
+
var cache = /* @__PURE__ */ new Map();
|
|
914
|
+
async function resolveFonts(input) {
|
|
915
|
+
const fonts = [];
|
|
916
|
+
const warnings = [];
|
|
917
|
+
const seen = /* @__PURE__ */ new Set();
|
|
918
|
+
const add = (font) => {
|
|
919
|
+
const weight = font.weight ?? 400;
|
|
920
|
+
const style = font.style ?? "normal";
|
|
921
|
+
const key = `${font.family.trim().toLowerCase()}|${weight}|${style}`;
|
|
922
|
+
if (seen.has(key)) return;
|
|
923
|
+
seen.add(key);
|
|
924
|
+
fonts.push({ family: font.family, src: font.src, weight, style });
|
|
925
|
+
};
|
|
926
|
+
for (const font of input.fonts ?? []) add(font);
|
|
927
|
+
const faces = await Promise.all((input.theme?.fonts ?? []).map((face) => themeFace(face, input.sources ?? {})));
|
|
928
|
+
for (const face of faces) {
|
|
929
|
+
warnings.push(...face.warnings);
|
|
930
|
+
if (face.font) add(face.font);
|
|
931
|
+
}
|
|
932
|
+
return { fonts, warnings };
|
|
933
|
+
}
|
|
934
|
+
async function themeFace(face, policy) {
|
|
935
|
+
const { weight, range } = weightOf(face.weight);
|
|
936
|
+
const style = face.style === "italic" ? "italic" : "normal";
|
|
937
|
+
const name = `"${face.family}" (${weight}${style === "italic" ? " italic" : ""})`;
|
|
938
|
+
const sources = (Array.isArray(face.src) ? face.src : [face.src]).filter((s) => typeof s === "string" && s.trim() !== "");
|
|
939
|
+
const reasons = [];
|
|
940
|
+
for (const src of sources) {
|
|
941
|
+
const loaded = await load(src.trim(), policy);
|
|
942
|
+
if ("reason" in loaded) {
|
|
943
|
+
reasons.push(loaded.reason);
|
|
944
|
+
continue;
|
|
945
|
+
}
|
|
946
|
+
const warnings = range ? [`The font "${face.family}" declares the weights ${String(face.weight)} from one file; the PDF engine is given it at ${weight} only. Declare a file per weight for the others to be embedded.`] : [];
|
|
947
|
+
return { font: { family: face.family, src: loaded.src, weight, style }, warnings };
|
|
948
|
+
}
|
|
949
|
+
return {
|
|
950
|
+
warnings: [
|
|
951
|
+
`The font ${name} has no TrueType or OpenType source the PDF engine can read, so the PDF draws that text in a standard face and breaks its lines differently from the screen. Add a .ttf or .otf to its src${reasons.length ? ` (${reasons.join("; ")})` : ""}.`
|
|
952
|
+
]
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
function weightOf(value) {
|
|
956
|
+
if (typeof value === "number" && Number.isFinite(value)) return { weight: Math.round(value), range: false };
|
|
957
|
+
const text = String(value ?? "").trim().toLowerCase();
|
|
958
|
+
if (text === "bold") return { weight: 700, range: false };
|
|
959
|
+
const numbers = text.split(/\s+/).map(Number).filter((n) => Number.isFinite(n) && n > 0);
|
|
960
|
+
if (!numbers.length) return { weight: 400, range: false };
|
|
961
|
+
return { weight: Math.round(numbers[0]), range: numbers.length > 1 && numbers[1] !== numbers[0] };
|
|
962
|
+
}
|
|
963
|
+
async function load(src, policy) {
|
|
964
|
+
if (/^data:(font|application)\/(x-)?woff2?[;,]/i.test(src) || /\.woff2?([?#]|$)/i.test(src.startsWith("data:") ? "" : src)) {
|
|
965
|
+
return { reason: `${describe(src)} is WOFF, which only a browser reads` };
|
|
966
|
+
}
|
|
967
|
+
if (src.startsWith("data:")) return fromDataUri(src);
|
|
968
|
+
const remote = /^https?:\/\//i.test(src);
|
|
969
|
+
if (remote) {
|
|
970
|
+
let host;
|
|
971
|
+
try {
|
|
972
|
+
host = new URL(src).hostname;
|
|
973
|
+
} catch {
|
|
974
|
+
return { reason: `${describe(src)} is not a valid address` };
|
|
975
|
+
}
|
|
976
|
+
const allowed = policy.allowedHosts;
|
|
977
|
+
if (allowed && !allowed.includes("*") && !allowed.includes(host)) return { reason: `${host} is not an allowed host` };
|
|
978
|
+
} else if (policy.local === false) {
|
|
979
|
+
return { reason: `${describe(src)} is neither a data: URI nor a web address, and files are not read here` };
|
|
980
|
+
} else if (!localReader) {
|
|
981
|
+
return { src };
|
|
982
|
+
}
|
|
983
|
+
const key = src;
|
|
984
|
+
const hit = cache.get(key);
|
|
985
|
+
if (hit) return hit;
|
|
986
|
+
const pending = (async () => {
|
|
987
|
+
const bytes = remote ? await download(src, Boolean(policy.allowedHosts && !policy.allowedHosts.includes("*"))) : await localReader(src);
|
|
988
|
+
if (bytes.byteLength > MAX_FONT_BYTES) return { reason: `${describe(src)} is larger than ${MAX_FONT_BYTES / 1024 / 1024} MB` };
|
|
989
|
+
const problem = notEmbeddable(bytes, src);
|
|
990
|
+
return problem ? { reason: problem } : { src: `data:font/ttf;base64,${toBase64(bytes)}` };
|
|
991
|
+
})().catch((e) => {
|
|
992
|
+
cache.delete(key);
|
|
993
|
+
return { reason: `${describe(src)} could not be read (${e.message || String(e)})` };
|
|
994
|
+
});
|
|
995
|
+
cache.set(key, pending);
|
|
996
|
+
while (cache.size > MAX_CACHED) cache.delete(cache.keys().next().value);
|
|
997
|
+
return pending;
|
|
998
|
+
}
|
|
999
|
+
async function download(src, restricted) {
|
|
1000
|
+
const signal = typeof AbortSignal !== "undefined" && "timeout" in AbortSignal ? AbortSignal.timeout(FETCH_TIMEOUT_MS) : void 0;
|
|
1001
|
+
const response = await fetch(src, { redirect: restricted ? "error" : "follow", ...signal ? { signal } : {} });
|
|
1002
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
1003
|
+
const length2 = Number(response.headers.get("content-length"));
|
|
1004
|
+
if (length2 > MAX_FONT_BYTES) throw new Error(`larger than ${MAX_FONT_BYTES / 1024 / 1024} MB`);
|
|
1005
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
1006
|
+
}
|
|
1007
|
+
function fromDataUri(src) {
|
|
1008
|
+
const match = /^data:([^,]*),(.*)$/s.exec(src);
|
|
1009
|
+
if (!match) return { reason: "a data: URI with no data" };
|
|
1010
|
+
const base64 = /;base64$/i.test(match[1]);
|
|
1011
|
+
let head;
|
|
1012
|
+
try {
|
|
1013
|
+
head = base64 ? fromBase64(match[2].slice(0, 8)) : Uint8Array.from(decodeURIComponent(match[2].slice(0, 64)), (c) => c.charCodeAt(0) & 255);
|
|
1014
|
+
} catch {
|
|
1015
|
+
return { reason: "a data: URI that does not decode" };
|
|
1016
|
+
}
|
|
1017
|
+
const problem = notEmbeddable(head, "a data: URI");
|
|
1018
|
+
if (problem) return { reason: problem };
|
|
1019
|
+
if (base64) return { src };
|
|
1020
|
+
const bytes = Uint8Array.from(decodeURIComponent(match[2]), (c) => c.charCodeAt(0) & 255);
|
|
1021
|
+
return { src: `data:font/ttf;base64,${toBase64(bytes)}` };
|
|
1022
|
+
}
|
|
1023
|
+
function notEmbeddable(bytes, src) {
|
|
1024
|
+
if (bytes.length < 4) return `${describe(src)} is not a font file`;
|
|
1025
|
+
const tag = String.fromCharCode(bytes[0], bytes[1], bytes[2], bytes[3]);
|
|
1026
|
+
if (bytes[0] === 0 && bytes[1] === 1 && bytes[2] === 0 && bytes[3] === 0 || tag === "OTTO" || tag === "true") return null;
|
|
1027
|
+
if (tag === "wOFF" || tag === "wOF2") return `${describe(src)} is WOFF, which only a browser reads`;
|
|
1028
|
+
if (tag === "ttcf") return `${describe(src)} is a font collection; give the single face instead`;
|
|
1029
|
+
return `${describe(src)} is not a TrueType or OpenType file`;
|
|
1030
|
+
}
|
|
1031
|
+
function describe(src) {
|
|
1032
|
+
return src.startsWith("data:") ? "a data: URI" : src.length > 80 ? `${src.slice(0, 77)}\u2026` : src;
|
|
1033
|
+
}
|
|
1034
|
+
function toBase64(bytes) {
|
|
1035
|
+
let binary = "";
|
|
1036
|
+
for (let i = 0; i < bytes.length; i += 32768) binary += String.fromCharCode(...bytes.subarray(i, i + 32768));
|
|
1037
|
+
return btoa(binary);
|
|
1038
|
+
}
|
|
1039
|
+
function fromBase64(text) {
|
|
1040
|
+
const clean = text.replace(/[^A-Za-z0-9+/]/g, "");
|
|
1041
|
+
const binary = atob(clean.slice(0, clean.length - clean.length % 4));
|
|
1042
|
+
return Uint8Array.from(binary, (c) => c.charCodeAt(0));
|
|
1043
|
+
}
|
|
1044
|
+
|
|
905
1045
|
// src/editor.ts
|
|
906
1046
|
function hash(s) {
|
|
907
1047
|
let h = 2166136261;
|
|
@@ -928,8 +1068,8 @@ var FormeEditorMeasurer = class {
|
|
|
928
1068
|
destroy() {
|
|
929
1069
|
this.cache.clear();
|
|
930
1070
|
}
|
|
931
|
-
key(section, width, mode) {
|
|
932
|
-
return hash(JSON.stringify(section)) + ":" + Math.round(width * 100) + ":" + mode;
|
|
1071
|
+
key(section, width, mode, faces) {
|
|
1072
|
+
return hash(JSON.stringify(section)) + ":" + Math.round(width * 100) + ":" + mode + ":" + faces;
|
|
933
1073
|
}
|
|
934
1074
|
async measure(sections, width) {
|
|
935
1075
|
const opts = this.opts;
|
|
@@ -937,8 +1077,9 @@ var FormeEditorMeasurer = class {
|
|
|
937
1077
|
const out = {};
|
|
938
1078
|
const pending = [];
|
|
939
1079
|
const keys = /* @__PURE__ */ new Map();
|
|
1080
|
+
const faces = facesKey(opts.theme.fonts);
|
|
940
1081
|
for (const section of sections) {
|
|
941
|
-
const k = this.key(section, width, opts.mode);
|
|
1082
|
+
const k = this.key(section, width, opts.mode, faces);
|
|
942
1083
|
const hit = this.cache.get(k);
|
|
943
1084
|
if (hit) out[section.key] = hit;
|
|
944
1085
|
else {
|
|
@@ -947,7 +1088,9 @@ var FormeEditorMeasurer = class {
|
|
|
947
1088
|
}
|
|
948
1089
|
}
|
|
949
1090
|
if (!pending.length) return out;
|
|
950
|
-
const
|
|
1091
|
+
const given = await this.config.fonts;
|
|
1092
|
+
const { fonts, warnings } = await resolveFonts({ fonts: given, theme: opts.theme });
|
|
1093
|
+
for (const w of warnings) this.warn(w);
|
|
951
1094
|
const measurer = new FormeMeasurer({
|
|
952
1095
|
registry: opts.registry,
|
|
953
1096
|
theme: opts.theme,
|
|
@@ -965,14 +1108,21 @@ var FormeEditorMeasurer = class {
|
|
|
965
1108
|
const k = keys.get(sectionKey);
|
|
966
1109
|
if (k) this.cache.set(k, geometry);
|
|
967
1110
|
}
|
|
968
|
-
for (const w of measurer.warnings)
|
|
969
|
-
if (this.seen.has(w)) continue;
|
|
970
|
-
this.seen.add(w);
|
|
971
|
-
this.config.onWarning?.(w);
|
|
972
|
-
}
|
|
1111
|
+
for (const w of measurer.warnings) this.warn(w);
|
|
973
1112
|
return out;
|
|
974
1113
|
}
|
|
1114
|
+
warn(message) {
|
|
1115
|
+
if (this.seen.has(message)) return;
|
|
1116
|
+
this.seen.add(message);
|
|
1117
|
+
this.config.onWarning?.(message);
|
|
1118
|
+
}
|
|
975
1119
|
};
|
|
1120
|
+
function facesKey(faces) {
|
|
1121
|
+
if (!faces?.length) return "";
|
|
1122
|
+
return hash(
|
|
1123
|
+
faces.map((f) => [f.family, f.weight, f.style, ...(Array.isArray(f.src) ? f.src : [f.src]).map((s) => `${s.length}:${s.slice(0, 64)}${s.slice(-64)}`)].join("|")).join(";")
|
|
1124
|
+
);
|
|
1125
|
+
}
|
|
976
1126
|
|
|
977
1127
|
// src/api.ts
|
|
978
1128
|
import {
|
|
@@ -1313,7 +1463,7 @@ function emitPages(input) {
|
|
|
1313
1463
|
const width = page.contentWidth;
|
|
1314
1464
|
const bleed = page.margins.right;
|
|
1315
1465
|
const bodyDependsOnPage = isPageDependent(resolved.body);
|
|
1316
|
-
const
|
|
1466
|
+
const cache2 = /* @__PURE__ */ new Map();
|
|
1317
1467
|
const rendered = /* @__PURE__ */ new Map();
|
|
1318
1468
|
const treeOf = (section, opts) => {
|
|
1319
1469
|
if (bodyDependsOnPage) return renderNode2(section, opts, width);
|
|
@@ -1322,7 +1472,7 @@ function emitPages(input) {
|
|
|
1322
1472
|
};
|
|
1323
1473
|
const translateSection = (section, opts, from, to) => {
|
|
1324
1474
|
const cacheKey = `${section.key}|${from}|${to}`;
|
|
1325
|
-
if (!bodyDependsOnPage &&
|
|
1475
|
+
if (!bodyDependsOnPage && cache2.has(cacheKey)) return cache2.get(cacheKey);
|
|
1326
1476
|
const v = treeOf(section, opts);
|
|
1327
1477
|
if (!v) return null;
|
|
1328
1478
|
const geom = geometry[section.key];
|
|
@@ -1330,7 +1480,7 @@ function emitPages(input) {
|
|
|
1330
1480
|
const t = translate(cropped.node, ctx);
|
|
1331
1481
|
if (!t) return null;
|
|
1332
1482
|
const out = { node: t.node, offset: cropped.offset };
|
|
1333
|
-
if (!bodyDependsOnPage)
|
|
1483
|
+
if (!bodyDependsOnPage) cache2.set(cacheKey, out);
|
|
1334
1484
|
return out;
|
|
1335
1485
|
};
|
|
1336
1486
|
const headers = /* @__PURE__ */ new Map();
|
|
@@ -1487,11 +1637,12 @@ async function buildFormeDocument(opts) {
|
|
|
1487
1637
|
defaultPage: { size: "A4", margin: { top: 54, right: 54, bottom: 54, left: 54 }, wrap: true },
|
|
1488
1638
|
defaultStyle: mapStyle4({ fontSize: pxToPt(theme.typography.baseFontSize ?? 11), fontFamily: theme.typography.bodyFont.split(",")[0].trim().replace(/^['"]|['"]$/g, "") })
|
|
1489
1639
|
};
|
|
1490
|
-
|
|
1640
|
+
const fonts = await resolveFonts({ fonts: opts.fonts, theme, sources: opts.fontSources });
|
|
1641
|
+
if (fonts.fonts.length) doc.fonts = fontEntries(fonts.fonts);
|
|
1491
1642
|
if (opts.tagged) doc.tagged = true;
|
|
1492
1643
|
if (opts.pdfUa) doc.pdfUa = true;
|
|
1493
1644
|
if (opts.pdfA) doc.pdfa = opts.pdfA;
|
|
1494
|
-
const warnings = [...ctx.warnings, ...resolved.warnings.map((w) => w.message)];
|
|
1645
|
+
const warnings = [...ctx.warnings, ...resolved.warnings.map((w) => w.message), ...fonts.warnings];
|
|
1495
1646
|
return { doc, warnings, license };
|
|
1496
1647
|
}
|
|
1497
1648
|
var engineLoader = null;
|
|
@@ -1572,6 +1723,7 @@ async function layoutPaginated(opts) {
|
|
|
1572
1723
|
};
|
|
1573
1724
|
const ctx = { warnings: /* @__PURE__ */ new Set(), suppressPageBreaks: true };
|
|
1574
1725
|
const render = opts.renderer ?? defaultRenderer;
|
|
1726
|
+
const fonts = await resolveFonts({ fonts: opts.fonts, theme, sources: opts.fontSources });
|
|
1575
1727
|
const layoutSections = async (list) => {
|
|
1576
1728
|
if (!list.length) return {};
|
|
1577
1729
|
const measured = [];
|
|
@@ -1599,7 +1751,7 @@ async function layoutPaginated(opts) {
|
|
|
1599
1751
|
defaultPage: { size: "A4", margin: { top: 54, right: 54, bottom: 54, left: 54 }, wrap: true },
|
|
1600
1752
|
defaultStyle: baseStyle(theme)
|
|
1601
1753
|
};
|
|
1602
|
-
if (
|
|
1754
|
+
if (fonts.fonts.length) doc.fonts = fontEntries(fonts.fonts);
|
|
1603
1755
|
const result = await render(doc);
|
|
1604
1756
|
const pages = result.layout?.pages ?? [];
|
|
1605
1757
|
const scale2 = 1 / PX_TO_PT_LOCAL;
|
|
@@ -1663,7 +1815,7 @@ async function layoutPaginated(opts) {
|
|
|
1663
1815
|
defaultPage: { size: "A4", margin: { top: 54, right: 54, bottom: 54, left: 54 }, wrap: true },
|
|
1664
1816
|
defaultStyle: baseStyle(theme)
|
|
1665
1817
|
};
|
|
1666
|
-
if (
|
|
1818
|
+
if (fonts.fonts.length) doc.fonts = fontEntries(fonts.fonts);
|
|
1667
1819
|
if (opts.tagged) doc.tagged = true;
|
|
1668
1820
|
if (opts.pdfUa) doc.pdfUa = true;
|
|
1669
1821
|
if (opts.pdfA) doc.pdfa = opts.pdfA;
|
|
@@ -1673,7 +1825,7 @@ async function layoutPaginated(opts) {
|
|
|
1673
1825
|
pdf: result.pdf,
|
|
1674
1826
|
pages: layout?.pages.length ?? to - from,
|
|
1675
1827
|
layout,
|
|
1676
|
-
warnings: [...ctx.warnings, ...resolved.warnings.map((w) => w.message), ...paged.warnings.map((w) => w.message), ...result.warnings ?? []]
|
|
1828
|
+
warnings: [...ctx.warnings, ...resolved.warnings.map((w) => w.message), ...paged.warnings.map((w) => w.message), ...fonts.warnings, ...result.warnings ?? []]
|
|
1677
1829
|
};
|
|
1678
1830
|
}
|
|
1679
1831
|
};
|
|
@@ -1711,6 +1863,8 @@ export {
|
|
|
1711
1863
|
UNBOUNDED_PAGE_HEIGHT,
|
|
1712
1864
|
geometryFromLayout,
|
|
1713
1865
|
FormeMeasurer,
|
|
1866
|
+
setFontFileReader,
|
|
1867
|
+
resolveFonts,
|
|
1714
1868
|
FormeEditorMeasurer,
|
|
1715
1869
|
buildFormeDocument,
|
|
1716
1870
|
setEngineLoader,
|
|
@@ -1724,4 +1878,4 @@ export {
|
|
|
1724
1878
|
verifyLicense,
|
|
1725
1879
|
parseLicense
|
|
1726
1880
|
};
|
|
1727
|
-
//# sourceMappingURL=chunk-
|
|
1881
|
+
//# sourceMappingURL=chunk-QA4APTWM.js.map
|