reamkit 1.34.0 → 1.35.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.
@@ -34,7 +34,7 @@ function extractPageText(file, page, painted) {
34
34
  };
35
35
  const links = collectLinks(file, page);
36
36
  const marks = collectTextMarkup(file, page);
37
- const shown = withAccentsComposed(withoutRestrikes(runs));
37
+ const shown = withCyrillicSchwa(withAccentsComposed(withoutRestrikes(runs)));
38
38
  if (links.length === 0 && marks.length === 0) return shown;
39
39
  return shown.flatMap((run) => {
40
40
  const link = links.find((l) => inRect(run.x, run.y, l.rect));
@@ -217,6 +217,70 @@ function withAccentsComposed(runs) {
217
217
  return out;
218
218
  }
219
219
  /**
220
+ * The Latin schwa a page wrote for the Cyrillic one, read as the Cyrillic.
221
+ *
222
+ * Arial, Times New Roman and Courier New draw ә (U+04D9) with the glyph of the
223
+ * Latin schwa ə (U+0259), Ә with that of Ə, and a writer that names a glyph
224
+ * after the first character reaching it says Latin: Word, Excel and PowerPoint
225
+ * for Mac export every ә of a Kazakh, Tatar, Bashkir or Azerbaijani document
226
+ * set in those faces as ə (probes of 2026-11-06), and Ream itself did until
227
+ * it named each glyph after the letter it drew. Read as written, "және" is a
228
+ * Cyrillic word with a Latin letter in it, which no language spells.
229
+ *
230
+ * A schwa is read as Cyrillic where its word's other letters are Cyrillic and
231
+ * none Latin, and a word that is nothing but schwas (an alphabet's "Әә") where
232
+ * its line's letters are: Cyrillic, at least three of them, and no Latin besides.
233
+ * An Azerbaijani word in the Latin alphabet — "Azərbaycan" — is left as it is.
234
+ *
235
+ * @param runs The page's runs.
236
+ * @returns The runs, each schwa among Cyrillic letters made Cyrillic.
237
+ */
238
+ function withCyrillicSchwa(runs) {
239
+ if (!runs.some((r) => /[\u0259\u018f]/u.test(r.text))) return [...runs];
240
+ const out = [...runs];
241
+ let from = 0;
242
+ for (let i = 1; i <= runs.length; i++) {
243
+ const prev = runs[i - 1];
244
+ const run = runs[i];
245
+ if (run && continuesLine(prev, run)) continue;
246
+ if (runs.slice(from, i).some((r) => /[\u0259\u018f]/u.test(r.text))) {
247
+ const texts = runs.slice(from, i).map((r) => r.text);
248
+ const line = cyrillicSchwas(texts.join(""));
249
+ let at = 0;
250
+ texts.forEach((t, k) => {
251
+ const mine = line.slice(at, at + t.length);
252
+ at += t.length;
253
+ if (mine !== t) out[from + k] = {
254
+ ...runs[from + k],
255
+ text: mine
256
+ };
257
+ });
258
+ }
259
+ from = i;
260
+ }
261
+ return out;
262
+ }
263
+ /** Whether one run takes up where another left off, on its baseline. */
264
+ function continuesLine(a, b) {
265
+ if (a.angleDeg !== void 0 || b.angleDeg !== void 0) return false;
266
+ const size = b.fontSizePt || a.fontSizePt || 10;
267
+ return Math.abs(a.y - b.y) < size * .3 && Math.abs(b.x - a.endX) < size * .3;
268
+ }
269
+ /** A line's schwas among Cyrillic letters, made Cyrillic (see {@link withCyrillicSchwa}). */
270
+ function cyrillicSchwas(line) {
271
+ const letters = [...line].filter((ch) => /\p{L}/u.test(ch));
272
+ const cyrillic = letters.filter((ch) => /\p{Script=Cyrillic}/u.test(ch)).length;
273
+ const otherLatin = letters.some((ch) => /\p{Script=Latin}/u.test(ch) && !SCHWA.has(ch));
274
+ const lineIsCyrillic = cyrillic >= 3 && !otherLatin;
275
+ return line.replace(/[\p{L}\p{M}]+/gu, (word) => {
276
+ if (!/[\u0259\u018f]/u.test(word)) return word;
277
+ const rest = [...word].filter((ch) => !SCHWA.has(ch) && /\p{L}/u.test(ch));
278
+ return (rest.length > 0 ? rest.some((ch) => /\p{Script=Cyrillic}/u.test(ch)) && !rest.some((ch) => /\p{Script=Latin}/u.test(ch)) : lineIsCyrillic) ? word.replace(/[\u0259\u018f]/gu, (ch) => SCHWA.get(ch)) : word;
279
+ });
280
+ }
281
+ /** The Latin schwas, and the Cyrillic letters a page drawn in Cyrillic meant by them. */
282
+ var SCHWA = /* @__PURE__ */ new Map([["ə", "ә"], ["Ə", "Ә"]]);
283
+ /**
220
284
  * Whether an accent stands over the first letter of a run (the pen taken back
221
285
  * under it) or over the last (the pen taken back to strike it): on one
222
286
  * baseline, give or take the lift an accent over a capital takes, and
@@ -1,4 +1,4 @@
1
- import { fetchFontSet, resolveFamilyKey } from "../core/fonts/remote-fonts.js";
1
+ import { companionOf, fetchFontSet, resolveFamilyKey } from "../core/fonts/remote-fonts.js";
2
2
  import { FontRegistry } from "../core/font/font-registry.js";
3
3
  import "../core/font/index.js";
4
4
  import { signPdf } from "../pdf/signature.js";
@@ -137,7 +137,8 @@ async function buildUnsignedDocxPdf(docx, options) {
137
137
  async function resolveDocxAutoFonts(docx, options = {}) {
138
138
  const fetchOpt = options.fontFetch ? { fetch: options.fontFetch } : {};
139
139
  const keys = options.fontFamily ? /* @__PURE__ */ new Set([resolveFamilyKey(options.fontFamily)]) : detectDocxFamilyKeys(docx);
140
- if (keys.size <= 1) {
140
+ const companions = detectDocxCompanions(docx, keys);
141
+ if (keys.size <= 1 && companions.size === 0) {
141
142
  const family = keys.values().next().value;
142
143
  return { fonts: await fetchFontSet({
143
144
  ...family ? { family } : {},
@@ -154,10 +155,52 @@ async function resolveDocxAutoFonts(docx, options = {}) {
154
155
  registriesByFamily.set(key, FontRegistry.fromBytes(bytes));
155
156
  if (key === "arimo" || !baseBytes) baseBytes = bytes;
156
157
  }
158
+ for (const family of companions) {
159
+ if (registriesByFamily.has(family)) continue;
160
+ try {
161
+ registriesByFamily.set(family, FontRegistry.fromBytes(await fetchFontSet({
162
+ family,
163
+ ...fetchOpt
164
+ })));
165
+ } catch {}
166
+ }
157
167
  return {
158
168
  fonts: baseBytes,
159
169
  registriesByFamily
160
170
  };
161
171
  }
172
+ /**
173
+ * The companions a `.docx`'s substitutes need (see `companionOf`): those whose
174
+ * blocks the document's text holds a character of — a Cambria document in
175
+ * Russian needs Noto Serif for its letters, one in English does not.
176
+ *
177
+ * @param docx The `.docx` bytes.
178
+ * @param families The substitute families it names.
179
+ * @returns The companion families to fetch.
180
+ */
181
+ function detectDocxCompanions(docx, families) {
182
+ const wanted = [...families].flatMap((key) => {
183
+ const companion = companionOf(key);
184
+ return companion ? [companion] : [];
185
+ });
186
+ const out = /* @__PURE__ */ new Set();
187
+ if (wanted.length === 0) return out;
188
+ let xml;
189
+ try {
190
+ const main = OpcPackage.open(docx).getPart(MAIN_DOCUMENT_PART);
191
+ if (!main) return out;
192
+ xml = new TextDecoder("utf-8").decode(main);
193
+ } catch {
194
+ return out;
195
+ }
196
+ for (const c of wanted) for (const ch of xml) {
197
+ const cp = ch.codePointAt(0);
198
+ if (c.ranges.some(([lo, hi]) => cp >= lo && cp <= hi)) {
199
+ out.add(c.family);
200
+ break;
201
+ }
202
+ }
203
+ return out;
204
+ }
162
205
  //#endregion
163
206
  export { convertDocxToPdf, convertDocxToPdfSync, detectDocxFamilyKeys, resolveDocxAutoFonts };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reamkit",
3
- "version": "1.34.0",
3
+ "version": "1.35.0",
4
4
  "description": "Ream — convert DOCX, XLSX, PPTX, PDF and legacy DOC, XLS and PPT to PDF, SVG, HTML, Markdown, DOCX and XLSX, built from scratch on the ECMA-376 and ISO 32000 specifications. Parse once, convert anywhere.",
5
5
  "license": "MIT",
6
6
  "author": "Alexandr Krassavin <info@reamkit.dev>",
@@ -113,5 +113,10 @@
113
113
  },
114
114
  "engines": {
115
115
  "node": ">=20"
116
+ },
117
+ "allowScripts": {
118
+ "esbuild": false,
119
+ "unrs-resolver": false,
120
+ "fsevents": false
116
121
  }
117
122
  }