@json-to-office/jto-ops 4.1.0 → 4.2.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/index.d.ts +53 -9
- package/dist/index.js +186 -47
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { FontRuntimeOpts, ServicesConfig, GenerationWarning, RendererStatus, PptxBatchRasterizer, PptxRasterizer, ResolvedFont } from '@json-to-office/shared';
|
|
2
|
-
import { QualityProfile, QualityPolicy, PreparedDocument, QualityAnalysis, QualityDiagnostic } from '@json-to-office/quality';
|
|
2
|
+
import { QualityProfile, QualityPolicy, PreparedDocument, QualityAnalysis, QualityRulePack, QualityDiagnostic, QualityAnalyzeOptions } from '@json-to-office/quality';
|
|
3
3
|
|
|
4
4
|
type FormatName = 'docx' | 'pptx';
|
|
5
5
|
interface GeneratorOptions {
|
|
@@ -401,6 +401,20 @@ declare function extractPdfFonts(pdfPath: string, options?: {
|
|
|
401
401
|
timeoutMs?: number;
|
|
402
402
|
}): Promise<PdfFontInfo[]>;
|
|
403
403
|
|
|
404
|
+
/**
|
|
405
|
+
* The rendered pass as a quality rule pack (#344).
|
|
406
|
+
*
|
|
407
|
+
* `draftRenderedFindings` measures; these rules are how the measurement
|
|
408
|
+
* enters the quality contract. Each rule owns one code, one description the
|
|
409
|
+
* design guide prints, and the defaults a profile or policy can move. All of
|
|
410
|
+
* them read a single `rendered/geometry` fact — the PDF's word boxes, its
|
|
411
|
+
* embedded fonts and the document's text inventory — and share one matching
|
|
412
|
+
* pass over it, memoised per fact, so eight rules cost one search.
|
|
413
|
+
*/
|
|
414
|
+
|
|
415
|
+
type RenderedRuleId = 'rendered/clip' | 'rendered/spill' | 'rendered/overlap' | 'rendered/text-missing' | 'rendered/font-substituted' | 'rendered/empty-page' | 'rendered/heading-stranded' | 'rendered/paragraph-split';
|
|
416
|
+
declare const RENDERED_QUALITY_RULES: QualityRulePack;
|
|
417
|
+
|
|
404
418
|
/**
|
|
405
419
|
* Locate authored text in rendered PDF geometry.
|
|
406
420
|
*
|
|
@@ -462,6 +476,12 @@ interface InventoryEntry {
|
|
|
462
476
|
text: string;
|
|
463
477
|
/** Header/footer text repeats per page: every occurrence is legitimate. */
|
|
464
478
|
repeats?: boolean;
|
|
479
|
+
/**
|
|
480
|
+
* Painted by the renderer from other authored text — a contents entry —
|
|
481
|
+
* so it claims its occurrence when there is one and is `skipped`, never
|
|
482
|
+
* `missing`, when there is not.
|
|
483
|
+
*/
|
|
484
|
+
optional?: boolean;
|
|
465
485
|
}
|
|
466
486
|
type MappingStatus = 'mapped' | 'ambiguous' | 'missing' | 'skipped';
|
|
467
487
|
interface InventoryMatch<T extends InventoryEntry = InventoryEntry> {
|
|
@@ -493,8 +513,8 @@ interface InventoryAssignment<T extends InventoryEntry> {
|
|
|
493
513
|
* stream without those words; each claims the first unclaimed occurrence at
|
|
494
514
|
* or after the previous claim, which is how "Total" in the second table
|
|
495
515
|
* finds the second "Total". An entry with no occurrence is `missing` —
|
|
496
|
-
* fully clipped, or never set — and one whose every
|
|
497
|
-
* claimed is `ambiguous`.
|
|
516
|
+
* fully clipped, or never set — unless it is optional, and one whose every
|
|
517
|
+
* occurrence was already claimed is `ambiguous`.
|
|
498
518
|
*/
|
|
499
519
|
declare function assignInventory<T extends InventoryEntry>(pages: readonly PdfTextPage[], inventory: readonly T[]): InventoryAssignment<T>;
|
|
500
520
|
|
|
@@ -518,11 +538,17 @@ declare function assignInventory<T extends InventoryEntry>(pages: readonly PdfTe
|
|
|
518
538
|
* Pure: geometry and fonts are extracted by the caller (see
|
|
519
539
|
* `extractPdfTextGeometry`, `extractPdfFonts`), so every rule here is
|
|
520
540
|
* testable from captured fixtures with no converter on the host.
|
|
541
|
+
*
|
|
542
|
+
* The checks draft findings; the quality engine turns them into
|
|
543
|
+
* diagnostics. Each check is one rule of `RENDERED_QUALITY_RULES`, so a
|
|
544
|
+
* profile can switch one off or move its severity, a policy can suppress
|
|
545
|
+
* one at a pointer and a gate can make one blocking — the same levers every
|
|
546
|
+
* static rule answers to, applied to the pass that measures.
|
|
521
547
|
*/
|
|
522
548
|
|
|
523
549
|
/** One authored string the pass can match rendered words back to. */
|
|
524
550
|
interface RenderedTextEntry extends InventoryEntry {
|
|
525
|
-
role: 'heading' | 'body' | 'list-item' | 'table-header' | 'table-cell' | 'statistic' | 'caption' | 'chrome' | 'slide-text';
|
|
551
|
+
role: 'heading' | 'body' | 'list-item' | 'table-header' | 'table-cell' | 'statistic' | 'caption' | 'toc-entry' | 'chrome' | 'slide-text';
|
|
526
552
|
level?: number;
|
|
527
553
|
/** A declared box the text must fit: a docx frame, a pptx text box. */
|
|
528
554
|
box?: {
|
|
@@ -544,6 +570,8 @@ interface RequestedFont {
|
|
|
544
570
|
}
|
|
545
571
|
interface RenderedAnalysisInput {
|
|
546
572
|
format: 'docx' | 'pptx';
|
|
573
|
+
/** Renderer identity, for a profile that declares renderer targets. */
|
|
574
|
+
renderer?: string;
|
|
547
575
|
pages: readonly PdfTextPage[];
|
|
548
576
|
inventory: readonly RenderedTextEntry[];
|
|
549
577
|
/** Fonts the PDF carries; `undefined` when the host could not inspect them. */
|
|
@@ -557,21 +585,37 @@ interface RenderedAnalysisSummary {
|
|
|
557
585
|
words: number;
|
|
558
586
|
/** Inventory entries by mapping outcome. */
|
|
559
587
|
inventory: Record<MappingStatus, number>;
|
|
560
|
-
/** Findings by mapping outcome. */
|
|
588
|
+
/** Findings by mapping outcome, after suppressions. */
|
|
561
589
|
findings: Record<RenderedMapping, number>;
|
|
562
590
|
fonts?: {
|
|
563
591
|
requested: number;
|
|
564
592
|
substituted: number;
|
|
565
593
|
};
|
|
594
|
+
/** Findings a policy suppression removed. */
|
|
595
|
+
suppressed: number;
|
|
596
|
+
/** Whether a policy gate made any finding blocking. */
|
|
597
|
+
blocked: boolean;
|
|
598
|
+
/** Whether a policy `maxDiagnostics` budget cut the findings returned. */
|
|
599
|
+
truncated: boolean;
|
|
600
|
+
/** The quality profile the pass ran under, when one applied. */
|
|
601
|
+
profileId?: string;
|
|
566
602
|
}
|
|
567
603
|
interface RenderedAnalysis {
|
|
568
|
-
|
|
604
|
+
/** The pass's diagnostics, as the quality engine finalised them. */
|
|
605
|
+
findings: readonly QualityDiagnostic[];
|
|
569
606
|
summary: RenderedAnalysisSummary;
|
|
607
|
+
/** The engine's own account: evaluated rules, rule errors, truncation. */
|
|
608
|
+
analysis: QualityAnalysis;
|
|
570
609
|
}
|
|
571
610
|
/** Spill below this is sub-visual: renderer rounding, descender fuzz. */
|
|
572
611
|
declare const VISIBLE_SPILL_PT = 2;
|
|
573
|
-
/**
|
|
574
|
-
|
|
612
|
+
/**
|
|
613
|
+
* Run the pass. Geometry, fonts and inventory become one `rendered/geometry`
|
|
614
|
+
* fact; the rendered rule pack reads it under the caller's profile and
|
|
615
|
+
* policy, so the findings come back suppressed, re-severed and gated the
|
|
616
|
+
* way `jto_validate`'s would.
|
|
617
|
+
*/
|
|
618
|
+
declare function analyzeRenderedDocument(input: RenderedAnalysisInput, options?: QualityAnalyzeOptions): RenderedAnalysis;
|
|
575
619
|
|
|
576
620
|
/**
|
|
577
621
|
* Make resolved fonts visible to the LibreOffice child process for the
|
|
@@ -764,4 +808,4 @@ declare function emitDiagnostic(text: string, tone?: DiagnosticTone): void;
|
|
|
764
808
|
*/
|
|
765
809
|
declare const stderrDiagnosticSink: DiagnosticSink;
|
|
766
810
|
|
|
767
|
-
export { type DiagnosticSink, type DiagnosticTone, DocxFormatAdapter, type FontStageHandle, type FontStageOptions, type FontStager, FontconfigStager, type FormatAdapter, type FormatName, type GeneratorOptions, type GeneratorResult, type InventoryEntry, type InventoryMatch, MacOSCoreTextStager, type MappingStatus, NoopFontStager, type PdfFontInfo, type PdfTextLine, type PdfTextPage, type PdfTextWord, PptxFormatAdapter, type RasterizerCacheStats, type RenderedAnalysis, type RenderedAnalysisInput, type RenderedAnalysisSummary, type RenderedMapping, type RenderedTextEntry, type RequestedFont, type TextOccurrence, VISIBLE_SPILL_PT, WindowsFontStager, analyzeRenderedDocument, assignInventory, authoredTextForMatch, clearRasterizerCache, createAdapter, createLibreOfficePptxBatchRasterizer, createLibreOfficePptxRasterizer, emitDiagnostic, extractPdfFonts, extractPdfTextGeometry, familyRendered, getFontStager, getRasterizerCacheStats, normalizeForMatch, parsePdfFonts, parsePdfTextBbox, pdffontsAvailable, pdftotextAvailable, runWithDiagnosticSink, stderrDiagnosticSink };
|
|
811
|
+
export { type DiagnosticSink, type DiagnosticTone, DocxFormatAdapter, type FontStageHandle, type FontStageOptions, type FontStager, FontconfigStager, type FormatAdapter, type FormatName, type GeneratorOptions, type GeneratorResult, type InventoryEntry, type InventoryMatch, MacOSCoreTextStager, type MappingStatus, 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, clearRasterizerCache, createAdapter, createLibreOfficePptxBatchRasterizer, createLibreOfficePptxRasterizer, emitDiagnostic, extractPdfFonts, extractPdfTextGeometry, familyRendered, getFontStager, getRasterizerCacheStats, normalizeForMatch, parsePdfFonts, parsePdfTextBbox, pdffontsAvailable, pdftotextAvailable, runWithDiagnosticSink, stderrDiagnosticSink };
|
package/dist/index.js
CHANGED
|
@@ -1947,9 +1947,93 @@ async function extractPdfFonts(pdfPath, options = {}) {
|
|
|
1947
1947
|
}
|
|
1948
1948
|
|
|
1949
1949
|
// src/rendered-analysis.ts
|
|
1950
|
+
import {
|
|
1951
|
+
QualityEngine
|
|
1952
|
+
} from "@json-to-office/quality";
|
|
1953
|
+
|
|
1954
|
+
// src/rendered-rules.ts
|
|
1950
1955
|
import {
|
|
1951
1956
|
QUALITY_CODES
|
|
1952
1957
|
} from "@json-to-office/quality";
|
|
1958
|
+
function renderedRule(spec) {
|
|
1959
|
+
return {
|
|
1960
|
+
id: spec.id,
|
|
1961
|
+
code: spec.code,
|
|
1962
|
+
description: spec.description,
|
|
1963
|
+
category: spec.category,
|
|
1964
|
+
defaultSeverity: spec.defaultSeverity,
|
|
1965
|
+
defaultCertainty: "rendered",
|
|
1966
|
+
...spec.formats && { formats: spec.formats },
|
|
1967
|
+
evaluate({ facts }) {
|
|
1968
|
+
const fact = facts.find(isRenderedGeometryFact);
|
|
1969
|
+
if (!fact) return [];
|
|
1970
|
+
return renderedDraftFor(fact).byRule.get(spec.id) ?? [];
|
|
1971
|
+
}
|
|
1972
|
+
};
|
|
1973
|
+
}
|
|
1974
|
+
var RENDERED_QUALITY_RULES = {
|
|
1975
|
+
id: "rendered",
|
|
1976
|
+
rules: [
|
|
1977
|
+
renderedRule({
|
|
1978
|
+
id: "rendered/clip",
|
|
1979
|
+
code: QUALITY_CODES.RENDERED_CLIP,
|
|
1980
|
+
description: "Words rendered past the page edge by more than 2 pt, or an authored string of which only a leading part rendered (`kind: truncated`, with the share found).",
|
|
1981
|
+
category: "integrity",
|
|
1982
|
+
defaultSeverity: "warning"
|
|
1983
|
+
}),
|
|
1984
|
+
renderedRule({
|
|
1985
|
+
id: "rendered/spill",
|
|
1986
|
+
code: QUALITY_CODES.RENDERED_SPILL,
|
|
1987
|
+
description: "A framed paragraph or slide text box drawn wider (DOCX) or larger (PPTX) than the box it declared.",
|
|
1988
|
+
category: "integrity",
|
|
1989
|
+
defaultSeverity: "warning"
|
|
1990
|
+
}),
|
|
1991
|
+
renderedRule({
|
|
1992
|
+
id: "rendered/overlap",
|
|
1993
|
+
code: QUALITY_CODES.RENDERED_OVERLAP,
|
|
1994
|
+
description: "Two words from different lines whose boxes intersect by more than 30% of the smaller.",
|
|
1995
|
+
category: "integrity",
|
|
1996
|
+
defaultSeverity: "warning"
|
|
1997
|
+
}),
|
|
1998
|
+
renderedRule({
|
|
1999
|
+
id: "rendered/text-missing",
|
|
2000
|
+
code: QUALITY_CODES.RENDERED_TEXT_MISSING,
|
|
2001
|
+
description: "An authored string that appears nowhere in the PDF: fully clipped, hidden, or dropped by the renderer.",
|
|
2002
|
+
category: "integrity",
|
|
2003
|
+
defaultSeverity: "warning"
|
|
2004
|
+
}),
|
|
2005
|
+
renderedRule({
|
|
2006
|
+
id: "rendered/font-substituted",
|
|
2007
|
+
code: QUALITY_CODES.RENDERED_FONT_SUBSTITUTED,
|
|
2008
|
+
description: "A requested family the PDF embeds no face of; a warning when the document declared a source for it, information when the host simply lacks it.",
|
|
2009
|
+
category: "brand",
|
|
2010
|
+
defaultSeverity: "info"
|
|
2011
|
+
}),
|
|
2012
|
+
renderedRule({
|
|
2013
|
+
id: "rendered/empty-page",
|
|
2014
|
+
code: QUALITY_CODES.RENDERED_EMPTY_PAGE,
|
|
2015
|
+
description: "A page with no text at all: a full-page figure is fine, a blank page from a stray break is not.",
|
|
2016
|
+
category: "integrity",
|
|
2017
|
+
defaultSeverity: "info"
|
|
2018
|
+
}),
|
|
2019
|
+
renderedRule({
|
|
2020
|
+
id: "rendered/heading-stranded",
|
|
2021
|
+
code: QUALITY_CODES.RENDERED_HEADING_STRANDED,
|
|
2022
|
+
description: "A heading that is the last body line on its page.",
|
|
2023
|
+
category: "composition",
|
|
2024
|
+
defaultSeverity: "warning",
|
|
2025
|
+
formats: ["docx"]
|
|
2026
|
+
}),
|
|
2027
|
+
renderedRule({
|
|
2028
|
+
id: "rendered/paragraph-split",
|
|
2029
|
+
code: QUALITY_CODES.RENDERED_PARAGRAPH_SPLIT,
|
|
2030
|
+
description: "A paragraph leaving one line alone on either side of a page break (`kind: orphan` or `widow`).",
|
|
2031
|
+
category: "composition",
|
|
2032
|
+
defaultSeverity: "info",
|
|
2033
|
+
formats: ["docx"]
|
|
2034
|
+
})
|
|
2035
|
+
]
|
|
2036
|
+
};
|
|
1953
2037
|
|
|
1954
2038
|
// src/rendered-text-match.ts
|
|
1955
2039
|
function normalizeForMatch(value) {
|
|
@@ -2142,7 +2226,7 @@ function assignInventory(pages, inventory) {
|
|
|
2142
2226
|
});
|
|
2143
2227
|
});
|
|
2144
2228
|
const body = indexDocument(pages, chromeWords);
|
|
2145
|
-
const claimed = /* @__PURE__ */ new
|
|
2229
|
+
const claimed = /* @__PURE__ */ new Map();
|
|
2146
2230
|
let cursor = 0;
|
|
2147
2231
|
for (const entry of inventory) {
|
|
2148
2232
|
if (entry.repeats) continue;
|
|
@@ -2154,7 +2238,7 @@ function assignInventory(pages, inventory) {
|
|
|
2154
2238
|
}
|
|
2155
2239
|
let all = findOccurrences(body, segments);
|
|
2156
2240
|
let partial;
|
|
2157
|
-
if (all.length === 0 && segments.length === 1) {
|
|
2241
|
+
if (all.length === 0 && segments.length === 1 && !entry.optional) {
|
|
2158
2242
|
const prefix = longestRenderedPrefix(body, needle);
|
|
2159
2243
|
if (prefix) {
|
|
2160
2244
|
all = [occurrence(body, prefix.at, prefix.at + prefix.length)];
|
|
@@ -2162,10 +2246,29 @@ function assignInventory(pages, inventory) {
|
|
|
2162
2246
|
}
|
|
2163
2247
|
}
|
|
2164
2248
|
if (all.length === 0) {
|
|
2165
|
-
results.set(entry, {
|
|
2249
|
+
results.set(entry, {
|
|
2250
|
+
entry,
|
|
2251
|
+
needle,
|
|
2252
|
+
status: entry.optional ? "skipped" : "missing",
|
|
2253
|
+
occurrences: []
|
|
2254
|
+
});
|
|
2166
2255
|
continue;
|
|
2167
2256
|
}
|
|
2168
|
-
|
|
2257
|
+
let free = all.filter((o) => !claimed.has(o.at));
|
|
2258
|
+
if (free.length === 0 && !entry.optional) {
|
|
2259
|
+
const held = all.find((o) => claimed.get(o.at)?.optional);
|
|
2260
|
+
if (held) {
|
|
2261
|
+
const holder = claimed.get(held.at);
|
|
2262
|
+
claimed.delete(held.at);
|
|
2263
|
+
results.set(holder, {
|
|
2264
|
+
entry: holder,
|
|
2265
|
+
needle: results.get(holder)?.needle ?? "",
|
|
2266
|
+
status: "skipped",
|
|
2267
|
+
occurrences: []
|
|
2268
|
+
});
|
|
2269
|
+
free = [held];
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
2169
2272
|
if (free.length === 0) {
|
|
2170
2273
|
results.set(entry, {
|
|
2171
2274
|
entry,
|
|
@@ -2176,7 +2279,7 @@ function assignInventory(pages, inventory) {
|
|
|
2176
2279
|
continue;
|
|
2177
2280
|
}
|
|
2178
2281
|
const chosen = free.find((o) => o.at >= cursor) ?? free[0];
|
|
2179
|
-
claimed.
|
|
2282
|
+
claimed.set(chosen.at, entry);
|
|
2180
2283
|
cursor = chosen.at;
|
|
2181
2284
|
results.set(entry, {
|
|
2182
2285
|
entry,
|
|
@@ -2193,15 +2296,23 @@ function assignInventory(pages, inventory) {
|
|
|
2193
2296
|
}
|
|
2194
2297
|
|
|
2195
2298
|
// src/rendered-analysis.ts
|
|
2299
|
+
function isRenderedGeometryFact(fact) {
|
|
2300
|
+
return fact.kind === "rendered/geometry";
|
|
2301
|
+
}
|
|
2302
|
+
var drafts = /* @__PURE__ */ new WeakMap();
|
|
2303
|
+
function renderedDraftFor(fact) {
|
|
2304
|
+
let draft = drafts.get(fact);
|
|
2305
|
+
if (!draft) {
|
|
2306
|
+
draft = draftRenderedFindings(fact.input);
|
|
2307
|
+
drafts.set(fact, draft);
|
|
2308
|
+
}
|
|
2309
|
+
return draft;
|
|
2310
|
+
}
|
|
2196
2311
|
var VISIBLE_SPILL_PT = 2;
|
|
2197
2312
|
var OVERLAP_FRACTION = 0.3;
|
|
2198
2313
|
function finding(draft) {
|
|
2199
|
-
const {
|
|
2314
|
+
const { mapping, page, context, ...rest } = draft;
|
|
2200
2315
|
return {
|
|
2201
|
-
source: "quality",
|
|
2202
|
-
ruleId,
|
|
2203
|
-
certainty: "rendered",
|
|
2204
|
-
blocking: false,
|
|
2205
2316
|
...rest,
|
|
2206
2317
|
context: {
|
|
2207
2318
|
...context,
|
|
@@ -2259,7 +2370,7 @@ function unionBox(parts) {
|
|
|
2259
2370
|
{ xMin: Infinity, yMin: Infinity, xMax: -Infinity, yMax: -Infinity }
|
|
2260
2371
|
);
|
|
2261
2372
|
}
|
|
2262
|
-
function
|
|
2373
|
+
function draftRenderedFindings(input) {
|
|
2263
2374
|
const { pages, format } = input;
|
|
2264
2375
|
const { matches, chromeWords } = assignInventory(pages, input.inventory);
|
|
2265
2376
|
const owners = wordOwners(matches);
|
|
@@ -2293,9 +2404,6 @@ function analyzeRenderedDocument(input) {
|
|
|
2293
2404
|
findings.push(
|
|
2294
2405
|
finding({
|
|
2295
2406
|
ruleId: "rendered/clip",
|
|
2296
|
-
code: QUALITY_CODES.RENDERED_CLIP,
|
|
2297
|
-
category: "integrity",
|
|
2298
|
-
severity: "warning",
|
|
2299
2407
|
mapping: bucket.match ? "mapped" : "unmapped",
|
|
2300
2408
|
page: bucket.page,
|
|
2301
2409
|
path: bucket.match?.entry.path ?? "",
|
|
@@ -2320,9 +2428,6 @@ function analyzeRenderedDocument(input) {
|
|
|
2320
2428
|
findings.push(
|
|
2321
2429
|
finding({
|
|
2322
2430
|
ruleId: "rendered/clip",
|
|
2323
|
-
code: QUALITY_CODES.RENDERED_CLIP,
|
|
2324
|
-
category: "integrity",
|
|
2325
|
-
severity: "warning",
|
|
2326
2431
|
mapping: "mapped",
|
|
2327
2432
|
page: o.endPageIndex + 1,
|
|
2328
2433
|
path: match.entry.path,
|
|
@@ -2359,9 +2464,6 @@ function analyzeRenderedDocument(input) {
|
|
|
2359
2464
|
findings.push(
|
|
2360
2465
|
finding({
|
|
2361
2466
|
ruleId: "rendered/spill",
|
|
2362
|
-
code: QUALITY_CODES.RENDERED_SPILL,
|
|
2363
|
-
category: "integrity",
|
|
2364
|
-
severity: "warning",
|
|
2365
2467
|
mapping: "mapped",
|
|
2366
2468
|
page: o.pageIndex + 1,
|
|
2367
2469
|
path: match.entry.path,
|
|
@@ -2407,9 +2509,6 @@ function analyzeRenderedDocument(input) {
|
|
|
2407
2509
|
findings.push(
|
|
2408
2510
|
finding({
|
|
2409
2511
|
ruleId: "rendered/overlap",
|
|
2410
|
-
code: QUALITY_CODES.RENDERED_OVERLAP,
|
|
2411
|
-
category: "integrity",
|
|
2412
|
-
severity: "warning",
|
|
2413
2512
|
mapping,
|
|
2414
2513
|
page: pageIndex + 1,
|
|
2415
2514
|
path: pathA || pathB,
|
|
@@ -2432,9 +2531,6 @@ function analyzeRenderedDocument(input) {
|
|
|
2432
2531
|
findings.push(
|
|
2433
2532
|
finding({
|
|
2434
2533
|
ruleId: "rendered/text-missing",
|
|
2435
|
-
code: QUALITY_CODES.RENDERED_TEXT_MISSING,
|
|
2436
|
-
category: "integrity",
|
|
2437
|
-
severity: "warning",
|
|
2438
2534
|
mapping: "mapped",
|
|
2439
2535
|
path: match.entry.path,
|
|
2440
2536
|
message: `"${excerpt(match.entry.text)}" does not appear anywhere in the rendered pages.`,
|
|
@@ -2456,8 +2552,6 @@ function analyzeRenderedDocument(input) {
|
|
|
2456
2552
|
findings.push(
|
|
2457
2553
|
finding({
|
|
2458
2554
|
ruleId: "rendered/font-substituted",
|
|
2459
|
-
code: QUALITY_CODES.RENDERED_FONT_SUBSTITUTED,
|
|
2460
|
-
category: "brand",
|
|
2461
2555
|
severity: requested.declared ? "warning" : "info",
|
|
2462
2556
|
mapping: requested.path ? "mapped" : "unmapped",
|
|
2463
2557
|
path: requested.path ?? "",
|
|
@@ -2478,9 +2572,6 @@ function analyzeRenderedDocument(input) {
|
|
|
2478
2572
|
findings.push(
|
|
2479
2573
|
finding({
|
|
2480
2574
|
ruleId: "rendered/empty-page",
|
|
2481
|
-
code: QUALITY_CODES.RENDERED_EMPTY_PAGE,
|
|
2482
|
-
category: "integrity",
|
|
2483
|
-
severity: "info",
|
|
2484
2575
|
mapping: "unmapped",
|
|
2485
2576
|
page: pageIndex + 1,
|
|
2486
2577
|
path: "",
|
|
@@ -2503,9 +2594,6 @@ function analyzeRenderedDocument(input) {
|
|
|
2503
2594
|
findings.push(
|
|
2504
2595
|
finding({
|
|
2505
2596
|
ruleId: "rendered/heading-stranded",
|
|
2506
|
-
code: QUALITY_CODES.RENDERED_HEADING_STRANDED,
|
|
2507
|
-
category: "composition",
|
|
2508
|
-
severity: "warning",
|
|
2509
2597
|
mapping: "mapped",
|
|
2510
2598
|
page: part.pageIndex + 1,
|
|
2511
2599
|
path: match.entry.path,
|
|
@@ -2539,9 +2627,6 @@ function analyzeRenderedDocument(input) {
|
|
|
2539
2627
|
findings.push(
|
|
2540
2628
|
finding({
|
|
2541
2629
|
ruleId: "rendered/paragraph-split",
|
|
2542
|
-
code: QUALITY_CODES.RENDERED_PARAGRAPH_SPLIT,
|
|
2543
|
-
category: "composition",
|
|
2544
|
-
severity: "info",
|
|
2545
2630
|
mapping: "mapped",
|
|
2546
2631
|
page: (which === "orphan" ? first.pageIndex : last.pageIndex) + 1,
|
|
2547
2632
|
path: match.entry.path,
|
|
@@ -2563,27 +2648,80 @@ function analyzeRenderedDocument(input) {
|
|
|
2563
2648
|
skipped: 0
|
|
2564
2649
|
};
|
|
2565
2650
|
for (const match of matches) inventory[match.status] += 1;
|
|
2651
|
+
const byRule = /* @__PURE__ */ new Map();
|
|
2652
|
+
for (const draft of findings) {
|
|
2653
|
+
const bucket = byRule.get(draft.ruleId);
|
|
2654
|
+
if (bucket) bucket.push(draft);
|
|
2655
|
+
else byRule.set(draft.ruleId, [draft]);
|
|
2656
|
+
}
|
|
2657
|
+
return {
|
|
2658
|
+
drafts: findings,
|
|
2659
|
+
byRule,
|
|
2660
|
+
inventory,
|
|
2661
|
+
// The same gate the check itself runs under: an empty font list
|
|
2662
|
+
// verified nothing, and must not read as "nothing was substituted".
|
|
2663
|
+
...input.fonts && input.fonts.length > 0 && input.requestedFonts && {
|
|
2664
|
+
fonts: { requested: input.requestedFonts.length, substituted }
|
|
2665
|
+
}
|
|
2666
|
+
};
|
|
2667
|
+
}
|
|
2668
|
+
var engine;
|
|
2669
|
+
function renderedEngine() {
|
|
2670
|
+
engine ??= new QualityEngine(RENDERED_QUALITY_RULES.rules);
|
|
2671
|
+
return engine;
|
|
2672
|
+
}
|
|
2673
|
+
function analyzeRenderedDocument(input, options = {}) {
|
|
2674
|
+
const fact = {
|
|
2675
|
+
id: "rendered:geometry",
|
|
2676
|
+
kind: "rendered/geometry",
|
|
2677
|
+
path: "",
|
|
2678
|
+
input
|
|
2679
|
+
};
|
|
2680
|
+
const analysis = renderedEngine().analyzeSync(
|
|
2681
|
+
{
|
|
2682
|
+
format: input.format,
|
|
2683
|
+
model: void 0,
|
|
2684
|
+
facts: [fact],
|
|
2685
|
+
provenance: {},
|
|
2686
|
+
...input.renderer !== void 0 && { renderer: input.renderer }
|
|
2687
|
+
},
|
|
2688
|
+
options
|
|
2689
|
+
);
|
|
2690
|
+
let draft;
|
|
2691
|
+
try {
|
|
2692
|
+
draft = renderedDraftFor(fact);
|
|
2693
|
+
} catch {
|
|
2694
|
+
draft = void 0;
|
|
2695
|
+
}
|
|
2566
2696
|
const byMapping = {
|
|
2567
2697
|
mapped: 0,
|
|
2568
2698
|
ambiguous: 0,
|
|
2569
2699
|
unmapped: 0
|
|
2570
2700
|
};
|
|
2571
|
-
for (const f of
|
|
2701
|
+
for (const f of analysis.diagnostics) {
|
|
2572
2702
|
byMapping[f.context?.mapping ?? "unmapped"] += 1;
|
|
2573
2703
|
}
|
|
2574
2704
|
return {
|
|
2575
|
-
findings,
|
|
2705
|
+
findings: analysis.diagnostics,
|
|
2576
2706
|
summary: {
|
|
2577
|
-
pages: pages.length,
|
|
2578
|
-
words: pages.reduce((n, p) => n + p.words.length, 0),
|
|
2579
|
-
inventory
|
|
2707
|
+
pages: input.pages.length,
|
|
2708
|
+
words: input.pages.reduce((n, p) => n + p.words.length, 0),
|
|
2709
|
+
inventory: draft?.inventory ?? {
|
|
2710
|
+
mapped: 0,
|
|
2711
|
+
ambiguous: 0,
|
|
2712
|
+
missing: 0,
|
|
2713
|
+
skipped: 0
|
|
2714
|
+
},
|
|
2580
2715
|
findings: byMapping,
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2716
|
+
...draft?.fonts && { fonts: draft.fonts },
|
|
2717
|
+
suppressed: analysis.suppressedCount,
|
|
2718
|
+
blocked: analysis.blocked,
|
|
2719
|
+
truncated: analysis.truncated,
|
|
2720
|
+
...analysis.profileId !== void 0 && {
|
|
2721
|
+
profileId: analysis.profileId
|
|
2585
2722
|
}
|
|
2586
|
-
}
|
|
2723
|
+
},
|
|
2724
|
+
analysis
|
|
2587
2725
|
};
|
|
2588
2726
|
}
|
|
2589
2727
|
export {
|
|
@@ -2592,6 +2730,7 @@ export {
|
|
|
2592
2730
|
MacOSCoreTextStager,
|
|
2593
2731
|
NoopFontStager,
|
|
2594
2732
|
PptxFormatAdapter,
|
|
2733
|
+
RENDERED_QUALITY_RULES,
|
|
2595
2734
|
VISIBLE_SPILL_PT,
|
|
2596
2735
|
WindowsFontStager,
|
|
2597
2736
|
analyzeRenderedDocument,
|