@json-to-office/jto-ops 7.0.1 → 7.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 +14 -4
- package/dist/index.js +102 -5
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
package/dist/index.d.ts
CHANGED
|
@@ -475,7 +475,7 @@ declare function extractPdfFonts(pdfPath: string, options?: {
|
|
|
475
475
|
* pass over it, memoised per fact, so eight rules cost one search.
|
|
476
476
|
*/
|
|
477
477
|
|
|
478
|
-
type RenderedRuleId = 'rendered/clip' | 'rendered/spill' | 'rendered/overlap' | 'rendered/text-missing' | 'rendered/font-substituted' | 'rendered/empty-page' | 'rendered/heading-stranded' | 'rendered/paragraph-split' | 'rendered/table-split' | 'rendered/page-underfilled';
|
|
478
|
+
type RenderedRuleId = 'rendered/clip' | 'rendered/spill' | 'rendered/overlap' | 'rendered/text-missing' | 'rendered/placeholder' | 'rendered/font-substituted' | 'rendered/empty-page' | 'rendered/heading-stranded' | 'rendered/paragraph-split' | 'rendered/table-split' | 'rendered/page-underfilled';
|
|
479
479
|
declare const RENDERED_QUALITY_RULES: QualityRulePack;
|
|
480
480
|
|
|
481
481
|
/**
|
|
@@ -544,8 +544,11 @@ interface TextOccurrence {
|
|
|
544
544
|
* row line by line across every cell, poppler 26 lists it cell by cell, and
|
|
545
545
|
* an authored string in a wrapped cell only survives the second.
|
|
546
546
|
*
|
|
547
|
-
* Rows are grouped by vertical overlap and split into fragments
|
|
548
|
-
* wider than half the line is tall
|
|
547
|
+
* Rows are grouped by vertical overlap and split into fragments, a cell's
|
|
548
|
+
* line each: at gaps wider than half the line is tall, at gaps wider than
|
|
549
|
+
* the row's own spaces account for, and before a word that starts on the
|
|
550
|
+
* edge of a column the rows around it mark, where two cells ran together
|
|
551
|
+
* at a gap no wider than a space. A row of two or more fragments opens a
|
|
549
552
|
* column block; the rows under it belong to the block while each of their
|
|
550
553
|
* fragments sits inside one column (a right-aligned cell's second line
|
|
551
554
|
* starts further right; a wrapped label's later lines are the only
|
|
@@ -644,7 +647,8 @@ declare function assignInventory<T extends InventoryEntry>(pages: readonly PdfTe
|
|
|
644
647
|
* the preview PDF, the fonts the PDF embeds, and the document's authored
|
|
645
648
|
* text inventory, and reports the defects only ink can show: text past the
|
|
646
649
|
* page or its frame, words drawn over each other, text that never rendered
|
|
647
|
-
* (fully clipped, or set in a place poppler cannot read),
|
|
650
|
+
* (fully clipped, or set in a place poppler cannot read), a placeholder the
|
|
651
|
+
* renderer painted where it could not draw a component, faces substituted
|
|
648
652
|
* on the way, pages with nothing on them, headings stranded at a page foot
|
|
649
653
|
* and paragraphs split into a lone line.
|
|
650
654
|
*
|
|
@@ -673,6 +677,12 @@ interface RenderedTextEntry extends InventoryEntry {
|
|
|
673
677
|
widthPt?: number;
|
|
674
678
|
heightPt?: number;
|
|
675
679
|
};
|
|
680
|
+
/**
|
|
681
|
+
* Set when the entry is the placeholder a renderer paints for a component
|
|
682
|
+
* it could not draw there: the component's name. Authored text that quotes
|
|
683
|
+
* the placeholder's wording carries none.
|
|
684
|
+
*/
|
|
685
|
+
unrendered?: string;
|
|
676
686
|
}
|
|
677
687
|
interface RequestedFont {
|
|
678
688
|
family: string;
|
package/dist/index.js
CHANGED
|
@@ -2113,7 +2113,9 @@ async function extractPdfFonts(pdfPath, options = {}) {
|
|
|
2113
2113
|
|
|
2114
2114
|
// src/rendered-analysis.ts
|
|
2115
2115
|
import {
|
|
2116
|
-
QualityEngine
|
|
2116
|
+
QualityEngine,
|
|
2117
|
+
unrenderedComponentName,
|
|
2118
|
+
unrenderedComponentText
|
|
2117
2119
|
} from "@json-to-office/quality";
|
|
2118
2120
|
|
|
2119
2121
|
// src/rendered-rules.ts
|
|
@@ -2167,6 +2169,14 @@ var RENDERED_QUALITY_RULES = {
|
|
|
2167
2169
|
category: "integrity",
|
|
2168
2170
|
defaultSeverity: "warning"
|
|
2169
2171
|
}),
|
|
2172
|
+
renderedRule({
|
|
2173
|
+
id: "rendered/placeholder",
|
|
2174
|
+
code: QUALITY_CODES.RENDERED_PLACEHOLDER,
|
|
2175
|
+
description: `DOCX: the renderer painted "[Unsupported component type: \u2026]" where it could not draw a component \u2014 a table cell holding one it cannot render. Mapped to the cell when the document puts it there; reported on its page, unmapped, when it came from somewhere the document does not show, such as a plugin's output.`,
|
|
2176
|
+
category: "integrity",
|
|
2177
|
+
defaultSeverity: "warning",
|
|
2178
|
+
formats: ["docx"]
|
|
2179
|
+
}),
|
|
2170
2180
|
renderedRule({
|
|
2171
2181
|
id: "rendered/font-substituted",
|
|
2172
2182
|
code: QUALITY_CODES.RENDERED_FONT_SUBSTITUTED,
|
|
@@ -2283,10 +2293,26 @@ function linedOrder(words2, lines = []) {
|
|
|
2283
2293
|
};
|
|
2284
2294
|
}
|
|
2285
2295
|
function rotatedWords(words2, lines) {
|
|
2296
|
+
const upright = words2.filter(
|
|
2297
|
+
(w) => w.text.trim() !== "" && w.xMax - w.xMin >= w.yMax - w.yMin
|
|
2298
|
+
);
|
|
2299
|
+
const standsInALine = (w) => {
|
|
2300
|
+
const height = w.yMax - w.yMin;
|
|
2301
|
+
return upright.some((other) => {
|
|
2302
|
+
if (other === w) return false;
|
|
2303
|
+
const overlap = Math.min(w.yMax, other.yMax) - Math.max(w.yMin, other.yMin);
|
|
2304
|
+
const shorter = Math.min(height, other.yMax - other.yMin);
|
|
2305
|
+
if (shorter <= 0 || overlap <= shorter / 2) return false;
|
|
2306
|
+
const ratio = height / (other.yMax - other.yMin);
|
|
2307
|
+
return ratio > 0.8 && ratio < 1.25;
|
|
2308
|
+
});
|
|
2309
|
+
};
|
|
2286
2310
|
const rotated = new Set(
|
|
2287
2311
|
words2.map((_, i) => i).filter((i) => {
|
|
2288
2312
|
const w = words2[i];
|
|
2289
|
-
|
|
2313
|
+
if (w.text.length <= 2) return false;
|
|
2314
|
+
if (w.yMax - w.yMin <= 1.5 * (w.xMax - w.xMin)) return false;
|
|
2315
|
+
return !standsInALine(w);
|
|
2290
2316
|
})
|
|
2291
2317
|
);
|
|
2292
2318
|
for (const line of lines) {
|
|
@@ -2330,10 +2356,12 @@ function groupRows(words2, indices) {
|
|
|
2330
2356
|
}
|
|
2331
2357
|
return rows;
|
|
2332
2358
|
}
|
|
2359
|
+
var COLUMN_EDGE_TOLERANCE_PT = 0.05;
|
|
2360
|
+
var WORD_SPACE_TOLERANCE_PT = 0.1;
|
|
2333
2361
|
function uprightOrder(words2, indices) {
|
|
2334
2362
|
const grouped = groupRows(words2, indices);
|
|
2335
2363
|
const rows = grouped.map((row) => row.words);
|
|
2336
|
-
const fragmentsOf = (row) => {
|
|
2364
|
+
const fragmentsOf = (row, onEdge) => {
|
|
2337
2365
|
const sorted = [...row].sort((a, b) => words2[a].xMin - words2[b].xMin);
|
|
2338
2366
|
const gaps = sorted.slice(1).map((i, n) => {
|
|
2339
2367
|
const height = words2[i].yMax - words2[i].yMin;
|
|
@@ -2344,7 +2372,7 @@ function uprightOrder(words2, indices) {
|
|
|
2344
2372
|
const typical = spaces.length > 1 ? median(spaces) : void 0;
|
|
2345
2373
|
const isCellBoundary = (n) => {
|
|
2346
2374
|
const g = gaps[n - 1];
|
|
2347
|
-
return g.wide || typical !== void 0 && g.gap > g.height / 3 && g.gap >= typical * 1.5;
|
|
2375
|
+
return g.wide || typical !== void 0 && g.gap > g.height / 3 && g.gap >= typical * 1.5 || onEdge !== void 0 && typical !== void 0 && spaces.length >= 3 && Math.abs(g.gap - typical) > WORD_SPACE_TOLERANCE_PT && onEdge(words2[sorted[n - 1]], words2[sorted[n]]);
|
|
2348
2376
|
};
|
|
2349
2377
|
const out = [];
|
|
2350
2378
|
sorted.forEach((i, n) => {
|
|
@@ -2357,7 +2385,26 @@ function uprightOrder(words2, indices) {
|
|
|
2357
2385
|
});
|
|
2358
2386
|
return out;
|
|
2359
2387
|
};
|
|
2360
|
-
const
|
|
2388
|
+
const byGaps = rows.map((row) => fragmentsOf(row));
|
|
2389
|
+
const onColumnEdge = (r, before, word) => {
|
|
2390
|
+
const x = word.xMin;
|
|
2391
|
+
for (const step of [-1, 1]) {
|
|
2392
|
+
for (let t = r + step; t >= 0 && t < rows.length; t += step) {
|
|
2393
|
+
const edge = byGaps[t].slice(1).some(
|
|
2394
|
+
(f) => Math.abs(f.xMin - x) <= COLUMN_EDGE_TOLERANCE_PT && before.xMax <= f.xMin
|
|
2395
|
+
);
|
|
2396
|
+
if (edge) return true;
|
|
2397
|
+
const across = rows[t].some(
|
|
2398
|
+
(i) => words2[i].xMin < x - COLUMN_EDGE_TOLERANCE_PT && words2[i].xMax > x + COLUMN_EDGE_TOLERANCE_PT
|
|
2399
|
+
);
|
|
2400
|
+
if (across) break;
|
|
2401
|
+
}
|
|
2402
|
+
}
|
|
2403
|
+
return false;
|
|
2404
|
+
};
|
|
2405
|
+
const fragments = rows.map(
|
|
2406
|
+
(row, r) => fragmentsOf(row, (before, word) => onColumnEdge(r, before, word))
|
|
2407
|
+
);
|
|
2361
2408
|
const boxes = grouped;
|
|
2362
2409
|
const order = [];
|
|
2363
2410
|
const read = /* @__PURE__ */ new Set();
|
|
@@ -3160,6 +3207,53 @@ function draftRenderedFindings(input) {
|
|
|
3160
3207
|
})
|
|
3161
3208
|
);
|
|
3162
3209
|
}
|
|
3210
|
+
const placeholderFinding = (name, page, path9, mapping) => {
|
|
3211
|
+
const text = name ? unrenderedComponentText(name) : "[Unsupported component type: \u2026]";
|
|
3212
|
+
return finding({
|
|
3213
|
+
ruleId: "rendered/placeholder",
|
|
3214
|
+
mapping,
|
|
3215
|
+
page,
|
|
3216
|
+
path: path9,
|
|
3217
|
+
message: `Page ${page} shows "${text}": the renderer could not draw ${name ? `a ${name}` : "a component"} there and painted this instead.`,
|
|
3218
|
+
suggestion: "Put only what the position renders there \u2014 a table cell takes a string, a paragraph, an image, a visual or a highcharts chart \u2014 or move the component out of the table; statistics side by side are a columns component.",
|
|
3219
|
+
evidence: {
|
|
3220
|
+
summary: "Renderer placeholder text in the PDF",
|
|
3221
|
+
actual: text
|
|
3222
|
+
},
|
|
3223
|
+
context: {
|
|
3224
|
+
kind: "unrendered-component",
|
|
3225
|
+
...name && { component: name }
|
|
3226
|
+
}
|
|
3227
|
+
});
|
|
3228
|
+
};
|
|
3229
|
+
for (const match of matches) {
|
|
3230
|
+
const name = match.entry.unrendered;
|
|
3231
|
+
if (match.status !== "mapped" || name === void 0) continue;
|
|
3232
|
+
for (const o of match.occurrences) {
|
|
3233
|
+
findings.push(
|
|
3234
|
+
placeholderFinding(name, o.pageIndex + 1, match.entry.path, "mapped")
|
|
3235
|
+
);
|
|
3236
|
+
}
|
|
3237
|
+
}
|
|
3238
|
+
pages.forEach((page, pageIndex) => {
|
|
3239
|
+
page.words.forEach((word, index) => {
|
|
3240
|
+
if (!word.text.startsWith("[Unsupported")) return;
|
|
3241
|
+
if (ownerOf(pageIndex, index)) return;
|
|
3242
|
+
const phrase = [];
|
|
3243
|
+
for (const next of page.words.slice(index, index + 6)) {
|
|
3244
|
+
phrase.push(next.text);
|
|
3245
|
+
if (next.text.endsWith("]")) break;
|
|
3246
|
+
}
|
|
3247
|
+
findings.push(
|
|
3248
|
+
placeholderFinding(
|
|
3249
|
+
unrenderedComponentName(phrase.join(" ")),
|
|
3250
|
+
pageIndex + 1,
|
|
3251
|
+
"",
|
|
3252
|
+
"unmapped"
|
|
3253
|
+
)
|
|
3254
|
+
);
|
|
3255
|
+
});
|
|
3256
|
+
});
|
|
3163
3257
|
let substituted = 0;
|
|
3164
3258
|
if (input.fonts && input.fonts.length > 0 && input.requestedFonts) {
|
|
3165
3259
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -3500,6 +3594,9 @@ function renderedInventoryFromFacts(format, facts) {
|
|
|
3500
3594
|
...typeof fact.level === "number" && { level: fact.level },
|
|
3501
3595
|
...fact.repeats === true && { repeats: true },
|
|
3502
3596
|
...fact.optional === true && { optional: true },
|
|
3597
|
+
...typeof fact.unrendered === "string" && {
|
|
3598
|
+
unrendered: fact.unrendered
|
|
3599
|
+
},
|
|
3503
3600
|
...frame && {
|
|
3504
3601
|
box: {
|
|
3505
3602
|
...frame.widthPt !== void 0 && { widthPt: frame.widthPt },
|