@json-to-office/jto-ops 4.4.2 → 4.4.4

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/index.d.ts CHANGED
@@ -294,6 +294,57 @@ declare function createLibreOfficePptxBatchRasterizer(options?: {
294
294
  cacheDir?: string | null;
295
295
  }): PptxBatchRasterizer;
296
296
 
297
+ /**
298
+ * Where the ink is on a rendered page, row by row.
299
+ *
300
+ * Text geometry says where the words are; it says nothing about a chart, a
301
+ * table rule or a full-bleed image, and a page that holds only those would
302
+ * read as blank. A low-resolution grayscale raster of the same PDF answers
303
+ * the other question — how far down the page anything at all was painted —
304
+ * and that is what the page-fill rule needs. `pdftoppm -gray` writes one
305
+ * PGM (P5) file per page: an ASCII header, then one byte per pixel, so
306
+ * reading it needs no image library.
307
+ */
308
+ /** One page's ink profile, as cached beside its text geometry. */
309
+ interface PdfPageInk {
310
+ /** Raster rows the page was sampled into; row `r` spans `heightPt * r / rows` to `heightPt * (r + 1) / rows`. */
311
+ rows: number;
312
+ /** Row indices, ascending, where at least one pixel is darker than the ink threshold. */
313
+ inked: number[];
314
+ }
315
+ interface Pgm {
316
+ width: number;
317
+ height: number;
318
+ maxValue: number;
319
+ /** Row-major, one value per pixel, `width * height` long. */
320
+ pixels: Uint8Array;
321
+ }
322
+ /** Pixels at or below this value count as ink: dark enough to be text, a rule or a chart bar, not anti-aliasing haze. */
323
+ declare const INK_THRESHOLD = 200;
324
+ /** Dots per inch the fill measurement rasterizes at: an A4 page is ~280 rows, enough to place content to within 3pt. */
325
+ declare const INK_DPI = 24;
326
+ /** Parse a binary PGM (`P5`) as pdftoppm writes it: header tokens, one whitespace byte, then the pixels. */
327
+ declare function parsePgm(buffer: Buffer): Pgm;
328
+ /** Rows carrying at least one pixel at or below `threshold`, ascending. */
329
+ declare function inkedRows(pgm: Pgm, threshold?: number): number[];
330
+ declare function pageInkFromPgm(buffer: Buffer, threshold?: number): PdfPageInk;
331
+ interface ExtractPdfPageInkOptions {
332
+ dpi?: number;
333
+ threshold?: number;
334
+ /** A resolved pdftoppm path, when the caller already found one. */
335
+ binary?: string;
336
+ timeoutMs?: number;
337
+ /** Aborting kills the pdftoppm process and rejects. */
338
+ signal?: AbortSignal;
339
+ }
340
+ /**
341
+ * Rasterize every page of a PDF in grayscale at a low resolution and read
342
+ * back which rows carry ink. One pdftoppm spawn into a private temp dir,
343
+ * removed before returning; the result is small enough to cache with the
344
+ * text geometry.
345
+ */
346
+ declare function extractPdfPageInk(pdfPath: string, options?: ExtractPdfPageInkOptions): Promise<PdfPageInk[]>;
347
+
297
348
  /**
298
349
  * Text geometry from a rendered PDF — the ground truth the quality
299
350
  * estimators are guessing at (#216 follow-up).
@@ -314,6 +365,7 @@ declare function createLibreOfficePptxBatchRasterizer(options?: {
314
365
  * Consumers: the quality ground-truth harness (estimator calibration) today;
315
366
  * a `rendered`-certainty analysis pass tomorrow.
316
367
  */
368
+
317
369
  /** One word as laid out on the page, in PDF points, top-left origin. */
