@ai-matrx/print 0.8.1 → 0.8.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/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.8.2
4
+
5
+ - **PDF font fallback covers Japanese / Chinese / Korean.** Characters the
6
+ body, emoji and symbol faces cannot draw now fall back to Noto Sans JP
7
+ (SIL OFL, fontsource) — loading only the unicode-range subsets the document
8
+ uses (`fonts.cjk` loader; default jsDelivr). Found on a real conversation
9
+ whose citations carried Japanese source titles. A glyph is announced only
10
+ when no face in the chain can draw it.
11
+ - Consumer action: none — refresh to latest.
12
+
3
13
  ## 0.8.1
4
14
 
5
15
  - **Fixed (Word):** exporting DOCX laid out a whole PDF to number the table of
package/dist/document.cjs CHANGED
@@ -35,12 +35,14 @@ __export(document_exports, {
35
35
  CitationEngine: () => CitationEngine,
36
36
  DEFAULT_FOOTER: () => DEFAULT_FOOTER,
37
37
  DOCUMENT_FORMATS: () => DOCUMENT_FORMATS,
38
+ NOTO_CJK_BASE: () => NOTO_CJK_BASE,
38
39
  NOTO_EMOJI_URL: () => NOTO_EMOJI_URL,
39
40
  NOTO_SYMBOLS_URL: () => NOTO_SYMBOLS_URL,
40
41
  PAGE_SIZES: () => PAGE_SIZES,
41
42
  buildPdfDefinition: () => buildPdfDefinition,
42
43
  countUnplacedWords: () => countUnplacedWords,
43
44
  cslStyleId: () => cslStyleId,
45
+ defaultCjkFontLoader: () => defaultCjkFontLoader,
44
46
  defaultCslFetcher: () => defaultCslFetcher,
45
47
  defaultEmojiFontLoader: () => defaultEmojiFontLoader,
46
48
  defaultImageLoader: () => defaultImageLoader,
@@ -63,6 +65,7 @@ __export(document_exports, {
63
65
  parseLength: () => parseLength,
64
66
  parseLocator: () => parseLocator,
65
67
  parseMathML: () => parseMathML,
68
+ parseUnicodeRangeCss: () => parseUnicodeRangeCss,
66
69
  plainText: () => plainText,
67
70
  prepareDocument: () => prepareDocument,
68
71
  renderBlocksHtml: () => renderBlocksHtml,
@@ -4533,12 +4536,13 @@ async function loadTypography(tree, loaders = {}) {
4533
4536
  const base = await loadFaces(vfs, s.fontSize);
4534
4537
  let needEmoji = false;
4535
4538
  let needSymbols = false;
4539
+ const cjk = /* @__PURE__ */ new Set();
4536
4540
  for (const ch of documentText(tree)) {
4537
4541
  const cp = ch.codePointAt(0);
4538
4542
  if (cp < 32 || cp === 8205 || cp === 65039 || base.body(cp)) continue;
4539
4543
  if (EMOJI_CP.test(ch)) needEmoji = true;
4544
+ else if (isCjk(cp)) cjk.add(cp);
4540
4545
  else needSymbols = true;
4541
- if (needEmoji && needSymbols) break;
4542
4546
  }
4543
4547
  const fallbacks = [];
4544
4548
  if (needEmoji) {
@@ -4549,6 +4553,26 @@ async function loadTypography(tree, loaders = {}) {
4549
4553
  const b = await registerFace("NotoSymbols", "NotoSansMath.woff", loaders.symbols ?? defaultSymbolFontLoader, tree, "symbols");
4550
4554
  if (b) fallbacks.push({ font: "NotoSymbols", bytes: b });
4551
4555
  }
4556
+ if (cjk.size) {
4557
+ let subsets = [];
4558
+ try {
4559
+ subsets = await (loaders.cjk ?? defaultCjkFontLoader)([...cjk]);
4560
+ } catch {
4561
+ subsets = [];
4562
+ }
4563
+ for (const sub of subsets) {
4564
+ const font = `NotoCJK_${sub.name.replace(/[^\w]/g, "_")}`;
4565
+ const b = await registerFace(font, `${sub.name}.woff`, async () => sub.bytes, tree, "Japanese / Chinese");
4566
+ if (b) fallbacks.push({ font, bytes: b });
4567
+ }
4568
+ if (!subsets.length) {
4569
+ tree.notices.push({
4570
+ code: "glyph-substituted",
4571
+ message: "The Japanese / Chinese font could not be loaded, so those characters were left out of the PDF (Word, EPUB and HTML keep them).",
4572
+ remedy: "Check the internet connection (the font comes from cdn.jsdelivr.net) and export again."
4573
+ });
4574
+ }
4575
+ }
4552
4576
  const faces = fallbacks.length ? await loadFaces(vfs, s.fontSize, fallbacks) : base;
4553
4577
  if (!s.hyphenate) return { faces };
4554
4578
  const split = await loadHyphenator(s.lang);
@@ -4571,6 +4595,37 @@ var urlLoader = (url) => async () => {
4571
4595
  };
4572
4596
  var defaultEmojiFontLoader = urlLoader(NOTO_EMOJI_URL);
4573
4597
  var defaultSymbolFontLoader = urlLoader(NOTO_SYMBOLS_URL);
4598
+ var NOTO_CJK_BASE = "https://cdn.jsdelivr.net/npm/@fontsource/noto-sans-jp@5.3.0";
4599
+ function parseUnicodeRangeCss(css) {
4600
+ const out = [];
4601
+ for (const block3 of css.split("@font-face").slice(1)) {
4602
+ const file = /url\(\.\/files\/([\w-]+)\.woff\)/.exec(block3)?.[1];
4603
+ const range = /unicode-range:\s*([^;]+);/.exec(block3)?.[1];
4604
+ if (!file || !range) continue;
4605
+ const ranges = range.split(",").map((r) => {
4606
+ const [a, b] = r.trim().replace(/^U\+/i, "").split("-");
4607
+ const lo = parseInt(a, 16);
4608
+ return [lo, b ? parseInt(b, 16) : lo];
4609
+ });
4610
+ out.push({ file, ranges });
4611
+ }
4612
+ return out;
4613
+ }
4614
+ var defaultCjkFontLoader = async (cps) => {
4615
+ if (typeof fetch !== "function" || cps.length === 0) return [];
4616
+ const res = await fetch(`${NOTO_CJK_BASE}/400.css`);
4617
+ if (!res.ok) return [];
4618
+ const faces = parseUnicodeRangeCss(await res.text()).filter((f) => cps.some((cp) => f.ranges.some(([a, b]) => cp >= a && cp <= b)));
4619
+ const out = [];
4620
+ await Promise.all(
4621
+ faces.map(async (f) => {
4622
+ const r = await fetch(`${NOTO_CJK_BASE}/files/${f.file}.woff`);
4623
+ if (r.ok) out.push({ name: f.file, bytes: new Uint8Array(await r.arrayBuffer()) });
4624
+ })
4625
+ );
4626
+ return out;
4627
+ };
4628
+ var isCjk = (cp) => cp >= 11904 && cp <= 40959 || cp >= 44032 && cp <= 55215 || cp >= 63744 && cp <= 64255 || cp >= 65072 && cp <= 65103 || cp >= 65280 && cp <= 65519 || cp >= 131072 && cp <= 201551;
4574
4629
  var EMOJI_CP = /\p{Extended_Pictographic}|\p{Regional_Indicator}|\u20E3/u;
4575
4630
  function documentText(tree) {
4576
4631
  const s = tree.settings;