@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 +59 -2
- package/dist/index.js +215 -9
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
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-
|
|
1853
|
+
// src/pdf-page-ink.ts
|
|
1854
1854
|
import { execFile as execFile3 } from "child_process";
|
|
1855
|
-
import
|
|
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
|
-
|
|
1891
|
-
|
|
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
|
|
2016
|
+
async function run3(binary, args, timeoutMs) {
|
|
1900
2017
|
return new Promise((resolve2, reject) => {
|
|
1901
|
-
|
|
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
|
|
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
|
|
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
|
|
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,
|