318
370
  interface PdfTextWord {
319
371
  text: string;
@@ -341,6 +393,11 @@ interface PdfTextPage {
341
393
  words: PdfTextWord[];
342
394
  /** Line grouping; empty for plain `-bbox` output, which groups nothing. */
343
395
  lines: PdfTextLine[];
396
+ /**
397
+ * Which raster rows carry ink, when the host sampled the page (see
398
+ * `pdf-page-ink.ts`). Absent, the rules that need it stay silent.
399
+ */
400
+ ink?: PdfPageInk;
344
401
  }
345
402
  /**
346
403
  * Parse `pdftotext -bbox` or `-bbox-layout` output (XHTML with `<page>`,
@@ -412,7 +469,7 @@ declare function extractPdfFonts(pdfPath: string, options?: {
412
469
  * pass over it, memoised per fact, so eight rules cost one search.
413
470
  */
414
471
 
415
- type RenderedRuleId = 'rendered/clip' | 'rendered/spill' | 'rendered/overlap' | 'rendered/text-missing' | 'rendered/font-substituted' | 'rendered/empty-page' | 'rendered/heading-stranded' | 'rendered/paragraph-split';
472
+ type RenderedRuleId = 'rendered/clip' | 'rendered/spill' | 'rendered/overlap' | 'rendered/text-missing' | 'rendered/font-substituted' | 'rendered/empty-page' | 'rendered/heading-stranded' | 'rendered/paragraph-split' | 'rendered/page-underfilled';
416
473
  declare const RENDERED_QUALITY_RULES: QualityRulePack;
417
474
 
418
475
  /**
@@ -946,4 +1003,4 @@ declare function emitDiagnostic(text: string, tone?: DiagnosticTone): void;
946
1003
  */
947
1004
  declare const stderrDiagnosticSink: DiagnosticSink;
948
1005
 
949
- export { type BlockMatrixCase, type BlockMatrixDefinition, type BlockMatrixOptions, type CaseConditions, type DiagnosticSink, type DiagnosticTone, DocxFormatAdapter, FALLBACK_FONTS, type FontStageHandle, type FontStageOptions, type FontStager, FontconfigStager, type FormatAdapter, type FormatName, type GeneratorOptions, type GeneratorResult, type InventoryEntry, type InventoryMatch, MATRIX_IMAGE, MacOSCoreTextStager, type MappingStatus, type MatrixCanvas, type MatrixEdge, type MatrixFont, NoopFontStager, type PdfFontInfo, type PdfTextLine, type PdfTextPage, type PdfTextWord, PptxFormatAdapter, RENDERED_QUALITY_RULES, type RasterizerCacheStats, type RenderedAnalysis, type RenderedAnalysisInput, type RenderedAnalysisSummary, type RenderedMapping, type RenderedRuleId, type RenderedTextEntry, type RequestedFont, type TextOccurrence, VISIBLE_SPILL_PT, WindowsFontStager, analyzeRenderedDocument, assignInventory, authoredTextForMatch, blockCaseDocument, boundaryInvocation, boundarySlotValue, clearRasterizerCache, createAdapter, createLibreOfficePptxBatchRasterizer, createLibreOfficePptxRasterizer, emitDiagnostic, enumerateBlockDefinitions, extractPdfFonts, extractPdfTextGeometry, familyRendered, generateBlockMatrix, getFontStager, getRasterizerCacheStats, normalizeForMatch, overBudgetInvocation, parsePdfFonts, parsePdfTextBbox, pdffontsAvailable, pdftotextAvailable, readingOrder, reportCaseDocument, runWithDiagnosticSink, stderrDiagnosticSink, widestNumber };
1006
+ export { type BlockMatrixCase, type BlockMatrixDefinition, type BlockMatrixOptions, type CaseConditions, type DiagnosticSink, type DiagnosticTone, DocxFormatAdapter, type ExtractPdfPageInkOptions, FALLBACK_FONTS, type FontStageHandle, type FontStageOptions, type FontStager, FontconfigStager, type FormatAdapter, type FormatName, type GeneratorOptions, type GeneratorResult, INK_DPI, INK_THRESHOLD, type InventoryEntry, type InventoryMatch, MATRIX_IMAGE, MacOSCoreTextStager, type MappingStatus, type MatrixCanvas, type MatrixEdge, type MatrixFont, NoopFontStager, type PdfFontInfo, type PdfPageInk, type PdfTextLine, type PdfTextPage, type PdfTextWord, type Pgm, PptxFormatAdapter, RENDERED_QUALITY_RULES, type RasterizerCacheStats, type RenderedAnalysis, type RenderedAnalysisInput, type RenderedAnalysisSummary, type RenderedMapping, type RenderedRuleId, type RenderedTextEntry, type RequestedFont, type TextOccurrence, VISIBLE_SPILL_PT, WindowsFontStager, analyzeRenderedDocument, assignInventory, authoredTextForMatch, blockCaseDocument, boundaryInvocation, boundarySlotValue, clearRasterizerCache, createAdapter, createLibreOfficePptxBatchRasterizer, createLibreOfficePptxRasterizer, emitDiagnostic, enumerateBlockDefinitions, extractPdfFonts, extractPdfPageInk, extractPdfTextGeometry, familyRendered, generateBlockMatrix, getFontStager, getRasterizerCacheStats, inkedRows, normalizeForMatch, overBudgetInvocation, pageInkFromPgm, parsePdfFonts, parsePdfTextBbox, parsePgm, pdffontsAvailable, pdftotextAvailable, readingOrder, reportCaseDocument, runWithDiagnosticSink, stderrDiagnosticSink, widestNumber };
package/dist/index.js CHANGED
@@ -1850,9 +1850,126 @@ async function extractPdfTextGeometry(pdfPath, options = {}) {
1850
1850
  return parsePdfTextBbox(stdout);
1851
1851
  }
1852
1852
 
1853
- // src/pdf-fonts.ts
1853
+ // src/pdf-page-ink.ts
1854
1854
  import { execFile as execFile3 } from "child_process";
1855
- import * as path7 from "path";
1855
+ import { promises as fs6 } from "fs";
1856
+ import os2 from "os";
1857
+ import path7 from "path";
1858
+ var INK_THRESHOLD = 200;
1859
+ var INK_DPI = 24;
1860
+ function parsePgm(buffer) {
1861
+ const header = /^P5\s+(?:#[^\n]*\n\s*)*(\d+)\s+(\d+)\s+(\d+)\s/.exec(
1862
+ buffer.subarray(0, 64).toString("latin1")
1863
+ );
1864
+ if (!header) throw new Error("Not a binary PGM (P5) image.");
1865
+ const width = Number(header[1]);
1866
+ const height = Number(header[2]);
1867
+ const maxValue = Number(header[3]);
1868
+ if (maxValue > 255) throw new Error("16-bit PGM is not supported.");
1869
+ const start = header[0].length;
1870
+ if (buffer.length - start < width * height) {
1871
+ throw new Error("PGM shorter than its declared size.");
1872
+ }
1873
+ const pixels = new Uint8Array(
1874
+ buffer.buffer,
1875
+ buffer.byteOffset + start,
1876
+ width * height
1877
+ );
1878
+ return { width, height, maxValue, pixels };
1879
+ }
1880
+ function inkedRows(pgm, threshold = INK_THRESHOLD) {
1881
+ const rows = [];
1882
+ for (let row = 0; row < pgm.height; row += 1) {
1883
+ const start = row * pgm.width;
1884
+ const end = start + pgm.width;
1885
+ for (let i = start; i < end; i += 1) {
1886
+ if (pgm.pixels[i] <= threshold) {
1887
+ rows.push(row);
1888
+ break;
1889
+ }
1890
+ }
1891
+ }
1892
+ return rows;
1893
+ }
1894
+ function pageInkFromPgm(buffer, threshold) {
1895
+ const pgm = parsePgm(buffer);
1896
+ return { rows: pgm.height, inked: inkedRows(pgm, threshold) };
1897
+ }
1898
+ function pdftoppmCandidates2() {
1899
+ const configured = process.env.PDFTOPPM_PATH?.trim();
1900
+ return [.../* @__PURE__ */ new Set([...configured ? [configured] : [], "pdftoppm"])];
1901
+ }
1902
+ async function run2(binary, args, timeoutMs, signal) {
1903
+ return new Promise((resolve2, reject) => {
1904
+ execFile3(
1905
+ binary,
1906
+ args,
1907
+ {
1908
+ timeout: timeoutMs,
1909
+ maxBuffer: 16 * 1024 * 1024,
1910
+ windowsHide: true,
1911
+ ...signal && { signal }
1912
+ },
1913
+ (error) => error ? reject(error) : resolve2()
1914
+ );
1915
+ });
1916
+ }
1917
+ var pdftoppmPromise2;
1918
+ async function resolvePdftoppm2() {
1919
+ if (!pdftoppmPromise2) {
1920
+ pdftoppmPromise2 = (async () => {
1921
+ for (const candidate of pdftoppmCandidates2()) {
1922
+ try {
1923
+ await run2(candidate, ["-v"], 1e4);
1924
+ return candidate;
1925
+ } catch (error) {
1926
+ const code = error.code;
1927
+ if (code !== "ENOENT" && code !== "EACCES") return candidate;
1928
+ }
1929
+ }
1930
+ throw new Error(
1931
+ `Page ink extraction needs pdftoppm (poppler), which was not found. Install poppler-utils or set PDFTOPPM_PATH (searched: ${pdftoppmCandidates2().join(", ")}).`
1932
+ );
1933
+ })().catch((error) => {
1934
+ pdftoppmPromise2 = void 0;
1935
+ throw error;
1936
+ });
1937
+ }
1938
+ return pdftoppmPromise2;
1939
+ }
1940
+ async function extractPdfPageInk(pdfPath, options = {}) {
1941
+ const binary = options.binary ?? await resolvePdftoppm2();
1942
+ const dpi = options.dpi ?? INK_DPI;
1943
+ const dir = await fs6.mkdtemp(path7.join(os2.tmpdir(), "jto-page-ink-"));
1944
+ try {
1945
+ const prefix = path7.join(dir, "page");
1946
+ await run2(
1947
+ binary,
1948
+ ["-gray", "-r", String(dpi), pdfPath, prefix],
1949
+ options.timeoutMs ?? 6e4,
1950
+ options.signal
1951
+ );
1952
+ const files = (await fs6.readdir(dir)).filter((name) => /^page-\d+\.pgm$/.test(name)).sort(
1953
+ (a, b) => Number(/\d+/.exec(a)?.[0] ?? 0) - Number(/\d+/.exec(b)?.[0] ?? 0)
1954
+ );
1955
+ const pages = [];
1956
+ for (const file of files) {
1957
+ pages.push(
1958
+ pageInkFromPgm(
1959
+ await fs6.readFile(path7.join(dir, file)),
1960
+ options.threshold
1961
+ )
1962
+ );
1963
+ }
1964
+ return pages;
1965
+ } finally {
1966
+ await fs6.rm(dir, { recursive: true, force: true });
1967
+ }
1968
+ }
1969
+
1970
+ // src/pdf-fonts.ts
1971
+ import { execFile as execFile4 } from "child_process";
1972
+ import * as path8 from "path";
1856
1973
  var ROW_PATTERN = /^(\S+)\s+(.+?)\s+(\S+)\s+(yes|no)\s+(yes|no)\s+(yes|no)\s+/;
1857
1974
  function parsePdfFonts(stdout) {
1858
1975
  const fonts = [];
@@ -1887,8 +2004,8 @@ function pdffontsCandidates() {
1887
2004
  const sibling = process.env.PDFTOTEXT_PATH?.trim();
1888
2005
  if (sibling) {
1889
2006
  candidates.push(
1890
- path7.join(
1891
- path7.dirname(sibling),
2007
+ path8.join(
2008
+ path8.dirname(sibling),
1892
2009
  process.platform === "win32" ? "pdffonts.exe" : "pdffonts"
1893
2010
  )
1894
2011
  );
@@ -1896,9 +2013,9 @@ function pdffontsCandidates() {
1896
2013
  candidates.push("pdffonts");
1897
2014
  return [...new Set(candidates)];
1898
2015
  }
1899
- async function run2(binary, args, timeoutMs) {
2016
+ async function run3(binary, args, timeoutMs) {
1900
2017
  return new Promise((resolve2, reject) => {
1901
- execFile3(
2018
+ execFile4(
1902
2019
  binary,
1903
2020
  args,
1904
2021
  { timeout: timeoutMs, maxBuffer: 16 * 1024 * 1024 },
@@ -1915,7 +2032,7 @@ async function resolvePdffonts() {
1915
2032
  pdffontsPromise = (async () => {
1916
2033
  for (const candidate of pdffontsCandidates()) {
1917
2034
  try {
1918
- await run2(candidate, ["-v"], 1e4);
2035
+ await run3(candidate, ["-v"], 1e4);
1919
2036
  return candidate;
1920
2037
  } catch (error) {
1921
2038
  const code = error.code;
@@ -1942,7 +2059,7 @@ async function pdffontsAvailable() {
1942
2059
  }
1943
2060
  async function extractPdfFonts(pdfPath, options = {}) {
1944
2061
  const binary = await resolvePdffonts();
1945
- const stdout = await run2(binary, [pdfPath], options.timeoutMs ?? 3e4);
2062
+ const stdout = await run3(binary, [pdfPath], options.timeoutMs ?? 3e4);
1946
2063
  return parsePdfFonts(stdout);
1947
2064
  }
1948
2065
 
@@ -2016,6 +2133,14 @@ var RENDERED_QUALITY_RULES = {
2016
2133
  category: "integrity",
2017
2134
  defaultSeverity: "info"
2018
2135
  }),
2136
+ renderedRule({
2137
+ id: "rendered/page-underfilled",
2138
+ code: QUALITY_CODES.RENDERED_PAGE_UNDERFILLED,
2139
+ description: "A docx page, other than the first and the last, whose ink stops less than halfway down the body area before the next page begins (`context.fill` is the share used): a section that ends early on its own page, argued in a paragraph where a table or chart was owed.",
2140
+ category: "composition",
2141
+ defaultSeverity: "info",
2142
+ formats: ["docx"]
2143
+ }),
2019
2144
  renderedRule({
2020
2145
  id: "rendered/heading-stranded",
2021
2146
  code: QUALITY_CODES.RENDERED_HEADING_STRANDED,
@@ -2271,6 +2396,24 @@ function inChromeBand(page, box) {
2271
2396
  return box.yMax <= page.heightPt * CHROME_BAND || box.yMin >= page.heightPt * (1 - CHROME_BAND);
2272
2397
  }
2273
2398
  var PAGE_NUMBER_ROW = /^[0-9\s./|-]*$/;
2399
+ var CHROME_ROW_TOLERANCE_PT = 6;
2400
+ function chromeOccurrences(occurrences, pages) {
2401
+ const nearestPerPage = /* @__PURE__ */ new Map();
2402
+ const edgeDistance = (o) => Math.min(o.parts[0].yMin, pages[o.pageIndex].heightPt - o.parts[0].yMax);
2403
+ for (const o of occurrences) {
2404
+ const held = nearestPerPage.get(o.pageIndex);
2405
+ if (!held || edgeDistance(o) < edgeDistance(held)) {
2406
+ nearestPerPage.set(o.pageIndex, o);
2407
+ }
2408
+ }
2409
+ const nearest = [...nearestPerPage.values()];
2410
+ if (nearest.length < 2) return nearest;
2411
+ return nearest.filter(
2412
+ (o) => nearest.some(
2413
+ (other) => other.pageIndex !== o.pageIndex && Math.abs(other.parts[0].yMin - o.parts[0].yMin) <= CHROME_ROW_TOLERANCE_PT
2414
+ )
2415
+ );
2416
+ }
2274
2417
  function assignInventory(pages, inventory) {
2275
2418
  const results = /* @__PURE__ */ new Map();
2276
2419
  const chromeWords = /* @__PURE__ */ new Set();
@@ -2294,9 +2437,10 @@ function assignInventory(pages, inventory) {
2294
2437
  });
2295
2438
  continue;
2296
2439
  }
2297
- const occurrences = findOccurrences(full, segments).filter(
2440
+ const inBand = findOccurrences(full, segments).filter(
2298
2441
  (o) => o.pageIndex === o.endPageIndex && inChromeBand(pages[o.pageIndex], o.parts[0])
2299
2442
  );
2443
+ const occurrences = chromeOccurrences(inBand, pages);
2300
2444
  for (const o of occurrences) claimRow(o.pageIndex, o.parts[0]);
2301
2445
  results.set(entry, {
2302
2446
  entry,
@@ -2387,6 +2531,7 @@ function assignInventory(pages, inventory) {
2387
2531
  }
2388
2532
 
2389
2533
  // src/rendered-analysis.ts
2534
+ var MINIMUM_PAGE_FILL = 0.5;
2390
2535
  function isRenderedGeometryFact(fact) {
2391
2536
  return fact.kind === "rendered/geometry";
2392
2537
  }
@@ -2674,6 +2819,61 @@ function draftRenderedFindings(input) {
2674
2819
  })
2675
2820
  );
2676
2821
  });
2822
+ if (format === "docx") {
2823
+ pages.forEach((page, pageIndex) => {
2824
+ if (!page.ink || pageIndex === 0 || pageIndex === pages.length - 1) {
2825
+ return;
2826
+ }
2827
+ const rowPt = page.heightPt / page.ink.rows;
2828
+ let headerBottom = 0;
2829
+ let footerTop = page.heightPt;
2830
+ page.words.forEach((w, i) => {
2831
+ if (!chromeWords.has(`${pageIndex}:${i}`)) return;
2832
+ if (w.yMax <= page.heightPt * CHROME_BAND) {
2833
+ headerBottom = Math.max(headerBottom, w.yMax);
2834
+ } else if (w.yMin >= page.heightPt * (1 - CHROME_BAND)) {
2835
+ footerTop = Math.min(footerTop, w.yMin);
2836
+ }
2837
+ });
2838
+ const body = page.ink.inked.filter(
2839
+ (row) => row * rowPt > headerBottom + rowPt && (row + 1) * rowPt < footerTop
2840
+ );
2841
+ if (body.length === 0) return;
2842
+ const top = body[0] * rowPt;
2843
+ const bottom = (body[body.length - 1] + 1) * rowPt;
2844
+ const fill = Math.round((bottom - top) / (footerTop - top) * 100) / 100;
2845
+ if (fill >= MINIMUM_PAGE_FILL) return;
2846
+ let owner;
2847
+ for (const match of matches) {
2848
+ if (match.status !== "mapped" || match.entry.repeats) continue;
2849
+ for (const o of match.occurrences) {
2850
+ for (const part of o.parts) {
2851
+ if (part.pageIndex !== pageIndex) continue;
2852
+ if (!owner || part.yMax > owner.yMax) {
2853
+ owner = { path: match.entry.path, yMax: part.yMax };
2854
+ }
2855
+ }
2856
+ }
2857
+ }
2858
+ const section = owner ? /^\/children\/\d+/.exec(owner.path)?.[0] : void 0;
2859
+ findings.push(
2860
+ finding({
2861
+ ruleId: "rendered/page-underfilled",
2862
+ mapping: section ? "mapped" : "unmapped",
2863
+ page: pageIndex + 1,
2864
+ path: section ?? "",
2865
+ message: `Page ${pageIndex + 1} is ${Math.round(fill * 100)}% filled: its content stops ${Math.round(footerTop - bottom)}pt above the footer and the next page begins anyway.`,
2866
+ suggestion: "Let the next section continue on this page, or give the page the table or chart its argument owes; a short section on its own page reads as unfinished.",
2867
+ evidence: {
2868
+ summary: "Share of the body area the page paints, header to footer",
2869
+ expected: `\u2265 ${Math.round(MINIMUM_PAGE_FILL * 100)}%`,
2870
+ actual: fill
2871
+ },
2872
+ context: { fill }
2873
+ })
2874
+ );
2875
+ });
2876
+ }
2677
2877
  if (format === "docx") {
2678
2878
  for (const match of matches) {
2679
2879
  if (match.entry.role !== "heading" || match.status !== "mapped") continue;
@@ -3227,6 +3427,8 @@ export {
3227
3427
  DocxFormatAdapter,
3228
3428
  FALLBACK_FONTS,
3229
3429
  FontconfigStager,
3430
+ INK_DPI,
3431
+ INK_THRESHOLD,
3230
3432
  MATRIX_IMAGE,
3231
3433
  MacOSCoreTextStager,
3232
3434
  NoopFontStager,
@@ -3247,15 +3449,19 @@ export {
3247
3449
  emitDiagnostic,
3248
3450
  enumerateBlockDefinitions,
3249
3451
  extractPdfFonts,
3452
+ extractPdfPageInk,
3250
3453
  extractPdfTextGeometry,
3251
3454
  familyRendered,
3252
3455
  generateBlockMatrix,
3253
3456
  getFontStager,
3254
3457
  getRasterizerCacheStats,
3458
+ inkedRows,
3255
3459
  normalizeForMatch,
3256
3460
  overBudgetInvocation,
3461
+ pageInkFromPgm,
3257
3462
  parsePdfFonts,
3258
3463
  parsePdfTextBbox,
3464
+ parsePgm,
3259
3465
  pdffontsAvailable,
3260
3466
  pdftotextAvailable,
3261
3467
  readingOrder,