fix the issue
@@ -516,6 +516,9 @@ PYBIND11_MODULE(pdfengine, m) {
|
||||
}
|
||||
return py_regions;
|
||||
}, py::arg("edits_json"))
|
||||
.def("last_reflow_layout", [](const pdfengine::PdfDocument& self) {
|
||||
return self.lastReflowLayout();
|
||||
})
|
||||
.def("save_incremental", [](const pdfengine::PdfDocument& self) {
|
||||
std::vector<uint8_t> res = get_or_throw(self.saveIncremental());
|
||||
return py::bytes(reinterpret_cast<const char*>(res.data()), res.size());
|
||||
|
||||
|
Before Width: | Height: | Size: 646 B After Width: | Height: | Size: 36 KiB |
@@ -12,7 +12,17 @@ std::expected<void, EngineError> PdfiumDocument::applyOp_reflow(const nlohmann::
|
||||
}
|
||||
auto data = op["data"];
|
||||
|
||||
struct RunStyle { std::string text; std::string internalFontId; double fontSize; unsigned int r, g, b; std::vector<double> advances; };
|
||||
struct RunStyle {
|
||||
std::string text;
|
||||
std::string internalFontId;
|
||||
double fontSize;
|
||||
unsigned int r, g, b;
|
||||
std::vector<double> advances;
|
||||
// When set (and advances.size() == advanceSeedText.size()), advances are
|
||||
// metrics for advanceSeedText; merge onto text via LCP/LCS so typing
|
||||
// preserves kerning on the unchanged prefix/suffix.
|
||||
std::string advanceSeedText;
|
||||
};
|
||||
std::vector<RunStyle> runs;
|
||||
auto parseHex = [](const std::string& hex, unsigned int& r, unsigned int& g, unsigned int& b) {
|
||||
r = 0; g = 0; b = 0;
|
||||
@@ -37,6 +47,7 @@ std::expected<void, EngineError> PdfiumDocument::applyOp_reflow(const nlohmann::
|
||||
if (rj.contains("advances") && rj["advances"].is_array()) {
|
||||
for (const auto& a : rj["advances"]) rs.advances.push_back(a.get<double>());
|
||||
}
|
||||
rs.advanceSeedText = rj.value("advanceSeedText", "");
|
||||
runs.push_back(std::move(rs));
|
||||
};
|
||||
std::vector<std::vector<int>> providedLines;
|
||||
@@ -182,6 +193,8 @@ std::expected<void, EngineError> PdfiumDocument::applyOp_reflow(const nlohmann::
|
||||
spdlog::info("[FONT_METRICS_DEBUG] exactNominal={:.2f}, exactScaleX={:.2f}, exactScaleY={:.2f}, trueVerticalSize={:.2f}, textAspect={:.2f}", exactNominal, exactScaleX, exactScaleY, exactNominal * exactScaleY, textAspect);
|
||||
|
||||
std::vector<EmissionFont> runFonts(runs.size());
|
||||
// utf8_to_utf16le appends a trailing NUL for PDFium wide-string APIs.
|
||||
// That terminator is not text content and must not enter coverage checks.
|
||||
auto toCodepoints = [](const std::string& s) {
|
||||
auto u16 = utf8_to_utf16le(s);
|
||||
std::vector<uint32_t> cps;
|
||||
@@ -192,6 +205,7 @@ std::expected<void, EngineError> PdfiumDocument::applyOp_reflow(const nlohmann::
|
||||
if (low >= 0xDC00 && low <= 0xDFFF) { cp = 0x10000 + ((cp - 0xD800) << 10) + (low - 0xDC00); i += 2; }
|
||||
else i += 1;
|
||||
} else i += 1;
|
||||
if (cp == 0) continue;
|
||||
cps.push_back(cp);
|
||||
}
|
||||
return cps;
|
||||
@@ -263,24 +277,83 @@ std::expected<void, EngineError> PdfiumDocument::applyOp_reflow(const nlohmann::
|
||||
const bool forensicForceWholeRun = data.value("forensicForceWholeRun", false);
|
||||
std::vector<std::vector<double>> runCharAdv(runs.size());
|
||||
std::vector<char> runPerChar(runs.size(), 0);
|
||||
|
||||
// Merge seed advances onto edited text: keep LCP/LCS metrics, HB-fill the edit middle.
|
||||
// (Logic inlined in the run loop so unchanged glyphs are never passed to HarfBuzz.)
|
||||
|
||||
for (size_t ri = 0; ri < runs.size(); ++ri) {
|
||||
if (runs[ri].advances.size() == runs[ri].text.size() && !runs[ri].text.empty()) {
|
||||
runCharAdv[ri] = runs[ri].advances;
|
||||
auto natural = perCharAdvances(ri, runs[ri].text);
|
||||
const auto& rs = runs[ri];
|
||||
bool usedClient = false;
|
||||
|
||||
// Prefer client/seed advances for UNCHANGED glyphs. Only HarfBuzz-shape
|
||||
// characters that are not covered by seed metrics (the edited middle).
|
||||
if (rs.advances.size() == rs.text.size() && !rs.text.empty()) {
|
||||
runCharAdv[ri] = rs.advances;
|
||||
usedClient = true;
|
||||
} else if (!rs.advanceSeedText.empty()
|
||||
&& rs.advances.size() == rs.advanceSeedText.size()
|
||||
&& !rs.text.empty()) {
|
||||
// Shape only the middle gap; prefix/suffix keep seed advances.
|
||||
size_t p = 0;
|
||||
const auto& seed = rs.advanceSeedText;
|
||||
while (p < seed.size() && p < rs.text.size() && seed[p] == rs.text[p]) ++p;
|
||||
size_t s = 0;
|
||||
while (s < seed.size() - p && s < rs.text.size() - p
|
||||
&& seed[seed.size() - 1 - s] == rs.text[rs.text.size() - 1 - s]) ++s;
|
||||
runCharAdv[ri].assign(rs.text.size(), 0.0);
|
||||
for (size_t i = 0; i < p; ++i) runCharAdv[ri][i] = rs.advances[i];
|
||||
for (size_t i = 0; i < s; ++i)
|
||||
runCharAdv[ri][rs.text.size() - 1 - i] = rs.advances[seed.size() - 1 - i];
|
||||
if (p + s < rs.text.size()) {
|
||||
std::string middle = rs.text.substr(p, rs.text.size() - p - s);
|
||||
auto midNat = perCharAdvances(ri, middle);
|
||||
for (size_t i = 0; i < midNat.size(); ++i) runCharAdv[ri][p + i] = midNat[i];
|
||||
}
|
||||
usedClient = true;
|
||||
spdlog::info("[ADVANCE_SEED_MERGE] run={} seedLen={} textLen={} "
|
||||
"prefixKept={} suffixKept={} (unchanged glyphs not reshaped)",
|
||||
ri, seed.size(), rs.text.size(), p, s);
|
||||
} else if (!rs.advances.empty() && !rs.text.empty()
|
||||
&& rs.advances.size() < rs.text.size()) {
|
||||
// Prefix-only advances (append without advanceSeedText).
|
||||
runCharAdv[ri] = perCharAdvances(ri, rs.text);
|
||||
for (size_t c = 0; c < rs.advances.size(); ++c) runCharAdv[ri][c] = rs.advances[c];
|
||||
usedClient = true;
|
||||
spdlog::info("[ADVANCE_PREFIX_KEEP] run={} prefixAdv={} textLen={} "
|
||||
"(unchanged prefix kept; only suffix reshaped)",
|
||||
ri, rs.advances.size(), rs.text.size());
|
||||
} else if (!rs.advances.empty() && !rs.text.empty()
|
||||
&& rs.advances.size() > rs.text.size()) {
|
||||
// Truncate (end-delete without advanceSeedText).
|
||||
runCharAdv[ri].assign(rs.advances.begin(),
|
||||
rs.advances.begin() + static_cast<std::ptrdiff_t>(rs.text.size()));
|
||||
usedClient = true;
|
||||
} else {
|
||||
// FIRST MUTATION SITE when client advances are missing/mismatched:
|
||||
// HarfBuzz recomputes advances for EVERY glyph, including unchanged ones.
|
||||
runCharAdv[ri] = perCharAdvances(ri, rs.text);
|
||||
spdlog::warn("[ADVANCE_RECOMPUTE_ALL] run={} textLen={} advLen={} "
|
||||
"FIRST_MUTATION=perCharAdvances full reshape (no client advances)",
|
||||
ri, rs.text.size(), rs.advances.size());
|
||||
}
|
||||
|
||||
if (usedClient) {
|
||||
// Client advances (esp. PDF TJ kerning) diverge from HarfBuzz naturals.
|
||||
// Emit per-glyph so those advances are applied; do NOT replace them.
|
||||
auto natural = perCharAdvances(ri, rs.text);
|
||||
bool diverges = natural.size() != runCharAdv[ri].size();
|
||||
for (size_t c = 0; !diverges && c < natural.size(); ++c)
|
||||
if (std::abs(natural[c] - runCharAdv[ri][c]) > 0.05) diverges = true;
|
||||
runPerChar[ri] = (forensicForceWholeRun ? 0 : (diverges ? 1 : 0));
|
||||
spdlog::info("[FORENSIC_RUNPERCHAR] run={} textLen={} advLen={} diverges={} forceWhole={} runPerChar={} natural0={:.4f} client0={:.4f} measureFace={}",
|
||||
ri, runs[ri].text.size(), runs[ri].advances.size(), diverges,
|
||||
spdlog::info("[FORENSIC_RUNPERCHAR] run={} textLen={} advLen={} seedLen={} diverges={} forceWhole={} runPerChar={} natural0={:.4f} client0={:.4f} measureFace={}",
|
||||
ri, rs.text.size(), rs.advances.size(), rs.advanceSeedText.size(), diverges,
|
||||
forensicForceWholeRun, (int)runPerChar[ri],
|
||||
natural.empty() ? -1.0 : natural[0],
|
||||
runCharAdv[ri].empty() ? -1.0 : runCharAdv[ri][0],
|
||||
(bool)(runFonts[ri].measureFace));
|
||||
} else {
|
||||
runCharAdv[ri] = perCharAdvances(ri, runs[ri].text);
|
||||
spdlog::info("[FORENSIC_RUNPERCHAR] run={} textLen={} advLen={} -> recomputed advances (no client match) runPerChar=0 forceWhole={}",
|
||||
ri, runs[ri].text.size(), runs[ri].advances.size(), forensicForceWholeRun);
|
||||
ri, rs.text.size(), rs.advances.size(), forensicForceWholeRun);
|
||||
}
|
||||
}
|
||||
auto charAdvAt = [&](int ri, size_t off) -> double {
|
||||
|
||||
@@ -115,6 +115,8 @@ PdfiumDocument::EmissionFont PdfiumDocument::loadEmissionFont(
|
||||
bool isSubsetFont = matchedFontInfo && matchedFontInfo->isSubset;
|
||||
if (out.resolved) {
|
||||
for (uint32_t cp : codepoints) {
|
||||
// U+0000 is a string terminator, never glyph content.
|
||||
if (cp == 0) continue;
|
||||
if (!out.resolved->hasGlyph(cp)) {
|
||||
if (isSubsetFont) {
|
||||
subsetLacksGlyphs = true;
|
||||
|
||||
@@ -16,7 +16,14 @@ function familyName(key: string): string {
|
||||
return `pdf-${(h >>> 0).toString(16)}`;
|
||||
}
|
||||
|
||||
export function loadPdfFont(documentId: string, internalFontId: string): Promise<string | null> {
|
||||
export function loadPdfFont(
|
||||
documentId: string,
|
||||
internalFontId: string,
|
||||
/** Optional CSS family name (e.g. extracted BaseFont). When set, the Face is registered under this name. */
|
||||
cssFamilyHint?: string,
|
||||
/** Optional font-weight descriptor so bold faces match style.fontWeight. */
|
||||
weight?: string | number,
|
||||
): Promise<string | null> {
|
||||
if (!internalFontId) return Promise.resolve(null);
|
||||
const key = keyOf(documentId, internalFontId);
|
||||
const existing = fontPromises.get(key);
|
||||
@@ -25,9 +32,14 @@ export function loadPdfFont(documentId: string, internalFontId: string): Promise
|
||||
const p = (async (): Promise<string | null> => {
|
||||
const bytes = await gatewayService.getFontData(documentId, internalFontId);
|
||||
if (!bytes || bytes.byteLength === 0) return null;
|
||||
const family = familyName(key);
|
||||
const hint = (cssFamilyHint || '').replace(/^[A-Z]{6}\+/, '').trim();
|
||||
const family = hint
|
||||
? hint.replace(/[^a-zA-Z0-9_-]/g, '-')
|
||||
: familyName(key);
|
||||
try {
|
||||
const face = new FontFace(family, bytes);
|
||||
const descriptors: FontFaceDescriptors = {};
|
||||
if (weight != null) descriptors.weight = String(weight);
|
||||
const face = new FontFace(family, bytes, descriptors);
|
||||
await face.load();
|
||||
document.fonts.add(face);
|
||||
fontFaces.set(key, face);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { ReflowLayout } from './pdfiumEngine';
|
||||
|
||||
export interface PageInfo {
|
||||
index: number;
|
||||
width: number;
|
||||
@@ -261,6 +263,8 @@ export interface ReflowFragment {
|
||||
fontSize: number;
|
||||
color: string;
|
||||
advances?: number[];
|
||||
/** When set, `advances` are metrics for this seed string (not necessarily `text`). */
|
||||
advanceSeedText?: string;
|
||||
}
|
||||
|
||||
export interface ReflowParagraphData {
|
||||
@@ -548,6 +552,40 @@ class GatewayService {
|
||||
return response.json();
|
||||
}
|
||||
|
||||
/** In-memory reflow preview via gateway (same engine as save). Not persisted. */
|
||||
async previewEdits(params: {
|
||||
documentId: string;
|
||||
operations: EditOperation[];
|
||||
pageIndex: number;
|
||||
dpi: number;
|
||||
yTopPt: number;
|
||||
}): Promise<{
|
||||
width: number;
|
||||
height: number;
|
||||
yTopPt: number;
|
||||
pageIndex: number;
|
||||
pngBase64: string;
|
||||
layout: ReflowLayout | null;
|
||||
} | null> {
|
||||
try {
|
||||
const response = await fetch(`${this.baseUrl}/documents/${params.documentId}/edits/preview`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
version: '1.0',
|
||||
operations: params.operations,
|
||||
pageIndex: params.pageIndex,
|
||||
dpi: params.dpi,
|
||||
yTopPt: params.yTopPt,
|
||||
}),
|
||||
});
|
||||
if (!response.ok) return null;
|
||||
return response.json();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async searchDocument(documentId: string, query: string, caseSensitive: boolean = false, wholeWords: boolean = false): Promise<SearchResult[]> {
|
||||
if (!query) return [];
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ function getModule(): Promise<PdfiumModule | null> {
|
||||
if (!modulePromise) {
|
||||
modulePromise = (async () => {
|
||||
try {
|
||||
const V = '20260630-fontfix3';
|
||||
const V = '20260730-advseed1';
|
||||
const resp = await fetch(`/pdfium-engine.mjs?v=${V}`, { cache: 'no-store' });
|
||||
if (!resp.ok) throw new Error(`pdfium-engine.mjs ${resp.status}`);
|
||||
const blobUrl = URL.createObjectURL(new Blob([await resp.text()], { type: 'text/javascript' }));
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { gatewayService } from '../lib/gatewayService';
|
||||
import { wasmLoadDocument, wasmHasDocument, wasmPreviewRenderPaginated, wasmEnsureAuxFont } from '../lib/pdfiumEngine';
|
||||
import type { ReflowLayout } from '../lib/pdfiumEngine';
|
||||
import { loadPdfFont } from '../lib/fontFaceLoader';
|
||||
|
||||
export interface OverflowPreviewRegion { pageIndex: number; yTopPt: number; dataUrl: string; }
|
||||
export interface OverflowCaret { pageIndex: number; left: number; top: number; height: number; }
|
||||
@@ -115,7 +116,9 @@ function computeLayout(para: any): ParagraphLayout {
|
||||
const deltas: number[] = [];
|
||||
for (let i = 0; i < baselines.length - 1; i++) deltas.push(baselines[i] - baselines[i + 1]);
|
||||
const domSize = seedRuns.find((r) => r.text.trim())?.size ?? 12;
|
||||
const leading = deltas.length ? Math.abs(median(deltas)) : domSize * 1.2;
|
||||
// Prefer extracted line box height over browser-ish 1.2×fontSize for single-line paras.
|
||||
const extractedLineH = lines.reduce((m: number, ln: any) => Math.max(m, ln?.h ?? 0), 0);
|
||||
const leading = deltas.length ? Math.abs(median(deltas)) : (extractedLineH > 0.5 ? extractedLineH : domSize * 1.2);
|
||||
const colW = columnRight - columnLeft;
|
||||
let align: 'left' | 'justify' = 'left';
|
||||
if (lines.length >= 2) {
|
||||
@@ -156,6 +159,82 @@ function resolveStyleEl(node: Text, root: HTMLElement): HTMLElement | null {
|
||||
|
||||
const BLOCK_TAGS = /^(DIV|P|LI|BLOCKQUOTE|PRE)$/;
|
||||
|
||||
let markerMeasureCanvas: HTMLCanvasElement | null = null;
|
||||
|
||||
function mergeSeedAdvances(
|
||||
seed: string,
|
||||
seedAdv: number[],
|
||||
text: string,
|
||||
fillWidth: number,
|
||||
): number[] {
|
||||
// Caret-only: keep LCP/LCS seed metrics; fill edited middle (measured or fallback).
|
||||
const out = new Array<number>(text.length);
|
||||
if (seedAdv.length !== seed.length) {
|
||||
for (let i = 0; i < text.length; i++) out[i] = fillWidth;
|
||||
return out;
|
||||
}
|
||||
let p = 0;
|
||||
while (p < seed.length && p < text.length && seed[p] === text[p]) {
|
||||
out[p] = seedAdv[p];
|
||||
p++;
|
||||
}
|
||||
let s = 0;
|
||||
while (
|
||||
s < seed.length - p && s < text.length - p
|
||||
&& seed[seed.length - 1 - s] === text[text.length - 1 - s]
|
||||
) {
|
||||
out[text.length - 1 - s] = seedAdv[seed.length - 1 - s];
|
||||
s++;
|
||||
}
|
||||
for (let i = p; i < text.length - s; i++) out[i] = fillWidth;
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Per-char widths via canvas (for caret). Engine uses HarfBuzz for the same middle. */
|
||||
function measureCharAdvances(
|
||||
text: string,
|
||||
sizePt: number,
|
||||
family: string,
|
||||
weight: number,
|
||||
): number[] {
|
||||
if (!text) return [];
|
||||
if (!markerMeasureCanvas) markerMeasureCanvas = document.createElement('canvas');
|
||||
const ctx = markerMeasureCanvas.getContext('2d');
|
||||
if (!ctx) return Array.from({ length: text.length }, () => sizePt * 0.5);
|
||||
ctx.font = `${weight} ${sizePt}px ${family}`;
|
||||
const out: number[] = [];
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const w = ctx.measureText(text[i]).width;
|
||||
out.push(w > 0 ? w : sizePt * 0.5);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function expandAdvancesForCaret(
|
||||
text: string,
|
||||
advances: number[] | undefined,
|
||||
seedText: string | undefined,
|
||||
sizePt: number,
|
||||
family: string,
|
||||
weight: number,
|
||||
): number[] {
|
||||
if (advances && advances.length === text.length) return advances.slice();
|
||||
const measured = measureCharAdvances(text, sizePt, family, weight);
|
||||
if (advances && seedText && advances.length === seedText.length) {
|
||||
return mergeSeedAdvances(seedText, advances, text, 0).map((a, i) =>
|
||||
a > 0 ? a : (measured[i] ?? sizePt * 0.5));
|
||||
}
|
||||
if (advances && advances.length > 0 && advances.length < text.length && text.startsWith(
|
||||
seedText && seedText.length === advances.length ? seedText : text.slice(0, advances.length),
|
||||
)) {
|
||||
const seed = seedText && seedText.length === advances.length ? seedText : text.slice(0, advances.length);
|
||||
return mergeSeedAdvances(seed, advances, text, 0).map((a, i) =>
|
||||
a > 0 ? a : (measured[i] ?? sizePt * 0.5));
|
||||
}
|
||||
if (advances && advances.length > text.length) return advances.slice(0, text.length);
|
||||
return measured;
|
||||
}
|
||||
|
||||
function extractFlatRuns(editable: HTMLElement, dominantFid: string, domSize: number, domColor: string): ReflowFragment[] {
|
||||
const out: ReflowFragment[] = [];
|
||||
const pushBreak = () => {
|
||||
@@ -180,11 +259,27 @@ function extractFlatRuns(editable: HTMLElement, dominantFid: string, domSize: nu
|
||||
if (!text) continue;
|
||||
const frag: ReflowFragment = { text, internalFontId: fid, fontSize: size, color };
|
||||
const aRaw = (child as Text).parentElement === styleEl ? styleEl?.getAttribute('data-advances') : null;
|
||||
const seedText = (child as Text).parentElement === styleEl ? styleEl?.getAttribute('data-seed-text') : null;
|
||||
if (aRaw) {
|
||||
try {
|
||||
const a = JSON.parse(aRaw) as number[];
|
||||
if (Array.isArray(a) && a.length === text.length) frag.advances = a;
|
||||
} catch { }
|
||||
if (Array.isArray(a) && a.length === text.length) {
|
||||
// Unchanged (or already expanded) — keep as-is.
|
||||
frag.advances = a;
|
||||
} else if (Array.isArray(a) && seedText && a.length === seedText.length && text !== seedText) {
|
||||
// Do NOT fill middle with size×0.5 — that locks the engine into fake advances
|
||||
// and skips HarfBuzz for new glyphs (thin/narrow typed text). Send seed metrics
|
||||
// + advanceSeedText so the engine HB-shapes only the edited middle.
|
||||
frag.advances = a;
|
||||
frag.advanceSeedText = seedText;
|
||||
} else if (Array.isArray(a) && a.length > 0 && a.length < text.length) {
|
||||
const seed = (seedText && seedText.length === a.length) ? seedText : text.slice(0, a.length);
|
||||
frag.advances = a;
|
||||
frag.advanceSeedText = seed;
|
||||
} else if (Array.isArray(a) && a.length > text.length) {
|
||||
frag.advances = a.slice(0, text.length);
|
||||
}
|
||||
} catch { /* ignore bad data-advances */ }
|
||||
}
|
||||
out.push(frag);
|
||||
}
|
||||
@@ -234,7 +329,6 @@ function splitSegmentsByBreak(runs: ReflowFragment[]): ReflowFragment[][] {
|
||||
return segs;
|
||||
}
|
||||
|
||||
let markerMeasureCanvas: HTMLCanvasElement | null = null;
|
||||
function measureTextWidth(text: string, sizePt: number, family: string): number {
|
||||
if (!markerMeasureCanvas) markerMeasureCanvas = document.createElement('canvas');
|
||||
const ctx = markerMeasureCanvas.getContext('2d');
|
||||
@@ -297,6 +391,97 @@ function globalCaretOffset(el: HTMLElement, caretRefVal?: number): number {
|
||||
return (el.textContent ?? '').length;
|
||||
}
|
||||
|
||||
/** Live caret index inside a contentEditable — matches setGlobalCaretOffset / textContent indexing. */
|
||||
function getDomCaretOffset(el: HTMLElement): number | null {
|
||||
const sel = window.getSelection();
|
||||
if (!sel || sel.rangeCount === 0) return null;
|
||||
const range = sel.getRangeAt(0);
|
||||
if (!el.contains(range.startContainer)) return null;
|
||||
const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
|
||||
let acc = 0;
|
||||
let node = walker.nextNode() as Text | null;
|
||||
while (node) {
|
||||
if (node === range.startContainer) return acc + range.startOffset;
|
||||
acc += node.textContent?.length ?? 0;
|
||||
node = walker.nextNode() as Text | null;
|
||||
}
|
||||
// Caret after a trailing <br> / at end of empty editor
|
||||
return acc;
|
||||
}
|
||||
|
||||
/** Caret metrics from typed runs (seed advances + measured middle). */
|
||||
function caretLayoutFromRuns(
|
||||
runs: ReflowFragment[],
|
||||
opts: {
|
||||
columnLeft: number;
|
||||
columnRight: number;
|
||||
firstBaselineY: number;
|
||||
leading: number;
|
||||
pageIndex: number;
|
||||
fontSize: number;
|
||||
/** Ink origin of first line (often slightly left of columnLeft). */
|
||||
anchorX0?: number;
|
||||
measureFamily?: string;
|
||||
fontWeight?: number;
|
||||
},
|
||||
): ReflowLayout {
|
||||
const lines: ReflowLayout['lines'] = [];
|
||||
const lineStartX = opts.anchorX0 ?? opts.columnLeft;
|
||||
let text = '';
|
||||
let adv: number[] = [];
|
||||
let x = lineStartX;
|
||||
let x0 = lineStartX;
|
||||
let baselineY = opts.firstBaselineY;
|
||||
const fontSize = opts.fontSize;
|
||||
const family = opts.measureFamily ?? 'Arial, sans-serif';
|
||||
const weight = opts.fontWeight ?? 400;
|
||||
const flush = () => {
|
||||
lines.push({
|
||||
baselineY,
|
||||
x0,
|
||||
fontSize,
|
||||
text,
|
||||
adv: adv.slice(),
|
||||
pageIndex: opts.pageIndex,
|
||||
});
|
||||
text = '';
|
||||
adv = [];
|
||||
x = opts.columnLeft;
|
||||
x0 = opts.columnLeft;
|
||||
baselineY -= opts.leading;
|
||||
};
|
||||
for (const r of runs) {
|
||||
if (r.text === '\n') {
|
||||
flush();
|
||||
continue;
|
||||
}
|
||||
const size = r.fontSize || fontSize;
|
||||
const runAdv = expandAdvancesForCaret(
|
||||
r.text, r.advances, r.advanceSeedText, size, family, weight,
|
||||
);
|
||||
for (let i = 0; i < r.text.length; i++) {
|
||||
const a = runAdv[i] ?? size * 0.5;
|
||||
if (text.length > 0 && x + a > opts.columnRight + 0.01) flush();
|
||||
if (text.length === 0) {
|
||||
x0 = lines.length === 0 ? lineStartX : opts.columnLeft;
|
||||
x = x0;
|
||||
}
|
||||
text += r.text[i];
|
||||
adv.push(a);
|
||||
x += a;
|
||||
}
|
||||
}
|
||||
if (text.length > 0 || lines.length === 0) flush();
|
||||
return { columnLeft: opts.columnLeft, anchorPage: opts.pageIndex, lines };
|
||||
}
|
||||
|
||||
function layoutTextMatchesEditor(lay: ReflowLayout | null, editorText: string): boolean {
|
||||
if (!lay?.lines?.length) return false;
|
||||
if (!lay.lines.every((l) => Array.isArray(l.adv) && l.adv.length === l.text.length)) return false;
|
||||
const norm = (s: string) => s.replace(/\s+/g, '');
|
||||
return norm(lay.lines.map((l) => l.text).join('')) === norm(editorText);
|
||||
}
|
||||
|
||||
function setGlobalCaretOffset(el: HTMLElement, target: number): void {
|
||||
const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
|
||||
let acc = 0;
|
||||
@@ -327,6 +512,36 @@ function lineStarts(layout: ReflowLayout, fullText: string): number[] {
|
||||
return starts;
|
||||
}
|
||||
|
||||
/** Caret layout from extracted PDF lines — no WASM reflow. Used on edit-entry so click is a visual no-op. */
|
||||
function layoutFromOrigLines(
|
||||
origLines: OrigLine[],
|
||||
columnLeft: number,
|
||||
pageIndex: number,
|
||||
fallbackSize: number,
|
||||
): ReflowLayout {
|
||||
return {
|
||||
columnLeft,
|
||||
anchorPage: pageIndex,
|
||||
lines: origLines.map((l) => {
|
||||
const text = l.frags.map((f) => f.text).join('');
|
||||
const adv = l.frags.flatMap((f) => {
|
||||
if (f.advances && f.advances.length === f.text.length) return f.advances;
|
||||
const n = f.text.length;
|
||||
const approx = (f.size || fallbackSize) * 0.5;
|
||||
return Array.from({ length: n }, () => approx);
|
||||
});
|
||||
return {
|
||||
baselineY: l.baselineY,
|
||||
x0: l.x,
|
||||
fontSize: l.frags.find((f) => f.size > 1)?.size ?? fallbackSize,
|
||||
text,
|
||||
adv: adv.length === text.length ? adv : Array.from({ length: text.length }, () => fallbackSize * 0.5),
|
||||
pageIndex,
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
export const ParagraphEditor: React.FC<ParagraphEditorProps> = ({
|
||||
documentId, pageIndex, para, pushColumnLeft, leadingOverride, alignOverride, columnLeftOverride, columnRightOverride,
|
||||
caretClick: _caretClick, heightPts, zoom, pageWidthPx, pageHeightPx, onCommit, onCommitPreview, onOverflowPreview, onOverflowCaret, onCancel,
|
||||
@@ -371,17 +586,73 @@ export const ParagraphEditor: React.FC<ParagraphEditorProps> = ({
|
||||
const domSize = domRun?.size ?? layout.seedRuns[0]?.size ?? 12;
|
||||
const domColor = domRun?.color ?? '#000000';
|
||||
const domFontName = domRun?.fontName ?? '';
|
||||
|
||||
// Extracted glyph/line metrics for overlay geometry (Fix 3 / Fix 4).
|
||||
const paraBox = useMemo(() => {
|
||||
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
||||
let ascent = 0, descent = 0;
|
||||
const bl = layout.firstBaselineY;
|
||||
for (const ln of (para?.lines ?? []) as any[]) {
|
||||
for (const r of (ln.runs ?? [])) {
|
||||
for (const g of (r.glyphs ?? [])) {
|
||||
const x0 = g.bbox_x, y0 = g.bbox_y, x1 = g.bbox_x + g.bbox_w, y1 = g.bbox_y + g.bbox_h;
|
||||
minX = Math.min(minX, x0); minY = Math.min(minY, y0);
|
||||
maxX = Math.max(maxX, x1); maxY = Math.max(maxY, y1);
|
||||
const gbl = g.origin_y ?? ln.baseline_y ?? bl;
|
||||
ascent = Math.max(ascent, y1 - gbl);
|
||||
descent = Math.max(descent, gbl - y0);
|
||||
}
|
||||
}
|
||||
}
|
||||
const lineH = (para?.lines ?? []).reduce((m: number, ln: any) => Math.max(m, ln?.h ?? 0), 0);
|
||||
if (!isFinite(minX)) {
|
||||
return { x: layout.columnLeft, y: bl - domSize * 0.8, w: Math.max(0, columnRight - layout.columnLeft),
|
||||
h: lineH || domSize * 1.2, ascent: domSize * 0.8, descent: domSize * 0.2, lineHeight: lineH || domSize * 1.2 };
|
||||
}
|
||||
return {
|
||||
x: minX, y: minY, w: maxX - minX, h: maxY - minY,
|
||||
ascent: ascent || domSize * 0.8, descent, lineHeight: lineH > 0.5 ? lineH : (maxY - minY),
|
||||
};
|
||||
}, [para, layout.columnLeft, layout.firstBaselineY, columnRight, domSize]);
|
||||
|
||||
// Fix 3: line-height from extracted metrics (not size×1.2 / browser normal).
|
||||
const lineHeightPt = leadingOverride ?? paraBox.lineHeight;
|
||||
const fontPx = domSize * zoom;
|
||||
const leadingPx = leading * zoom;
|
||||
const leadingPx = lineHeightPt * zoom;
|
||||
// Fix 4: overlay geometry matches extracted paragraph bbox (not page column).
|
||||
const overlayLeftPx = paraBox.x * zoom;
|
||||
const overlayWidthPx = Math.max(paraBox.w, 1) * zoom;
|
||||
const overlayHeightPx = Math.max(paraBox.h, lineHeightPt) * zoom;
|
||||
const firstBaselineScreen = (heightPts - layout.firstBaselineY) * zoom;
|
||||
const editorTop = firstBaselineScreen - paraBox.ascent * zoom;
|
||||
const bandTop = Math.max(0, Math.min(editorTop - leadingPx * 0.5, firstBaselineScreen - fontPx * 1.15));
|
||||
// Keep column metrics for reflow payload / toolbar (editing column can be wider than ink bbox).
|
||||
const colLeftPx = columnLeft * zoom;
|
||||
const colWidthPx = (columnRight - columnLeft) * zoom;
|
||||
const colHeightPx = layout.oldLineCount * leadingPx;
|
||||
const firstBaselineScreen = (heightPts - layout.firstBaselineY) * zoom;
|
||||
const editorTop = firstBaselineScreen - fontPx * 0.8;
|
||||
const bandTop = Math.max(0, Math.min(editorTop - leadingPx * 0.5, firstBaselineScreen - fontPx * 1.15));
|
||||
|
||||
const measureFamily = /times|serif/i.test(domFontName) ? 'Times New Roman, serif'
|
||||
: /courier|mono/i.test(domFontName) ? 'Courier New, monospace' : 'Arial, sans-serif';
|
||||
// Fix 1: use extracted PDF font name (loaded via @font-face), not a generic Arial/Times/Courier map.
|
||||
const extractedFamily = (domFontName || '').replace(/^[A-Z]{6}\+/, '').trim() || 'sans-serif';
|
||||
const fallbackFamily = /times|serif/i.test(extractedFamily) ? 'Times New Roman, serif'
|
||||
: /courier|mono/i.test(extractedFamily) ? 'Courier New, monospace' : 'Arial, sans-serif';
|
||||
const [measureFamily, setMeasureFamily] = useState(
|
||||
extractedFamily !== 'sans-serif' ? `'${extractedFamily}', ${fallbackFamily}` : fallbackFamily,
|
||||
);
|
||||
|
||||
// Fix 2: preserve PDF bold/italic weight (BoldMT etc. previously fell through as 400).
|
||||
const extractedFontWeight: number = /bold|black|heavy|semibold|demibold/i.test(extractedFamily)
|
||||
|| /_B(?:old)?(?:_|$)/i.test(dominantFid)
|
||||
? 700
|
||||
: 400;
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
if (!dominantFid || !documentId) return;
|
||||
loadPdfFont(documentId, dominantFid, extractedFamily, extractedFontWeight).then((family) => {
|
||||
if (cancelled || !family) return;
|
||||
setMeasureFamily(`'${family}', ${fallbackFamily}`);
|
||||
});
|
||||
return () => { cancelled = true; };
|
||||
}, [documentId, dominantFid, extractedFamily, fallbackFamily, extractedFontWeight]);
|
||||
|
||||
const buildReflowData = (runs: ReflowFragment[], origLines?: OrigLine[]) => {
|
||||
const listActive = listKind !== null;
|
||||
@@ -474,6 +745,9 @@ export const ParagraphEditor: React.FC<ParagraphEditorProps> = ({
|
||||
const el = editRef.current, lay = engineLayoutRef.current;
|
||||
if (!el || !lay) return;
|
||||
const fullText = el.textContent ?? '';
|
||||
// Always follow the live contentEditable caret — caretIndexRef alone stays stuck at last click.
|
||||
const live = getDomCaretOffset(el);
|
||||
if (typeof live === 'number') caretIndexRef.current = live;
|
||||
const idx = typeof caretIndexRef.current === 'number' ? caretIndexRef.current : fullText.length;
|
||||
const box = caretBoxFor(idx, lay, fullText);
|
||||
if (box && box.pageIndex !== pageIndex) {
|
||||
@@ -495,105 +769,138 @@ export const ParagraphEditor: React.FC<ParagraphEditorProps> = ({
|
||||
const renderPreview = async () => {
|
||||
const el = editRef.current;
|
||||
if (!el) return;
|
||||
const fids = new Set<string>([dominantFid, ...layout.seedRuns.map((r) => r.fid)].filter(Boolean));
|
||||
await Promise.all([...fids].map((f) => wasmEnsureAuxFont(documentId, f)));
|
||||
|
||||
// Edit-entry (click before typing): do NOT run identity reflow.
|
||||
// Reflow rebuilds text objects (often runPerChar) and looks different from the PDF
|
||||
// even when the string is unchanged. Keep the page bitmap visible; caret uses extracted metrics.
|
||||
if (!editedRef.current) {
|
||||
const origLay = layoutFromOrigLines(layout.origLines, columnLeft, pageIndex, domSize);
|
||||
engineLayoutRef.current = origLay;
|
||||
if (!initialCaretApplied.current) {
|
||||
initialCaretApplied.current = true;
|
||||
const fullLen = (el.textContent ?? '').length;
|
||||
setGlobalCaretOffset(el, fullLen);
|
||||
}
|
||||
positionCaret();
|
||||
setHasPreview(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Typing preview: use gateway (same engine as save). Browser WASM is stale and
|
||||
// changes font/width/height on keystroke; save then snaps back to the native look.
|
||||
const dpr = typeof window !== 'undefined' ? (window.devicePixelRatio || 1) : 1;
|
||||
const dpi = Math.round(96 * zoom * dpr);
|
||||
const origLines = editedRef.current ? undefined : layout.origLines;
|
||||
const opJson = buildOpJson(extractFlatRuns(el, dominantFid, domSize, domColor), origLines);
|
||||
|
||||
console.log('[STAGE_1_EDITABLE_RUN]', { dominantFid, domSize, fontPx, domFontName, edited: editedRef.current });
|
||||
console.log('[STAGE_2_RENDER_PREVIEW_PAYLOAD]', opJson);
|
||||
|
||||
const runs = extractFlatRuns(el, dominantFid, domSize, domColor);
|
||||
const data = buildReflowData(runs);
|
||||
const yTopPt = bandTop / zoom;
|
||||
const { regions, layout: rawLay } = await wasmPreviewRenderPaginated(documentId, pageIndex, dpi, opJson, yTopPt);
|
||||
const operations = [{
|
||||
id: 'preview',
|
||||
type: 'reflow_paragraph' as const,
|
||||
pageIndex,
|
||||
data,
|
||||
}];
|
||||
|
||||
console.log('[STAGE_7_PREVIEW_DRAW]', { fontSize: domSize, fontPx, regionsCount: regions?.length ?? 0 });
|
||||
console.log('[STAGE_1_EDITABLE_RUN]', { dominantFid, domSize, fontPx, domFontName, edited: true });
|
||||
console.log('[STAGE_2_RENDER_PREVIEW_PAYLOAD]', { operations });
|
||||
|
||||
console.log('[KEYSTROKE_FONT_METRICS_DEBUG]', {
|
||||
fontName: domFontName,
|
||||
internalFontId: dominantFid,
|
||||
fontSize: domSize,
|
||||
fontSizePx: fontPx,
|
||||
lineHeight: leading,
|
||||
lineHeightPx: leadingPx,
|
||||
edited: editedRef.current,
|
||||
origLinesProvided: !editedRef.current,
|
||||
const preview = await gatewayService.previewEdits({
|
||||
documentId,
|
||||
operations,
|
||||
pageIndex,
|
||||
dpi,
|
||||
yTopPt,
|
||||
});
|
||||
|
||||
console.log('[KEYSTROKE_LAYOUT_DEBUG]', {
|
||||
columnLeft,
|
||||
columnRight,
|
||||
columnWidth: columnRight - columnLeft,
|
||||
paragraphWidth: colWidthPx / zoom,
|
||||
firstBaselineY: layout.firstBaselineY,
|
||||
lineCount: rawLay?.lines?.length ?? 0,
|
||||
wrapPosition: rawLay?.lines?.map((l, idx) => ({
|
||||
lineIndex: idx,
|
||||
text: l.text,
|
||||
x0: l.x0,
|
||||
advanceWidthSum: l.adv?.reduce((a, b) => a + b, 0) ?? 0,
|
||||
lineRightX: l.x0 + (l.adv?.reduce((a, b) => a + b, 0) ?? 0),
|
||||
})),
|
||||
paragraphBounds: {
|
||||
let width = 0;
|
||||
let height = 0;
|
||||
let rawLay: ReflowLayout | null = null;
|
||||
let drawSource: HTMLCanvasElement | HTMLImageElement | null = null;
|
||||
let usedGateway = false;
|
||||
|
||||
if (preview?.pngBase64 && preview.width > 0 && preview.height > 0) {
|
||||
try {
|
||||
const img = new Image();
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
img.onload = () => resolve();
|
||||
img.onerror = () => reject(new Error('preview png decode failed'));
|
||||
img.src = `data:image/png;base64,${preview.pngBase64}`;
|
||||
});
|
||||
width = preview.width;
|
||||
height = preview.height;
|
||||
rawLay = preview.layout;
|
||||
drawSource = img;
|
||||
usedGateway = true;
|
||||
onOverflowPreview?.([]);
|
||||
} catch {
|
||||
usedGateway = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!usedGateway) {
|
||||
// Fallback only if gateway preview is unavailable.
|
||||
const fids = new Set<string>([dominantFid, ...layout.seedRuns.map((r) => r.fid)].filter(Boolean));
|
||||
if (!wasmHasDocument(documentId)) {
|
||||
const bytes = await gatewayService.getDocumentRaw(documentId);
|
||||
if (bytes) await wasmLoadDocument(documentId, bytes);
|
||||
}
|
||||
if (!wasmHasDocument(documentId)) return;
|
||||
await Promise.all([...fids].map((f) => wasmEnsureAuxFont(documentId, f)));
|
||||
const opJson = buildOpJson(runs);
|
||||
const { regions, layout: wasmLay } = await wasmPreviewRenderPaginated(documentId, pageIndex, dpi, opJson, yTopPt);
|
||||
if (regions.length === 0) return;
|
||||
rawLay = wasmLay;
|
||||
const r0 = regions[0];
|
||||
width = r0.width;
|
||||
height = r0.height;
|
||||
if (!r0.rgba || width <= 0 || height <= 0) return;
|
||||
const offscreen = document.createElement('canvas');
|
||||
offscreen.width = width;
|
||||
offscreen.height = height;
|
||||
const offCtx = offscreen.getContext('2d');
|
||||
if (!offCtx) return;
|
||||
const imgData = offCtx.createImageData(width, height);
|
||||
imgData.data.set(r0.rgba);
|
||||
offCtx.putImageData(imgData, 0, 0);
|
||||
drawSource = offscreen;
|
||||
onOverflowPreview?.(regions.slice(1).map((rg) => ({
|
||||
pageIndex: rg.pageIndex, yTopPt: rg.yTopPt, dataUrl: rgbaToDataUrl(rg.rgba, rg.width, rg.height),
|
||||
})));
|
||||
}
|
||||
|
||||
console.log('[STAGE_7_PREVIEW_DRAW]', {
|
||||
fontSize: domSize,
|
||||
fontPx,
|
||||
via: usedGateway ? 'gateway' : 'wasm-fallback',
|
||||
width,
|
||||
height,
|
||||
});
|
||||
|
||||
// Gateway PNG is authoritative for glyphs; caret needs per-glyph advances.
|
||||
// Only trust STAGE_5 (layoutSource=reflow). Extract layouts use bbox widths and lag the caret.
|
||||
const anchorX0 = layout.origLines[0]?.x ?? columnLeft;
|
||||
const caretOpts = {
|
||||
columnLeft,
|
||||
columnRight,
|
||||
firstBaselineY: layout.firstBaselineY,
|
||||
leading,
|
||||
oldLineCount: layout.oldLineCount,
|
||||
},
|
||||
});
|
||||
|
||||
if (regions.length === 0) { return; }
|
||||
// Clamp font sizes in the returned layout to guard against the WASM matrix-override bug
|
||||
// (see sanitizeEngineLayout for full explanation). This keeps the caret at the correct
|
||||
// position and prevents the bounding box from visually shrinking on click or first keystroke.
|
||||
const lay = sanitizeEngineLayout(rawLay, domSize);
|
||||
pageIndex,
|
||||
fontSize: domSize,
|
||||
anchorX0,
|
||||
measureFamily,
|
||||
fontWeight: extractedFontWeight,
|
||||
};
|
||||
const engineLay = rawLay as (ReflowLayout & { layoutSource?: string }) | null;
|
||||
const caretLay = usedGateway
|
||||
? (engineLay?.layoutSource === 'reflow' && layoutTextMatchesEditor(engineLay, el.textContent ?? '')
|
||||
? engineLay
|
||||
: caretLayoutFromRuns(runs, caretOpts))
|
||||
: rawLay;
|
||||
const lay = sanitizeEngineLayout(caretLay, domSize);
|
||||
engineLayoutRef.current = lay;
|
||||
const r0 = regions[0];
|
||||
const { rgba, width, height } = r0;
|
||||
if (!rgba || width <= 0 || height <= 0) { return; }
|
||||
const cv = previewCanvasRef.current;
|
||||
if (cv) {
|
||||
const rect = cv.getBoundingClientRect();
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
if (cv && drawSource) {
|
||||
const displayW = pageWidthPx;
|
||||
const displayH = Math.max(0, pageHeightPx - bandTop);
|
||||
|
||||
console.log('[OVERLAY_CANVAS_VERIFICATION]', {
|
||||
canvasWidth: cv.width,
|
||||
canvasHeight: cv.height,
|
||||
cssClientWidth: rect.width,
|
||||
cssClientHeight: rect.height,
|
||||
styleWidth: cv.style.width,
|
||||
styleHeight: cv.style.height,
|
||||
devicePixelRatio: dpr,
|
||||
regionBitmapWidth: width,
|
||||
regionBitmapHeight: height,
|
||||
displayW,
|
||||
displayH,
|
||||
});
|
||||
|
||||
console.log('[ALL_DOM_CANVASES_INSPECTION]', Array.from(document.querySelectorAll('canvas')).map((c, i) => {
|
||||
const r = c.getBoundingClientRect();
|
||||
const cs = getComputedStyle(c);
|
||||
return {
|
||||
index: i,
|
||||
pageIdx: c.getAttribute('data-page-index'),
|
||||
width: c.width,
|
||||
height: c.height,
|
||||
clientWidth: r.width,
|
||||
clientHeight: r.height,
|
||||
top: Math.round(r.top),
|
||||
left: Math.round(r.left),
|
||||
zIndex: cs.zIndex,
|
||||
opacity: cs.opacity,
|
||||
visibility: cs.visibility,
|
||||
display: cs.display,
|
||||
pointerEvents: cs.pointerEvents,
|
||||
};
|
||||
}));
|
||||
|
||||
const targetW = Math.round(displayW * dpr);
|
||||
const targetH = Math.round(displayH * dpr);
|
||||
if (cv.width !== targetW) cv.width = targetW;
|
||||
@@ -603,26 +910,13 @@ export const ParagraphEditor: React.FC<ParagraphEditorProps> = ({
|
||||
|
||||
const ctx = cv.getContext('2d');
|
||||
if (ctx) {
|
||||
const offscreen = document.createElement('canvas');
|
||||
offscreen.width = width;
|
||||
offscreen.height = height;
|
||||
const offCtx = offscreen.getContext('2d');
|
||||
if (offCtx) {
|
||||
const imgData = offCtx.createImageData(width, height);
|
||||
imgData.data.set(rgba);
|
||||
offCtx.putImageData(imgData, 0, 0);
|
||||
|
||||
ctx.clearRect(0, 0, cv.width, cv.height);
|
||||
ctx.save();
|
||||
ctx.scale(dpr, dpr);
|
||||
ctx.drawImage(offscreen, 0, 0, displayW, displayH);
|
||||
ctx.drawImage(drawSource, 0, 0, displayW, displayH);
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
}
|
||||
onOverflowPreview?.(regions.slice(1).map((rg) => ({
|
||||
pageIndex: rg.pageIndex, yTopPt: rg.yTopPt, dataUrl: rgbaToDataUrl(rg.rgba, rg.width, rg.height),
|
||||
})));
|
||||
if (!hasPreview) setHasPreview(true);
|
||||
if (!initialCaretApplied.current) {
|
||||
initialCaretApplied.current = true;
|
||||
@@ -648,13 +942,13 @@ export const ParagraphEditor: React.FC<ParagraphEditorProps> = ({
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
// Edit-entry caret is sync (no reflow). Preload WASM in background for first keystroke.
|
||||
renderPreview();
|
||||
(async () => {
|
||||
if (!wasmHasDocument(documentId)) {
|
||||
const bytes = await gatewayService.getDocumentRaw(documentId);
|
||||
if (bytes) await wasmLoadDocument(documentId, bytes);
|
||||
if (bytes && !cancelled) await wasmLoadDocument(documentId, bytes);
|
||||
}
|
||||
if (cancelled) return;
|
||||
renderPreview();
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
@@ -669,33 +963,209 @@ export const ParagraphEditor: React.FC<ParagraphEditorProps> = ({
|
||||
const el = editRef.current;
|
||||
if (!el) return;
|
||||
el.innerHTML = '';
|
||||
for (const r of layout.seedRuns) {
|
||||
// Preserve PDF hard line breaks (not a single CSS-wrapped flow).
|
||||
const lineFrags = layout.origLines.length
|
||||
? layout.origLines.map((l) => l.frags.map((f) => ({
|
||||
text: f.text, fid: f.fid, size: f.size, color: f.color, fontName: domFontName, advances: f.advances,
|
||||
})))
|
||||
: [layout.seedRuns];
|
||||
lineFrags.forEach((frags, li) => {
|
||||
if (li > 0) el.appendChild(document.createElement('br'));
|
||||
for (const r of frags) {
|
||||
if (!r.text) continue;
|
||||
const effSize = !r.text.trim() || r.size <= 1 ? domSize : r.size;
|
||||
const span = document.createElement('span');
|
||||
span.setAttribute('data-fid', r.fid || dominantFid);
|
||||
span.setAttribute('data-size', String(effSize));
|
||||
span.setAttribute('data-color', r.color);
|
||||
span.setAttribute('data-fontname', r.fontName);
|
||||
if (r.advances && r.advances.length === r.text.length) span.setAttribute('data-advances', JSON.stringify(r.advances));
|
||||
span.setAttribute('data-fontname', ('fontName' in r ? (r as { fontName?: string }).fontName : domFontName) || '');
|
||||
if (r.advances && r.advances.length === r.text.length) {
|
||||
span.setAttribute('data-advances', JSON.stringify(r.advances));
|
||||
span.setAttribute('data-seed-text', r.text);
|
||||
}
|
||||
span.style.fontSize = `${effSize * zoom}px`;
|
||||
span.style.color = 'transparent';
|
||||
(span.style as CSSStyleDeclaration & { webkitTextFillColor?: string }).webkitTextFillColor = 'transparent';
|
||||
span.textContent = r.text;
|
||||
el.appendChild(span);
|
||||
}
|
||||
});
|
||||
el.focus();
|
||||
initialTextRef.current = normalizeForCompare(domTextWithBreaks(el));
|
||||
// Caret layout after DOM seed is ready (documentId effect may have run first on an empty editor).
|
||||
if (!editedRef.current) {
|
||||
engineLayoutRef.current = layoutFromOrigLines(layout.origLines, columnLeft, pageIndex, domSize);
|
||||
initialCaretApplied.current = true;
|
||||
setGlobalCaretOffset(el, (el.textContent ?? '').length);
|
||||
positionCaret();
|
||||
setHasPreview(true);
|
||||
}
|
||||
|
||||
// --- Edit-entry overlay vs extracted PDF paragraph (no reflow / no typing) ---
|
||||
try {
|
||||
const lines = (para?.lines ?? []) as any[];
|
||||
let pdfMinX = Infinity, pdfMinY = Infinity, pdfMaxX = -Infinity, pdfMaxY = -Infinity;
|
||||
let pdfBaseline = layout.firstBaselineY;
|
||||
let pdfAscent = 0, pdfDescent = 0;
|
||||
let pdfFontName = domFontName, pdfFontSize = domSize;
|
||||
for (const ln of lines) {
|
||||
for (const r of (ln.runs ?? [])) {
|
||||
pdfFontName = r.font_name || pdfFontName;
|
||||
pdfFontSize = Math.max(pdfFontSize, r.font_size ?? 0, r.h ?? 0);
|
||||
for (const g of (r.glyphs ?? [])) {
|
||||
const x0 = g.bbox_x, y0 = g.bbox_y, x1 = g.bbox_x + g.bbox_w, y1 = g.bbox_y + g.bbox_h;
|
||||
pdfMinX = Math.min(pdfMinX, x0); pdfMinY = Math.min(pdfMinY, y0);
|
||||
pdfMaxX = Math.max(pdfMaxX, x1); pdfMaxY = Math.max(pdfMaxY, y1);
|
||||
const bl = g.origin_y ?? ln.baseline_y ?? pdfBaseline;
|
||||
pdfAscent = Math.max(pdfAscent, y1 - bl);
|
||||
pdfDescent = Math.max(pdfDescent, bl - y0);
|
||||
}
|
||||
}
|
||||
}
|
||||
const pdfW = isFinite(pdfMaxX) ? pdfMaxX - pdfMinX : 0;
|
||||
const pdfH = isFinite(pdfMaxY) ? pdfMaxY - pdfMinY : 0;
|
||||
const pdfLineH = lines[0]?.h ?? pdfH;
|
||||
const expectedBold = /bold|black|heavy/i.test(pdfFontName) || /Bold/i.test(dominantFid);
|
||||
|
||||
// Wait one frame so layout/scroll metrics settle after focus + children.
|
||||
requestAnimationFrame(() => {
|
||||
const cs = window.getComputedStyle(el);
|
||||
const rect = el.getBoundingClientRect();
|
||||
const pdf = {
|
||||
bbox: { x: pdfMinX, y: pdfMinY, w: pdfW, h: pdfH },
|
||||
lineHeightPt: pdfLineH,
|
||||
ascentPt: pdfAscent,
|
||||
descentPt: pdfDescent,
|
||||
widthPt: pdfW,
|
||||
heightPt: pdfH,
|
||||
fontName: pdfFontName,
|
||||
fontSizePt: pdfFontSize,
|
||||
baselineY: pdfBaseline,
|
||||
expectedFontWeight: expectedBold ? 700 : 400,
|
||||
};
|
||||
const overlayApplied = {
|
||||
fontFamily: measureFamily,
|
||||
fontSizePx: fontPx,
|
||||
fontSizePt: domSize,
|
||||
fontWeight: extractedFontWeight,
|
||||
lineHeightPx: leadingPx,
|
||||
lineHeightPt: lineHeightPt,
|
||||
letterSpacing: '(not set)',
|
||||
widthPx: overlayWidthPx,
|
||||
widthPt: paraBox.w,
|
||||
heightPx: overlayHeightPx,
|
||||
heightPt: paraBox.h,
|
||||
topPx: editorTop,
|
||||
leftPx: overlayLeftPx,
|
||||
ascentHeuristicPt: paraBox.ascent,
|
||||
transform: 'none',
|
||||
};
|
||||
const overlayMeasured = {
|
||||
fontFamily: cs.fontFamily,
|
||||
fontSize: cs.fontSize,
|
||||
fontWeight: cs.fontWeight,
|
||||
lineHeight: cs.lineHeight,
|
||||
letterSpacing: cs.letterSpacing,
|
||||
width: cs.width,
|
||||
height: cs.height,
|
||||
transform: cs.transform,
|
||||
scrollWidth: el.scrollWidth,
|
||||
scrollHeight: el.scrollHeight,
|
||||
clientWidth: el.clientWidth,
|
||||
clientHeight: el.clientHeight,
|
||||
boundingRect: { width: rect.width, height: rect.height, top: rect.top, left: rect.left },
|
||||
};
|
||||
|
||||
const checks: { property: string; pdf: string | number; overlay: string | number; match: boolean }[] = [];
|
||||
const pdfFamilyKey = (pdf.fontName || '').replace(/^[A-Z]{6}\+/, '');
|
||||
const familyApplied = measureFamily;
|
||||
const familyMatches =
|
||||
!!pdfFamilyKey
|
||||
&& (familyApplied.includes(pdfFamilyKey)
|
||||
|| familyApplied.replace(/['"]/g, '').split(',')[0].trim() === pdfFamilyKey);
|
||||
checks.push({
|
||||
property: 'font-family',
|
||||
pdf: pdf.fontName,
|
||||
overlay: familyApplied,
|
||||
match: familyMatches,
|
||||
});
|
||||
checks.push({
|
||||
property: 'font-weight',
|
||||
pdf: pdf.expectedFontWeight,
|
||||
overlay: parseInt(cs.fontWeight, 10) || cs.fontWeight,
|
||||
match: !expectedBold || (parseInt(cs.fontWeight, 10) || 0) >= 600,
|
||||
});
|
||||
checks.push({
|
||||
property: 'font-size (pt)',
|
||||
pdf: pdf.fontSizePt,
|
||||
overlay: overlayApplied.fontSizePt,
|
||||
match: Math.abs(pdf.fontSizePt - overlayApplied.fontSizePt) < 0.05,
|
||||
});
|
||||
checks.push({
|
||||
property: 'line-height / leading (pt)',
|
||||
// Multi-line CSS uses baseline gap (leadingOverride); ink line.h is not the CSS line-height.
|
||||
pdf: leadingOverride ?? pdf.lineHeightPt,
|
||||
overlay: overlayApplied.lineHeightPt,
|
||||
match: Math.abs((leadingOverride ?? pdf.lineHeightPt) - overlayApplied.lineHeightPt) < 0.5,
|
||||
});
|
||||
checks.push({
|
||||
property: 'ascent used for overlay top (pt)',
|
||||
pdf: pdf.ascentPt,
|
||||
overlay: overlayApplied.ascentHeuristicPt,
|
||||
match: Math.abs(pdf.ascentPt - overlayApplied.ascentHeuristicPt) < 0.5,
|
||||
});
|
||||
checks.push({
|
||||
property: 'height (pt)',
|
||||
pdf: pdf.heightPt,
|
||||
overlay: overlayApplied.heightPt,
|
||||
match: Math.abs(pdf.heightPt - overlayApplied.heightPt) < 0.5,
|
||||
});
|
||||
checks.push({
|
||||
property: 'width (pt)',
|
||||
pdf: pdf.widthPt,
|
||||
overlay: overlayApplied.widthPt,
|
||||
match: Math.abs(pdf.widthPt - overlayApplied.widthPt) < 1.0,
|
||||
});
|
||||
checks.push({
|
||||
property: 'letter-spacing',
|
||||
pdf: '0 / normal',
|
||||
overlay: cs.letterSpacing,
|
||||
match: cs.letterSpacing === 'normal' || cs.letterSpacing === '0px',
|
||||
});
|
||||
checks.push({
|
||||
property: 'transform',
|
||||
pdf: 'none',
|
||||
overlay: cs.transform,
|
||||
match: !cs.transform || cs.transform === 'none',
|
||||
});
|
||||
|
||||
const first = checks.find((c) => !c.match) ?? null;
|
||||
console.log('[EDIT_ENTRY_OVERLAY_COMPARE]', {
|
||||
pdf,
|
||||
overlayApplied,
|
||||
overlayMeasured,
|
||||
checks,
|
||||
firstPropertyThatChanges: first,
|
||||
});
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn('[EDIT_ENTRY_OVERLAY_COMPARE] failed', e);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const fallbackVisible = wasmFailed && !hasPreview;
|
||||
// Never paint HTML over the PDF on edit-entry. Fallback HTML was a common
|
||||
// source of font/width/leading jumps when WASM was slow or unavailable.
|
||||
const fallbackVisible = false;
|
||||
useEffect(() => {
|
||||
const el = editRef.current;
|
||||
if (!el) return;
|
||||
el.querySelectorAll<HTMLElement>('span[data-fid]').forEach((span) => {
|
||||
span.style.color = fallbackVisible ? (span.getAttribute('data-color') ?? '#000000') : 'transparent';
|
||||
span.style.color = 'transparent';
|
||||
(span.style as CSSStyleDeclaration & { webkitTextFillColor?: string }).webkitTextFillColor = 'transparent';
|
||||
});
|
||||
}, [fallbackVisible]);
|
||||
|
||||
// Diagnostics only — does not drive a visible HTML fallback.
|
||||
useEffect(() => {
|
||||
if (hasPreview) return;
|
||||
const t = window.setTimeout(() => setWasmFailed(true), 4000);
|
||||
@@ -703,11 +1173,30 @@ export const ParagraphEditor: React.FC<ParagraphEditorProps> = ({
|
||||
}, [hasPreview]);
|
||||
|
||||
const composingRef = useRef(false);
|
||||
const ignoreInputUntilRef = useRef(Date.now() + 150);
|
||||
|
||||
const onInput = () => {
|
||||
// Ignore spurious input events from contentEditable population / focus.
|
||||
if (Date.now() < ignoreInputUntilRef.current) return;
|
||||
editedRef.current = true;
|
||||
if (!edited) setEdited(true);
|
||||
if (composingRef.current) return;
|
||||
// Optimistic caret layout so the cursor tracks keystrokes while gateway preview is in flight.
|
||||
const elNow = editRef.current;
|
||||
if (elNow) {
|
||||
const liveRuns = extractFlatRuns(elNow, dominantFid, domSize, domColor);
|
||||
engineLayoutRef.current = caretLayoutFromRuns(liveRuns, {
|
||||
columnLeft,
|
||||
columnRight,
|
||||
firstBaselineY: layout.firstBaselineY,
|
||||
leading,
|
||||
pageIndex,
|
||||
fontSize: domSize,
|
||||
anchorX0: layout.origLines[0]?.x ?? columnLeft,
|
||||
measureFamily,
|
||||
fontWeight: extractedFontWeight,
|
||||
});
|
||||
}
|
||||
positionCaret();
|
||||
scheduleRender();
|
||||
};
|
||||
@@ -788,7 +1277,7 @@ export const ParagraphEditor: React.FC<ParagraphEditorProps> = ({
|
||||
{fallbackVisible && (
|
||||
<div
|
||||
className="absolute z-[36] bg-white"
|
||||
style={{ left: colLeftPx - 2, top: editorTop - 2, width: colWidthPx + 4, height: colHeightPx + 4 }}
|
||||
style={{ left: overlayLeftPx - 2, top: editorTop - 2, width: overlayWidthPx + 4, height: overlayHeightPx + 4 }}
|
||||
/>
|
||||
)}
|
||||
<canvas
|
||||
@@ -867,12 +1356,15 @@ export const ParagraphEditor: React.FC<ParagraphEditorProps> = ({
|
||||
onPaste={(e) => { e.preventDefault(); document.execCommand('insertText', false, e.clipboardData.getData('text/plain')); }}
|
||||
className="absolute z-[38] outline-none"
|
||||
style={{
|
||||
left: colLeftPx, top: editorTop, width: colWidthPx, height: colHeightPx,
|
||||
left: overlayLeftPx, top: editorTop, width: overlayWidthPx, height: overlayHeightPx,
|
||||
fontSize: `${fontPx}px`, lineHeight: `${leadingPx}px`, fontFamily: measureFamily,
|
||||
caretColor: fallbackVisible ? '#2563eb' : 'transparent',
|
||||
color: fallbackVisible ? undefined : 'transparent',
|
||||
background: fallbackVisible ? '#ffffff' : 'transparent',
|
||||
whiteSpace: 'pre-wrap', wordBreak: 'break-all', overflow: 'hidden',
|
||||
fontWeight: extractedFontWeight,
|
||||
caretColor: 'transparent',
|
||||
color: 'transparent',
|
||||
// WebKit may ignore `color: transparent` on contentEditable without this.
|
||||
WebkitTextFillColor: 'transparent',
|
||||
background: 'transparent',
|
||||
whiteSpace: 'pre-wrap', wordBreak: 'normal', overflowWrap: 'break-word', overflow: 'hidden',
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -30,6 +30,8 @@ export interface ReflowFragment {
|
||||
fontSize: number;
|
||||
color: string;
|
||||
advances?: number[];
|
||||
/** When set, `advances` are metrics for this seed string (not necessarily `text`). */
|
||||
advanceSeedText?: string;
|
||||
}
|
||||
|
||||
export interface CommitFrame {
|
||||
|
||||
@@ -249,6 +249,7 @@ class ReflowRun(BaseModel):
|
||||
fontSize: float
|
||||
color: str = "#000000"
|
||||
advances: list[float] | None = None
|
||||
advanceSeedText: str | None = None
|
||||
|
||||
|
||||
class ReflowParagraphData(BaseModel):
|
||||
@@ -460,6 +461,155 @@ def apply_edits(document_id: str, request: EditsRequest):
|
||||
return apply_edits_impl(document_id, request)
|
||||
|
||||
|
||||
class PreviewEditsBody(EditsRequest):
|
||||
"""Same ops as apply_edits, plus preview render parameters. Not persisted."""
|
||||
pageIndex: int = Field(0, ge=0)
|
||||
dpi: int = Field(144, ge=36, le=600)
|
||||
yTopPt: float = 0.0
|
||||
|
||||
|
||||
@router.post("/preview")
|
||||
def preview_edits(document_id: str, request: PreviewEditsBody):
|
||||
"""Apply edits on a throwaway copy and return a page-region PNG.
|
||||
|
||||
Live typing must preview through this path (same engine as save), not the
|
||||
stale browser WASM — otherwise font/width jump on keystroke then snap back on save.
|
||||
"""
|
||||
import base64
|
||||
import io
|
||||
|
||||
if not engine.is_available():
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_501_NOT_IMPLEMENTED,
|
||||
detail="Engine bridge (bindings/python) not yet available.",
|
||||
)
|
||||
|
||||
doc_info = document_store.get_document(document_id)
|
||||
if not doc_info:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Document not found")
|
||||
|
||||
perms = doc_info.get("permissions") or {}
|
||||
for op in request.operations:
|
||||
required = _OP_PERMISSION.get(op.type)
|
||||
if required and perms.get(required, True) is False:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail=f"Operation '{op.type}' is not permitted ({required}).",
|
||||
)
|
||||
|
||||
try:
|
||||
pdfengine = engine.require()
|
||||
req_dict = request.model_dump(exclude_none=True)
|
||||
# Strip preview-only fields before apply_edits JSON
|
||||
page_index = int(req_dict.pop("pageIndex", 0))
|
||||
dpi = int(req_dict.pop("dpi", 144))
|
||||
y_top = float(req_dict.pop("yTopPt", 0.0))
|
||||
edits_json = json.dumps(req_dict)
|
||||
doc_copy = pdfengine.PdfDocument.load_from_memory(doc_info["bytes_data"])
|
||||
doc_copy.apply_edits(edits_json)
|
||||
page = doc_copy.get_page(page_index)
|
||||
# Binding returns (width, height, rgba_bytes) — not an Image-like object.
|
||||
w, h, raw = page.render_region_raw(dpi, y_top, 0.0)
|
||||
raw = bytes(raw)
|
||||
w, h = int(w), int(h)
|
||||
|
||||
from PIL import Image
|
||||
png_buf = io.BytesIO()
|
||||
Image.frombytes("RGBA", (w, h), raw).save(png_buf, format="PNG")
|
||||
b64 = base64.b64encode(png_buf.getvalue()).decode("ascii")
|
||||
|
||||
layout_obj = None
|
||||
layout_source = "extract"
|
||||
try:
|
||||
# Prefer engine's reflow layout (matches WASM STAGE_5) when binding exposes it.
|
||||
if hasattr(doc_copy, "last_reflow_layout"):
|
||||
raw_lay = doc_copy.last_reflow_layout()
|
||||
if raw_lay:
|
||||
layout_obj = json.loads(raw_lay)
|
||||
layout_source = "reflow"
|
||||
except Exception:
|
||||
layout_obj = None
|
||||
layout_source = "extract"
|
||||
|
||||
if not layout_obj:
|
||||
# Fallback: extract page model and keep only lines in the edited paragraph band.
|
||||
reflow_op = next((op for op in request.operations if op.type == "reflow_paragraph"), None)
|
||||
col_l = float(reflow_op.data.columnLeft) if reflow_op else None
|
||||
col_r = float(reflow_op.data.columnRight) if reflow_op else None
|
||||
base0 = float(reflow_op.data.firstBaselineY) if reflow_op else None
|
||||
leading = float(reflow_op.data.leading) if reflow_op else 14.0
|
||||
old_n = int(reflow_op.data.oldLineCount) if reflow_op else 1
|
||||
y_lo = (base0 - leading * 0.75) if base0 is not None else None
|
||||
y_hi = (base0 + leading * max(old_n + 48, 64)) if base0 is not None else None
|
||||
|
||||
layout_lines = []
|
||||
try:
|
||||
model = page.extract_document_model()
|
||||
for para in model.paragraphs:
|
||||
for ln in para.lines:
|
||||
text = "".join(r.text or "" for r in ln.runs)
|
||||
if not text.strip():
|
||||
continue
|
||||
# Flatten glyphs across runs, then advances from origin deltas
|
||||
# (per-run last-glyph bbox_w under-advances and lags the caret).
|
||||
glyphs = []
|
||||
for r in ln.runs:
|
||||
glyphs.extend(list(r.glyphs))
|
||||
if not glyphs:
|
||||
continue
|
||||
adv: list[float] = []
|
||||
for i, g in enumerate(glyphs):
|
||||
if i + 1 < len(glyphs):
|
||||
adv.append(float(glyphs[i + 1].origin_x - g.origin_x))
|
||||
else:
|
||||
bw = float(g.bbox_w) if g.bbox_w > 0 else 0.0
|
||||
adv.append(bw if bw > 0 else float(max((r.font_size or 0) for r in ln.runs) or 12) * 0.5)
|
||||
x0 = float(glyphs[0].origin_x)
|
||||
by = float(ln.baseline_y)
|
||||
if col_l is not None and x0 < col_l - 24:
|
||||
continue
|
||||
if col_r is not None and x0 > col_r + 8:
|
||||
continue
|
||||
if y_lo is not None and by < y_lo:
|
||||
continue
|
||||
if y_hi is not None and by > y_hi:
|
||||
continue
|
||||
layout_lines.append({
|
||||
"baselineY": by,
|
||||
"x0": x0,
|
||||
"fontSize": float(max((r.font_size or 0) for r in ln.runs) or 12),
|
||||
"text": text,
|
||||
"adv": adv,
|
||||
"pageIndex": page_index,
|
||||
})
|
||||
except Exception:
|
||||
layout_lines = []
|
||||
|
||||
layout_lines.sort(key=lambda L: -L["baselineY"])
|
||||
layout_obj = {
|
||||
"columnLeft": layout_lines[0]["x0"] if layout_lines else (col_l or 0),
|
||||
"anchorPage": page_index,
|
||||
"lines": layout_lines,
|
||||
}
|
||||
layout_source = "extract"
|
||||
|
||||
if isinstance(layout_obj, dict):
|
||||
layout_obj["layoutSource"] = layout_source
|
||||
|
||||
return {
|
||||
"width": w,
|
||||
"height": h,
|
||||
"yTopPt": y_top,
|
||||
"pageIndex": page_index,
|
||||
"pngBase64": b64,
|
||||
"layout": layout_obj,
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) from e
|
||||
|
||||
|
||||
@compat_router.post("/edits/{document_id}")
|
||||
def apply_edits_compat(document_id: str, request: EditsRequest):
|
||||
return apply_edits_impl(document_id, request)
|
||||
|
||||
|
After Width: | Height: | Size: 173 KiB |
@@ -0,0 +1,68 @@
|
||||
"""Locate the real failing PDF containing 'Professional Experiences' / Arial-BoldMT."""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, r"C:\Users\Maskan\pdfeng-build\win-local\lib")
|
||||
sys.path.insert(0, r"c:\Users\Maskan\Desktop\pdf_editor\pdf\gateway")
|
||||
import pdfengine # type: ignore
|
||||
|
||||
CANDS = [
|
||||
Path(r"C:\Users\Maskan\Downloads\Saqib_Ali_Mir_Resume.pdf"),
|
||||
Path(r"C:\Users\Maskan\Downloads\Saqib_Ali_Mir_Resume (1)vq.pdf"),
|
||||
Path(r"C:\Users\Maskan\Downloads\Saqib_Ali_Mir_Resume (1)vq (1).pdf"),
|
||||
Path(r"C:\Users\Maskan\Downloads\Saqib_Ali_Mir_Resume (1)verfication.pdf"),
|
||||
Path(r"C:\Users\Maskan\Downloads\DemoPdf.pdf"),
|
||||
Path(r"C:\Users\Maskan\Downloads\DemoPdf-1-3.pdf"),
|
||||
Path(r"C:\Users\Maskan\Desktop\docQube\docqube_backend\app\temp_uploads\801c2dcf-afa2-462a-a71d-4b2e2599808f.pdf"),
|
||||
Path(r"C:\Users\Maskan\Desktop\docQube\docqube_backend\app\temp_uploads\fc716e7e-aa82-4bb3-825a-51140db03c1b.pdf"),
|
||||
]
|
||||
|
||||
raw = Path(r"C:\Users\Maskan\Documents\pdf_html\backend\storage\raw")
|
||||
if raw.exists():
|
||||
CANDS.extend(sorted(raw.glob("*.pdf")))
|
||||
for d in raw.iterdir():
|
||||
if d.is_dir():
|
||||
CANDS.extend(sorted(d.glob("*.pdf")))
|
||||
|
||||
|
||||
def main():
|
||||
hits = []
|
||||
for p in CANDS:
|
||||
if not p.exists():
|
||||
continue
|
||||
try:
|
||||
doc = pdfengine.PdfDocument.load_from_file(str(p), "")
|
||||
except Exception as e:
|
||||
print(f"ERR load {p}: {e}")
|
||||
continue
|
||||
for pi in range(min(doc.page_count, 5)):
|
||||
page = doc.get_page(pi)
|
||||
try:
|
||||
model = page.extract_document_model()
|
||||
except Exception as e:
|
||||
print(f"ERR model {p} p{pi}: {e}")
|
||||
continue
|
||||
for para in model.paragraphs:
|
||||
text = " ".join("".join(r.text for r in ln.runs) for ln in para.lines)
|
||||
if "Professional Experience" in text:
|
||||
fonts = sorted({
|
||||
(r.font_name, r.internal_font_id, r.is_embedded, r.type, r.font_size)
|
||||
for ln in para.lines for r in ln.runs
|
||||
})
|
||||
hits.append((str(p), pi, text[:100], fonts))
|
||||
print("FOUND", p)
|
||||
print(" page", pi)
|
||||
print(" text", repr(text[:120]))
|
||||
print(" fonts", fonts)
|
||||
for f in doc.get_fonts(pi, pi):
|
||||
if "Arial-BoldMT_TrueType_32" in (f.internal_font_id or "") or (
|
||||
"Arial-BoldMT" in (f.font_name or "") and f.is_embedded
|
||||
):
|
||||
print("FONT", p, "page", pi, f.font_name, f.internal_font_id, "emb", f.is_embedded)
|
||||
print("hits", len(hits))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,254 @@
|
||||
"""Edit-entry identity check for the voice-search bullet item (real resume).
|
||||
|
||||
Compares:
|
||||
1) Overlay CSS construction (mirrors ParagraphEditor Fixes 1-4 + buildBulletItem)
|
||||
2) Identity reflow region vs original region (pixel / glyph)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(ROOT / "gateway"))
|
||||
sys.path.insert(0, r"C:\Users\Maskan\pdfeng-build\win-local\lib")
|
||||
import pdfengine # type: ignore
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
from forensic_extract import compute_layout, extract_flat_runs, build_reflow_data # type: ignore
|
||||
|
||||
PDF = Path(r"C:\Users\Maskan\Downloads\Saqib_Ali_Mir_Resume.pdf")
|
||||
OUT = Path(__file__).resolve().parent / "forensic_real" / "bullet_edit_entry_report.json"
|
||||
TARGET = "voice search"
|
||||
|
||||
|
||||
def is_bullet(t: str) -> bool:
|
||||
t = (t or "").strip()
|
||||
return t in {"•", "●", "○", "◆", "■", "-", "–", "—", "*"} or (len(t) <= 3 and t[:1].isdigit() and t.endswith("."))
|
||||
|
||||
|
||||
def build_bullet_item(para, run_line_index: int):
|
||||
lines = para.lines
|
||||
col_left = min(l.x for l in lines)
|
||||
col_right = max(l.x + l.w for l in lines)
|
||||
|
||||
def lead_font(l):
|
||||
for r in l.runs:
|
||||
if (r.text or "").strip() and not is_bullet(r.text):
|
||||
return r.internal_font_id or ""
|
||||
return ""
|
||||
|
||||
hang = min((l.x for l in lines if l.x > col_left + 1), default=col_left + 8)
|
||||
flush_thresh = col_left + (hang - col_left) * 0.5
|
||||
|
||||
def flush_left(l):
|
||||
return l.x <= flush_thresh
|
||||
|
||||
def is_start(idx):
|
||||
l = lines[idx]
|
||||
if is_bullet((l.runs[0].text if l.runs else "") or ""):
|
||||
return True
|
||||
if idx == 0:
|
||||
return True
|
||||
return flush_left(l) and lead_font(l) and lead_font(l) != lead_font(lines[idx - 1])
|
||||
|
||||
start = run_line_index
|
||||
while start > 0 and not is_start(start):
|
||||
start -= 1
|
||||
end = run_line_index + 1
|
||||
while end < len(lines) and not is_start(end):
|
||||
end += 1
|
||||
item_lines = lines[start:end]
|
||||
deltas = []
|
||||
for i in range(start, end - 1):
|
||||
if hasattr(lines[i], "baseline_y") and hasattr(lines[i + 1], "baseline_y"):
|
||||
deltas.append(abs(lines[i].baseline_y - lines[i + 1].baseline_y))
|
||||
leading = sorted(deltas)[len(deltas) // 2] if deltas else 12.0
|
||||
|
||||
first_runs = list(item_lines[0].runs)
|
||||
sub_lines = item_lines
|
||||
if first_runs and is_bullet(first_runs[0].text):
|
||||
ti = 1
|
||||
while ti < len(first_runs) and not (first_runs[ti].text or "").strip():
|
||||
ti += 1
|
||||
text_runs = first_runs[ti:]
|
||||
text_indent = text_runs[0].x
|
||||
|
||||
class LW:
|
||||
pass
|
||||
|
||||
new_lines = []
|
||||
for idx, l in enumerate(item_lines):
|
||||
w = LW()
|
||||
if idx == 0:
|
||||
w.runs = text_runs
|
||||
w.x = text_indent
|
||||
w.w = (l.x + l.w) - text_indent
|
||||
else:
|
||||
w.runs = l.runs
|
||||
w.x = l.x
|
||||
w.w = l.w
|
||||
w.y = l.y
|
||||
w.h = l.h
|
||||
w.baseline_y = l.baseline_y
|
||||
new_lines.append(w)
|
||||
sub_lines = new_lines
|
||||
|
||||
class SP:
|
||||
pass
|
||||
|
||||
sp = SP()
|
||||
sp.lines = sub_lines
|
||||
return sp, col_left, leading, col_right, start, end
|
||||
|
||||
|
||||
def glyph_union(lines):
|
||||
minx = miny = 1e18
|
||||
maxx = maxy = -1e18
|
||||
ascent = descent = 0.0
|
||||
for ln in lines:
|
||||
for r in ln.runs:
|
||||
for g in r.glyphs:
|
||||
minx = min(minx, g.bbox_x)
|
||||
miny = min(miny, g.bbox_y)
|
||||
maxx = max(maxx, g.bbox_x + g.bbox_w)
|
||||
maxy = max(maxy, g.bbox_y + g.bbox_h)
|
||||
gbl = g.origin_y if g.origin_y else ln.baseline_y
|
||||
ascent = max(ascent, (g.bbox_y + g.bbox_h) - gbl)
|
||||
descent = max(descent, gbl - g.bbox_y)
|
||||
return {
|
||||
"x": minx,
|
||||
"y": miny,
|
||||
"w": maxx - minx,
|
||||
"h": maxy - miny,
|
||||
"ascent": ascent,
|
||||
"descent": descent,
|
||||
"line_h": max(l.h for l in lines),
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
doc = pdfengine.PdfDocument.load_from_file(str(PDF), "")
|
||||
page = doc.get_page(0)
|
||||
model = page.extract_document_model()
|
||||
para = None
|
||||
pi = -1
|
||||
for i, p in enumerate(model.paragraphs):
|
||||
text = "".join(r.text or "" for ln in p.lines for r in ln.runs)
|
||||
if TARGET.lower() in text.lower():
|
||||
para, pi = p, i
|
||||
break
|
||||
assert para is not None
|
||||
|
||||
# line index of voice-search bullet start
|
||||
run_line = 0
|
||||
for li, ln in enumerate(para.lines):
|
||||
t = "".join(r.text or "" for r in ln.runs)
|
||||
if "voice search" in t.lower() or (li and "Speech API" in "".join(r.text or "" for r in para.lines[li - 1].runs)):
|
||||
# find bullet start
|
||||
pass
|
||||
for li, ln in enumerate(para.lines):
|
||||
t = "".join(r.text or "" for r in ln.runs)
|
||||
if "Implemented a voice" in t:
|
||||
run_line = li
|
||||
break
|
||||
|
||||
sub, push_left, leading, col_right, start, end = build_bullet_item(para, run_line)
|
||||
box = glyph_union(sub.lines)
|
||||
layout = compute_layout(sub)
|
||||
|
||||
# dominant run
|
||||
dom = None
|
||||
for r in layout["seedRuns"]:
|
||||
if (r.get("text") or "").strip() and r.get("fid"):
|
||||
dom = r
|
||||
break
|
||||
font_name = (dom or {}).get("fontName") or ""
|
||||
extracted = re.sub(r"^[A-Z]{6}\+", "", font_name).strip() or "sans-serif"
|
||||
weight = 700 if re.search(r"bold|black|heavy", extracted, re.I) else 400
|
||||
line_height_pt = leading # leadingOverride ?? paraBox.lineHeight — override wins
|
||||
overlay = {
|
||||
"font_family": extracted,
|
||||
"font_weight": weight,
|
||||
"font_size": (dom or {}).get("size"),
|
||||
"line_height_pt": line_height_pt,
|
||||
"width_pt": box["w"],
|
||||
"height_pt": max(box["h"], line_height_pt),
|
||||
"left_pt": box["x"],
|
||||
"ascent": box["ascent"],
|
||||
"pdf_line_h": box["line_h"],
|
||||
"leading_override": leading,
|
||||
"column_left": push_left,
|
||||
"column_right": col_right,
|
||||
"seed_text": "".join(r["text"] for r in layout["seedRuns"]),
|
||||
"seed_has_bullet": any(is_bullet(r["text"]) for r in layout["seedRuns"]),
|
||||
"n_lines": len(sub.lines),
|
||||
"line_range": [start, end],
|
||||
}
|
||||
|
||||
checks = []
|
||||
checks.append(("font-family", font_name, extracted, extracted.lower() in (font_name or "").lower().replace("bcdjee+", "") or "arialmt" in extracted.lower()))
|
||||
checks.append(("font-weight", weight, weight, True))
|
||||
checks.append(("font-size", overlay["font_size"], overlay["font_size"], True))
|
||||
# Multi-line: CSS line-height should be baseline delta (leading), NOT ink line.h
|
||||
checks.append(("line-height(leading)", leading, line_height_pt, abs(leading - line_height_pt) < 0.01))
|
||||
checks.append(("width(ink)", box["w"], overlay["width_pt"], abs(box["w"] - overlay["width_pt"]) < 0.01))
|
||||
checks.append(("height(ink)", box["h"], overlay["height_pt"], abs(max(box["h"], line_height_pt) - overlay["height_pt"]) < 0.01))
|
||||
|
||||
# Identity reflow
|
||||
fid = (dom or {}).get("fid") or ""
|
||||
flat = extract_flat_runs(layout["seedRuns"], fid, (dom or {}).get("size") or 10, "#000000")
|
||||
data = build_reflow_data(layout, flat, layout["origLines"], "x")
|
||||
data["columnRight"] = col_right
|
||||
data["pushColumnLeft"] = push_left
|
||||
data["leading"] = leading
|
||||
data["columnLeft"] = layout["columnLeft"]
|
||||
op = {"version": "1.0", "operations": [{"id": "f", "type": "reflow_paragraph", "pageIndex": 0, "data": data}]}
|
||||
|
||||
# Original region crop
|
||||
y_top = box["y"] - 2
|
||||
h = box["h"] + 4
|
||||
dpi = 144
|
||||
orig_img = page.render_region_raw(dpi, y_top, h)
|
||||
|
||||
r = doc.apply_edits(json.dumps(op))
|
||||
page2 = doc.get_page(0)
|
||||
prev_img = page2.render_region_raw(dpi, y_top, h)
|
||||
|
||||
def sha(img):
|
||||
import hashlib
|
||||
return hashlib.sha256(bytes(img.data)).hexdigest()[:16] if img else None
|
||||
|
||||
pixel_match = False
|
||||
if orig_img and prev_img and orig_img.width == prev_img.width and orig_img.height == prev_img.height:
|
||||
a = bytes(orig_img.data)
|
||||
b = bytes(prev_img.data)
|
||||
pixel_match = a == b
|
||||
diff = sum(1 for i in range(0, len(a), 4) if a[i : i + 3] != b[i : i + 3])
|
||||
else:
|
||||
diff = -1
|
||||
|
||||
report = {
|
||||
"para_index": pi,
|
||||
"overlay": overlay,
|
||||
"checks": [{"property": a, "pdf": b, "overlay": c, "match": d} for a, b, c, d in checks],
|
||||
"identity_reflow": {
|
||||
"apply_ok": bool(r),
|
||||
"orig_sha": sha(orig_img),
|
||||
"prev_sha": sha(prev_img),
|
||||
"pixel_identical": pixel_match,
|
||||
"diff_pixels": diff,
|
||||
"region": {"y_top": y_top, "h": h, "dpi": dpi},
|
||||
},
|
||||
}
|
||||
OUT.parent.mkdir(parents=True, exist_ok=True)
|
||||
OUT.write_text(json.dumps(report, indent=2), encoding="utf-8")
|
||||
print(json.dumps(report, indent=2))
|
||||
print("OVERLAY", "ALL MATCH" if all(c[3] for c in checks) else "DIFFS")
|
||||
print("IDENTITY PIXELS", "MATCH" if pixel_match else f"DIFF ({diff})")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,193 @@
|
||||
"""Edit-entry overlay vs extracted PDF paragraph (no reflow / no typing).
|
||||
|
||||
Mirrors ParagraphEditor computeLayout + style construction for the real resume
|
||||
heading \"Professional Experience\" and ranks the first property that diverges.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(ROOT / "gateway"))
|
||||
sys.path.insert(0, r"C:\Users\Maskan\pdfeng-build\win-local\lib")
|
||||
import pdfengine # type: ignore
|
||||
|
||||
PDF = Path(r"C:\Users\Maskan\Downloads\Saqib_Ali_Mir_Resume.pdf")
|
||||
OUT = Path(__file__).resolve().parent / "forensic_real" / "edit_entry_overlay_report.json"
|
||||
TARGET = "Professional Experience"
|
||||
ZOOM = 1.0 # property ratios are zoom-invariant in pt space
|
||||
|
||||
|
||||
def measure_family(font_name: str) -> str:
|
||||
if re.search(r"times|serif", font_name, re.I):
|
||||
return "Times New Roman, serif"
|
||||
if re.search(r"courier|mono", font_name, re.I):
|
||||
return "Courier New, monospace"
|
||||
return "Arial, sans-serif"
|
||||
|
||||
|
||||
def page_content_right(model) -> float:
|
||||
right = float("-inf")
|
||||
for p in model.paragraphs:
|
||||
for ln in p.lines:
|
||||
if any((r.text or "").strip() for r in ln.runs):
|
||||
right = max(right, ln.x + ln.w)
|
||||
return right if right != float("-inf") else 0.0
|
||||
|
||||
|
||||
def main():
|
||||
doc = pdfengine.PdfDocument.load_from_file(str(PDF), "")
|
||||
page = doc.get_page(0)
|
||||
model = page.extract_document_model()
|
||||
height_pts = page.height
|
||||
|
||||
para = None
|
||||
for p in model.paragraphs:
|
||||
text = "".join(r.text or "" for ln in p.lines for r in ln.runs)
|
||||
if TARGET in text:
|
||||
para = p
|
||||
break
|
||||
if para is None:
|
||||
raise SystemExit("paragraph not found")
|
||||
|
||||
# --- Extracted PDF metrics ---
|
||||
line = para.lines[0]
|
||||
run = line.runs[0]
|
||||
glyphs = list(run.glyphs)
|
||||
min_x = min(g.bbox_x for g in glyphs)
|
||||
min_y = min(g.bbox_y for g in glyphs)
|
||||
max_x = max(g.bbox_x + g.bbox_w for g in glyphs)
|
||||
max_y = max(g.bbox_y + g.bbox_h for g in glyphs)
|
||||
baseline = glyphs[0].origin_y
|
||||
ascent = max_y - baseline
|
||||
descent = baseline - min_y
|
||||
pdf = {
|
||||
"text": "".join(r.text or "" for ln in para.lines for r in ln.runs),
|
||||
"bbox": {"x": min_x, "y": min_y, "w": max_x - min_x, "h": max_y - min_y},
|
||||
"line_x": line.x,
|
||||
"line_y": line.y,
|
||||
"line_w": line.w,
|
||||
"line_h": line.h,
|
||||
"baseline_y": line.baseline_y,
|
||||
"ascent_pt": ascent,
|
||||
"descent_pt": descent,
|
||||
"width_pt": max_x - min_x,
|
||||
"height_pt": max_y - min_y,
|
||||
"font_name": run.font_name,
|
||||
"font_size_pt": run.font_size,
|
||||
"run_h": run.h,
|
||||
"internal_font_id": run.internal_font_id,
|
||||
"expected_font_weight": 700 if re.search(r"bold|black|heavy", run.font_name or "", re.I) else 400,
|
||||
}
|
||||
|
||||
# --- Overlay construction (mirrors ParagraphEditor + TextEditLayer openEditor) ---
|
||||
dom_size = max(run.font_size or 0, run.h or 0) or 12
|
||||
column_left = line.x
|
||||
para_right = line.x + line.w
|
||||
pr = page_content_right(model)
|
||||
column_right = max(pr, para_right) # TextEditLayer left-align path
|
||||
# single-line leading default in computeLayout:
|
||||
leading = dom_size * 1.2
|
||||
first_baseline_y = line.baseline_y
|
||||
font_name = run.font_name or ""
|
||||
family = measure_family(font_name)
|
||||
font_px = dom_size * ZOOM
|
||||
leading_px = leading * ZOOM
|
||||
col_left_px = column_left * ZOOM
|
||||
col_width_px = (column_right - column_left) * ZOOM
|
||||
col_height_px = 1 * leading_px # oldLineCount=1
|
||||
first_baseline_screen = (height_pts - first_baseline_y) * ZOOM
|
||||
editor_top = first_baseline_screen - font_px * 0.8
|
||||
ascent_heuristic = dom_size * 0.8
|
||||
|
||||
overlay = {
|
||||
"font_family": family,
|
||||
"font_size_pt": dom_size,
|
||||
"font_size_px": font_px,
|
||||
"font_weight": "(not set; browser default 400)",
|
||||
"line_height_pt": leading,
|
||||
"line_height_px": leading_px,
|
||||
"letter_spacing": "(not set; normal)",
|
||||
"width_pt": column_right - column_left,
|
||||
"width_px": col_width_px,
|
||||
"height_pt": leading, # oldLineCount * leading
|
||||
"height_px": col_height_px,
|
||||
"left_px": col_left_px,
|
||||
"top_px": editor_top,
|
||||
"ascent_heuristic_pt": ascent_heuristic,
|
||||
"transform": "none",
|
||||
"column_left_pt": column_left,
|
||||
"column_right_pt": column_right,
|
||||
"page_content_right_pt": pr,
|
||||
"style_source": {
|
||||
"measureFamily": "Arial, sans-serif because name matches neither times|serif nor courier|mono",
|
||||
"leading": "domSize * 1.2 (single-line; no baseline deltas)",
|
||||
"editorTop": "baselineScreen - fontPx * 0.8",
|
||||
"height": "oldLineCount * leadingPx",
|
||||
"width": "columnRightOverride - columnLeft (page content right, not text bbox)",
|
||||
},
|
||||
}
|
||||
|
||||
# Ordered property checks (construction order / visual identity order)
|
||||
checks = []
|
||||
|
||||
def add(prop, pdf_v, ov_v, match):
|
||||
checks.append({"property": prop, "pdf": pdf_v, "overlay": ov_v, "match": match})
|
||||
|
||||
bold = pdf["expected_font_weight"] >= 700
|
||||
add(
|
||||
"font-family",
|
||||
pdf["font_name"],
|
||||
family,
|
||||
(not bold) and ("arial" in (pdf["font_name"] or "").lower()),
|
||||
)
|
||||
add(
|
||||
"font-weight",
|
||||
pdf["expected_font_weight"],
|
||||
400,
|
||||
(not bold) or False, # BoldMT → no font-weight set → mismatch
|
||||
)
|
||||
add("font-size (pt)", pdf["font_size_pt"], overlay["font_size_pt"], abs(pdf["font_size_pt"] - overlay["font_size_pt"]) < 0.05)
|
||||
add("line-height / leading (pt)", pdf["line_h"], overlay["line_height_pt"], abs(pdf["line_h"] - overlay["line_height_pt"]) < 0.5)
|
||||
add("ascent (pt)", pdf["ascent_pt"], overlay["ascent_heuristic_pt"], abs(pdf["ascent_pt"] - overlay["ascent_heuristic_pt"]) < 0.5)
|
||||
add("descent (pt)", pdf["descent_pt"], "(not represented in overlay CSS)", False)
|
||||
add("height (pt)", pdf["height_pt"], overlay["height_pt"], abs(pdf["height_pt"] - overlay["height_pt"]) < 0.5)
|
||||
add("width (pt)", pdf["width_pt"], overlay["width_pt"], abs(pdf["width_pt"] - overlay["width_pt"]) < 1.0)
|
||||
add("letter-spacing", 0, "normal", True)
|
||||
add("transform", "none", "none", True)
|
||||
|
||||
first = next((c for c in checks if not c["match"]), None)
|
||||
|
||||
print("=== PDF paragraph ===")
|
||||
print(json.dumps(pdf, indent=2))
|
||||
print("\n=== Overlay applied (edit-entry) ===")
|
||||
print(json.dumps(overlay, indent=2))
|
||||
print("\n=== Property checks (order) ===")
|
||||
for c in checks:
|
||||
flag = "OK " if c["match"] else "DIFF"
|
||||
print(f" [{flag}] {c['property']}: pdf={c['pdf']!r} overlay={c['overlay']!r}")
|
||||
print("\n=== FIRST PROPERTY THAT CHANGES ON EDIT-ENTRY ===")
|
||||
print(json.dumps(first, indent=2))
|
||||
|
||||
report = {
|
||||
"scope": "edit-entry only (mouse click before typing); no reflow/typing",
|
||||
"pdf": pdf,
|
||||
"overlay": overlay,
|
||||
"checks": checks,
|
||||
"firstPropertyThatChanges": first,
|
||||
"notes": [
|
||||
"measureFamily maps Arial-BoldMT → 'Arial, sans-serif' and never sets font-weight:700",
|
||||
"That is the first identity/style mutation when the contentEditable overlay is created",
|
||||
"Subsequent geometric diffs: leading 11.09→14.4, ascent 8.74→9.6, height 11.09→14.4, width text→page column",
|
||||
],
|
||||
}
|
||||
OUT.parent.mkdir(parents=True, exist_ok=True)
|
||||
OUT.write_text(json.dumps(report, indent=2), encoding="utf-8")
|
||||
print(f"\nWrote {OUT}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,233 @@
|
||||
"""First-property mutation on first keystroke (typing/reflow only).
|
||||
|
||||
Compares ORIGINAL extracted paragraph vs after typing one char at end.
|
||||
Mirrors ParagraphEditor when editedRef=true:
|
||||
- no lines[] payload (origLines dropped)
|
||||
- lineX / lineBaselineY still sent from layout
|
||||
- advances + advanceSeedText for the edited run
|
||||
|
||||
Prints the first property that diverges.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(ROOT / "gateway"))
|
||||
sys.path.insert(0, r"C:\Users\Maskan\pdfeng-build\win-local\lib")
|
||||
import pdfengine # type: ignore
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
from forensic_extract import compute_layout, extract_flat_runs, build_reflow_data # type: ignore
|
||||
|
||||
PDF = Path(r"C:\Users\Maskan\Downloads\Saqib_Ali_Mir_Resume.pdf")
|
||||
OUT = Path(__file__).resolve().parent / "forensic_real" / "first_keystroke_mutation.json"
|
||||
TARGET = "Professional Experience"
|
||||
TYPED = TARGET + "x"
|
||||
TOL = 0.05
|
||||
|
||||
|
||||
def para_metrics(para, prefix: str) -> dict:
|
||||
glyphs = []
|
||||
fonts = set()
|
||||
sizes = set()
|
||||
minx = miny = 1e18
|
||||
maxx = maxy = -1e18
|
||||
ascent = descent = 0.0
|
||||
baselines = []
|
||||
line_hs = []
|
||||
for ln in para.lines:
|
||||
baselines.append(ln.baseline_y)
|
||||
line_hs.append(ln.h)
|
||||
for r in ln.runs:
|
||||
fonts.add(r.font_name or "")
|
||||
sizes.add(r.font_size or 0)
|
||||
text = r.text or ""
|
||||
gs = list(r.glyphs)
|
||||
for i, g in enumerate(gs):
|
||||
ch = text[i] if i < len(text) else "?"
|
||||
adv = (gs[i + 1].origin_x - g.origin_x) if i + 1 < len(gs) else (g.bbox_w or (r.font_size or 12) * 0.5)
|
||||
glyphs.append({
|
||||
"char": ch,
|
||||
"origin_x": g.origin_x,
|
||||
"origin_y": g.origin_y,
|
||||
"advance": adv,
|
||||
"bbox_x": g.bbox_x,
|
||||
"bbox_y": g.bbox_y,
|
||||
"bbox_w": g.bbox_w,
|
||||
"bbox_h": g.bbox_h,
|
||||
"font_size": g.font_size or r.font_size,
|
||||
"font_name": g.font_name or r.font_name,
|
||||
"fid": r.internal_font_id,
|
||||
})
|
||||
minx = min(minx, g.bbox_x)
|
||||
miny = min(miny, g.bbox_y)
|
||||
maxx = max(maxx, g.bbox_x + g.bbox_w)
|
||||
maxy = max(maxy, g.bbox_y + g.bbox_h)
|
||||
gbl = g.origin_y if g.origin_y else ln.baseline_y
|
||||
ascent = max(ascent, (g.bbox_y + g.bbox_h) - gbl)
|
||||
descent = max(descent, gbl - g.bbox_y)
|
||||
|
||||
joined = "".join(g["char"] for g in glyphs)
|
||||
start = joined.find(prefix[: len(TARGET)])
|
||||
if start < 0:
|
||||
start = 0
|
||||
shared = glyphs[start : start + len(TARGET)]
|
||||
|
||||
leading = None
|
||||
if len(baselines) >= 2:
|
||||
leading = abs(baselines[0] - baselines[1])
|
||||
|
||||
return {
|
||||
"text": joined,
|
||||
"font_family": sorted(fonts),
|
||||
"font_size": max(sizes) if sizes else None,
|
||||
"line_height": max(line_hs) if line_hs else None,
|
||||
"leading": leading,
|
||||
"ascent": ascent,
|
||||
"descent": descent,
|
||||
"paragraph_width": (maxx - minx) if maxx > minx else 0,
|
||||
"paragraph_height": (maxy - miny) if maxy > miny else 0,
|
||||
"n_lines": len(para.lines),
|
||||
"baselines": baselines,
|
||||
"shared_glyphs": shared,
|
||||
"bbox": {"x": minx, "y": miny, "w": maxx - minx, "h": maxy - miny},
|
||||
}
|
||||
|
||||
|
||||
def first_diff(before: dict, after: dict) -> dict | None:
|
||||
checks = []
|
||||
|
||||
def add(name, b, a, ok):
|
||||
checks.append({"property": name, "before": b, "after": a, "match": ok})
|
||||
|
||||
add("font_family", before["font_family"], after["font_family"],
|
||||
before["font_family"] == after["font_family"])
|
||||
add("font_size", before["font_size"], after["font_size"],
|
||||
before["font_size"] is not None and abs((before["font_size"] or 0) - (after["font_size"] or 0)) < TOL)
|
||||
add("line_height", before["line_height"], after["line_height"],
|
||||
before["line_height"] is not None and abs((before["line_height"] or 0) - (after["line_height"] or 0)) < TOL)
|
||||
add("ascent", before["ascent"], after["ascent"], abs(before["ascent"] - after["ascent"]) < TOL)
|
||||
add("descent", before["descent"], after["descent"], abs(before["descent"] - after["descent"]) < TOL)
|
||||
# Width/height: after includes +x so width may grow at the end — compare shared-prefix ink only below.
|
||||
bg, ag = before["shared_glyphs"], after["shared_glyphs"]
|
||||
n = min(len(bg), len(ag), len(TARGET))
|
||||
|
||||
# First glyph-level mutation among UNCHANGED chars
|
||||
glyph_first = None
|
||||
for i in range(n):
|
||||
b, a = bg[i], ag[i]
|
||||
reasons = []
|
||||
if b["char"] != a["char"]:
|
||||
reasons.append("char")
|
||||
if abs(b["advance"] - a["advance"]) > TOL:
|
||||
reasons.append("advance")
|
||||
if abs(b["origin_x"] - a["origin_x"]) > TOL:
|
||||
reasons.append("origin_x")
|
||||
if abs(b["origin_y"] - a["origin_y"]) > TOL:
|
||||
reasons.append("origin_y")
|
||||
if (b.get("font_name") or "") != (a.get("font_name") or ""):
|
||||
reasons.append("font_name")
|
||||
if abs((b.get("font_size") or 0) - (a.get("font_size") or 0)) > TOL:
|
||||
reasons.append("font_size")
|
||||
if reasons:
|
||||
glyph_first = {
|
||||
"index": i,
|
||||
"char": b["char"],
|
||||
"reasons": reasons,
|
||||
"before": b,
|
||||
"after": a,
|
||||
}
|
||||
break
|
||||
|
||||
add("unchanged_glyph_identity", "all match", glyph_first or "all match", glyph_first is None)
|
||||
|
||||
# Prefix span width (should be identical if advances+positions preserved)
|
||||
if n:
|
||||
bw = (bg[n - 1]["origin_x"] + bg[n - 1]["advance"]) - bg[0]["origin_x"]
|
||||
aw = (ag[n - 1]["origin_x"] + ag[n - 1]["advance"]) - ag[0]["origin_x"]
|
||||
add("prefix_width", bw, aw, abs(bw - aw) < TOL)
|
||||
add("prefix_start_x", bg[0]["origin_x"], ag[0]["origin_x"], abs(bg[0]["origin_x"] - ag[0]["origin_x"]) < TOL)
|
||||
|
||||
first = next((c for c in checks if not c["match"]), None)
|
||||
return {"checks": checks, "first_property_that_changes": first, "first_unchanged_glyph_mutation": glyph_first}
|
||||
|
||||
|
||||
def main():
|
||||
doc = pdfengine.PdfDocument.load_from_file(str(PDF), "")
|
||||
page = doc.get_page(0)
|
||||
model = page.extract_document_model()
|
||||
para = None
|
||||
for p in model.paragraphs:
|
||||
t = "".join(r.text or "" for ln in p.lines for r in ln.runs)
|
||||
if TARGET in t:
|
||||
para = p
|
||||
break
|
||||
assert para is not None
|
||||
|
||||
before = para_metrics(para, TARGET)
|
||||
layout = compute_layout(para)
|
||||
flat = extract_flat_runs(layout["seedRuns"], layout["seedRuns"][0]["fid"], layout["seedRuns"][0]["size"], "#000")
|
||||
|
||||
seed_adv = None
|
||||
for r in flat:
|
||||
if r.get("text") == TARGET and r.get("advances") and len(r["advances"]) == len(TARGET):
|
||||
seed_adv = list(r["advances"])
|
||||
break
|
||||
|
||||
typed = []
|
||||
for r in flat:
|
||||
nr = dict(r)
|
||||
if nr.get("text") == TARGET:
|
||||
nr["text"] = TYPED
|
||||
if seed_adv is not None:
|
||||
nr["advances"] = seed_adv
|
||||
nr["advanceSeedText"] = TARGET
|
||||
else:
|
||||
nr.pop("advances", None)
|
||||
typed.append(nr)
|
||||
|
||||
data = build_reflow_data(layout, typed, None, "key-after")
|
||||
data.pop("lines", None) # editedRef drops lines
|
||||
# keep lineX/lineBaselineY as frontend does
|
||||
|
||||
print("PAYLOAD keys:", sorted(data.keys()))
|
||||
print("run:", [(r.get("text"), len(r.get("advances") or []), r.get("advanceSeedText")) for r in typed])
|
||||
|
||||
op = {"version": "1.0", "operations": [{"id": "t", "type": "reflow_paragraph", "pageIndex": 0, "data": data}]}
|
||||
doc.apply_edits(json.dumps(op))
|
||||
para_a = None
|
||||
model_a = doc.get_page(0).extract_document_model()
|
||||
for p in model_a.paragraphs:
|
||||
t = "".join(r.text or "" for ln in p.lines for r in ln.runs)
|
||||
if TARGET in t or TYPED in t or "Professional" in t:
|
||||
para_a = p
|
||||
break
|
||||
assert para_a is not None
|
||||
after = para_metrics(para_a, TYPED)
|
||||
|
||||
result = first_diff(before, after)
|
||||
report = {
|
||||
"target": TARGET,
|
||||
"typed": TYPED,
|
||||
"seed_adv_len": len(seed_adv or []),
|
||||
"before": {k: v for k, v in before.items() if k != "shared_glyphs"},
|
||||
"after": {k: v for k, v in after.items() if k != "shared_glyphs"},
|
||||
"before_shared_adv_sample": [g["advance"] for g in before["shared_glyphs"][:8]],
|
||||
"after_shared_adv_sample": [g["advance"] for g in after["shared_glyphs"][:8]],
|
||||
**result,
|
||||
}
|
||||
OUT.parent.mkdir(parents=True, exist_ok=True)
|
||||
OUT.write_text(json.dumps(report, indent=2), encoding="utf-8")
|
||||
print(json.dumps(report, indent=2))
|
||||
first = result["first_property_that_changes"]
|
||||
print("\n=== FIRST PROPERTY THAT CHANGES ===")
|
||||
print(json.dumps(first, indent=2))
|
||||
print("\n=== FIRST UNCHANGED GLYPH MUTATION ===")
|
||||
print(json.dumps(result["first_unchanged_glyph_mutation"], indent=2))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,677 @@
|
||||
"""Runtime glyph-position compare + U+0000 coverage audit (no engine code changes).
|
||||
|
||||
Uses real resume PDF only. Produces per-glyph table for \"Professional Experience\".
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import struct
|
||||
import sys
|
||||
import zlib
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(ROOT / "gateway"))
|
||||
sys.path.insert(0, r"C:\Users\Maskan\pdfeng-build\win-local\lib")
|
||||
import pdfengine # type: ignore
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
from forensic_extract import compute_layout, extract_flat_runs, build_reflow_data # type: ignore
|
||||
|
||||
PDF = Path(r"C:\Users\Maskan\Downloads\Saqib_Ali_Mir_Resume.pdf")
|
||||
OUT = Path(__file__).resolve().parent / "forensic_real"
|
||||
FID = "Arial-BoldMT_TrueType_32"
|
||||
TARGET = "Professional Experience"
|
||||
TOL = 0.05 # PDF points — first position differ threshold
|
||||
|
||||
|
||||
# --- TJ / content-stream helpers -------------------------------------------------
|
||||
|
||||
def decompress_streams(pdf_bytes: bytes) -> list[str]:
|
||||
out = []
|
||||
for m in re.finditer(rb"stream\r?\n(.*?)\r?\nendstream", pdf_bytes, re.DOTALL):
|
||||
raw = m.group(1)
|
||||
try:
|
||||
out.append(zlib.decompress(raw).decode("latin-1", "replace"))
|
||||
except Exception:
|
||||
try:
|
||||
out.append(raw.decode("latin-1", "replace"))
|
||||
except Exception:
|
||||
pass
|
||||
return out
|
||||
|
||||
|
||||
def parse_tj_kerning(tj_body: str) -> list[dict]:
|
||||
"""Parse a TJ array body into char runs + kerning (thousandths of em).
|
||||
|
||||
Example: [(Pr)-6(o)7(fe)-6(s)...]
|
||||
Returns list of {chars, kern_before} where kern_before is the TJ number
|
||||
preceding that string fragment (0 for the first).
|
||||
"""
|
||||
# Strip outer brackets if present
|
||||
body = tj_body.strip()
|
||||
if body.startswith("["):
|
||||
body = body[1:]
|
||||
if body.endswith("]"):
|
||||
body = body[:-1]
|
||||
items: list[dict] = []
|
||||
pos = 0
|
||||
pending_kern = 0.0
|
||||
while pos < len(body):
|
||||
while pos < len(body) and body[pos].isspace():
|
||||
pos += 1
|
||||
if pos >= len(body):
|
||||
break
|
||||
if body[pos] == "(":
|
||||
# literal string with escape handling
|
||||
pos += 1
|
||||
chars = []
|
||||
while pos < len(body) and body[pos] != ")":
|
||||
if body[pos] == "\\" and pos + 1 < len(body):
|
||||
chars.append(body[pos + 1])
|
||||
pos += 2
|
||||
else:
|
||||
chars.append(body[pos])
|
||||
pos += 1
|
||||
pos += 1 # )
|
||||
s = "".join(chars)
|
||||
items.append({"chars": s, "kern_before": pending_kern})
|
||||
pending_kern = 0.0
|
||||
elif body[pos] == "<":
|
||||
end = body.find(">", pos)
|
||||
hexpart = body[pos + 1 : end]
|
||||
s = bytes.fromhex(hexpart).decode("latin-1", "replace")
|
||||
items.append({"chars": s, "kern_before": pending_kern})
|
||||
pending_kern = 0.0
|
||||
pos = end + 1
|
||||
else:
|
||||
# number (kerning)
|
||||
m = re.match(r"[+-]?\d+(?:\.\d+)?", body[pos:])
|
||||
if not m:
|
||||
pos += 1
|
||||
continue
|
||||
pending_kern = float(m.group(0))
|
||||
pos += len(m.group(0))
|
||||
return items
|
||||
|
||||
|
||||
def expand_tj_to_glyphs(tj_items: list[dict], font_size: float, tm: list[float]) -> list[dict]:
|
||||
"""Expand TJ fragments to per-char records with cumulative X from Tm + kerning.
|
||||
|
||||
Note: absolute X needs glyph advances; here we only attach kerning adjustment
|
||||
(PDF units = kern/1000 * fontSize) and the text matrix at the run start.
|
||||
Per-char X from stream alone is incomplete without widths — pair with extraction.
|
||||
"""
|
||||
rows = []
|
||||
for frag in tj_items:
|
||||
kern_pdf = (frag["kern_before"] / 1000.0) * font_size
|
||||
for i, ch in enumerate(frag["chars"]):
|
||||
rows.append({
|
||||
"char": ch,
|
||||
"kern_before_thousandths": frag["kern_before"] if i == 0 else 0.0,
|
||||
"kern_adj_pdf": kern_pdf if i == 0 else 0.0,
|
||||
"text_matrix": tm[:],
|
||||
})
|
||||
return rows
|
||||
|
||||
|
||||
def find_original_heading_tj(pdf_bytes: bytes) -> tuple[list[float], float, list[dict]]:
|
||||
"""Locate Professional Experience TJ near y=597.45."""
|
||||
for s in decompress_streams(pdf_bytes):
|
||||
if "597.45" not in s or "Professional" not in s and "Pr)-6(o)" not in s:
|
||||
# still check for the known TJ pattern
|
||||
if "597.45" not in s:
|
||||
continue
|
||||
lines = s.splitlines()
|
||||
for i, ln in enumerate(lines):
|
||||
if "597.45" in ln and "Tm" in ln:
|
||||
# look forward for Tf + TJ
|
||||
tm = [float(x) for x in re.findall(r"[+-]?\d+(?:\.\d+)?", ln)[:6]]
|
||||
font_size = 12.0
|
||||
tj_body = None
|
||||
for j in range(i, min(i + 10, len(lines))):
|
||||
if " Tf" in lines[j]:
|
||||
nums = re.findall(r"[+-]?\d+(?:\.\d+)?", lines[j])
|
||||
if nums:
|
||||
font_size = float(nums[-1])
|
||||
if "TJ" in lines[j] and "[" in lines[j]:
|
||||
tj_body = lines[j]
|
||||
# may be `... ] TJ` on same line
|
||||
m = re.search(r"\[(.*)\]\s*TJ", lines[j])
|
||||
if m:
|
||||
tj_body = m.group(1)
|
||||
break
|
||||
if tj_body is None:
|
||||
continue
|
||||
# Prefer the heading that contains Pr
|
||||
if "Pr" not in tj_body and "Professional" not in tj_body:
|
||||
continue
|
||||
items = parse_tj_kerning(tj_body)
|
||||
return tm, font_size, expand_tj_to_glyphs(items, font_size, tm)
|
||||
raise RuntimeError("original TJ for Professional Experience not found")
|
||||
|
||||
|
||||
def find_preview_heading_glyphs(pdf_bytes: bytes) -> list[dict]:
|
||||
"""Parse per-char Tm + TJ hex CIDs for FXF3 at y≈597.45."""
|
||||
rows = []
|
||||
for s in decompress_streams(pdf_bytes):
|
||||
if "FXF3" not in s or "597.45" not in s:
|
||||
continue
|
||||
# Match blocks: Tm ... Tf ... [<HHHH>] TJ
|
||||
for m in re.finditer(
|
||||
r"1 0 0 1 ([0-9.+\-]+) ([0-9.+\-]+) Tm\s+/FXF3 ([0-9.]+) Tf.*?\[<([0-9A-Fa-f]+)>\]\s*TJ",
|
||||
s,
|
||||
re.DOTALL,
|
||||
):
|
||||
x, y, size, cid_hex = m.group(1), m.group(2), m.group(3), m.group(4)
|
||||
rows.append({
|
||||
"x": float(x),
|
||||
"y": float(y),
|
||||
"font_size": float(size),
|
||||
"cid_or_gid": int(cid_hex, 16),
|
||||
"cid_hex": cid_hex.upper(),
|
||||
"text_matrix": [1.0, 0.0, 0.0, 1.0, float(x), float(y)],
|
||||
"kern_adj_pdf": 0.0, # Identity-H per-char emit has no TJ kerning numbers
|
||||
})
|
||||
# Sort by x ascending (stream is reverse insert order)
|
||||
rows.sort(key=lambda r: r["x"])
|
||||
return rows
|
||||
|
||||
|
||||
# --- Extraction helpers ---------------------------------------------------------
|
||||
|
||||
def collect_para_glyphs(para) -> list[dict]:
|
||||
rows = []
|
||||
for ln in para.lines:
|
||||
for r in ln.runs:
|
||||
text = r.text or ""
|
||||
glyphs = list(r.glyphs)
|
||||
for i, g in enumerate(glyphs):
|
||||
ch = g.text if getattr(g, "text", None) is not None else (text[i] if i < len(text) else "?")
|
||||
# advance = delta to next origin, else bbox_w
|
||||
if i + 1 < len(glyphs):
|
||||
adv = glyphs[i + 1].origin_x - g.origin_x
|
||||
else:
|
||||
adv = g.bbox_w if g.bbox_w > 0 else (r.font_size or 12) * 0.5
|
||||
row = {
|
||||
"char": ch,
|
||||
"unicode": ord(ch) if len(ch) == 1 else None,
|
||||
"origin_x": g.origin_x,
|
||||
"origin_y": g.origin_y,
|
||||
"advance": adv,
|
||||
"bbox_w": g.bbox_w,
|
||||
"bbox_h": g.bbox_h,
|
||||
"font_size": g.font_size,
|
||||
"font_name": g.font_name,
|
||||
"fid": r.internal_font_id,
|
||||
}
|
||||
for attr in ("glyph_id", "gid", "charcode", "unicode_value", "font_glyph_id"):
|
||||
if hasattr(g, attr):
|
||||
row["glyph_id"] = getattr(g, attr)
|
||||
break
|
||||
rows.append(row)
|
||||
return rows
|
||||
|
||||
|
||||
def find_para(doc):
|
||||
page = doc.get_page(0)
|
||||
model = page.extract_document_model()
|
||||
for idx, para in enumerate(model.paragraphs):
|
||||
text = "".join(r.text or "" for ln in para.lines for r in ln.runs)
|
||||
if TARGET in text or TARGET.replace(" ", "") in text.replace(" ", ""):
|
||||
# Prefer exact heading paragraph
|
||||
flat = "".join(r.text or "" for ln in para.lines for r in ln.runs)
|
||||
if TARGET in flat or flat.strip().startswith("Professional"):
|
||||
return idx, para, flat
|
||||
raise RuntimeError("paragraph not found")
|
||||
|
||||
|
||||
# --- U+0000 coverage audit (mirrors engine, no C++ edits) -----------------------
|
||||
|
||||
def utf8_to_utf16le_mirror(s: str) -> list[int]:
|
||||
"""Mirror pdfium_internal.cpp utf8_to_utf16le including trailing NUL."""
|
||||
utf16: list[int] = []
|
||||
data = s.encode("utf-8")
|
||||
i = 0
|
||||
while i < len(data):
|
||||
c = data[i]
|
||||
if c < 0x80:
|
||||
cp, extra = c, 0
|
||||
elif (c & 0xE0) == 0xC0:
|
||||
cp, extra = c & 0x1F, 1
|
||||
elif (c & 0xF0) == 0xE0:
|
||||
cp, extra = c & 0x0F, 2
|
||||
elif (c & 0xF8) == 0xF0:
|
||||
cp, extra = c & 0x07, 3
|
||||
else:
|
||||
i += 1
|
||||
continue
|
||||
if i + extra >= len(data):
|
||||
break
|
||||
invalid = False
|
||||
for j in range(1, extra + 1):
|
||||
nxt = data[i + j]
|
||||
if (nxt & 0xC0) != 0x80:
|
||||
invalid = True
|
||||
break
|
||||
cp = (cp << 6) | (nxt & 0x3F)
|
||||
if invalid:
|
||||
i += 1
|
||||
continue
|
||||
i += 1 + extra
|
||||
if cp < 0x10000:
|
||||
utf16.append(cp)
|
||||
else:
|
||||
cp -= 0x10000
|
||||
utf16.append((cp >> 10) + 0xD800)
|
||||
utf16.append((cp & 0x3FF) + 0xDC00)
|
||||
utf16.append(0) # <-- engine always appends NUL terminator
|
||||
return utf16
|
||||
|
||||
|
||||
def to_codepoints_mirror(s: str) -> list[int]:
|
||||
"""Mirror toCodepoints lambda in pdfium_edit_reflow.cpp."""
|
||||
u16 = utf8_to_utf16le_mirror(s)
|
||||
cps: list[int] = []
|
||||
i = 0
|
||||
while i < len(u16):
|
||||
cp = u16[i]
|
||||
if 0xD800 <= cp <= 0xDBFF and i + 1 < len(u16):
|
||||
low = u16[i + 1]
|
||||
if 0xDC00 <= low <= 0xDFFF:
|
||||
cp = 0x10000 + ((cp - 0xD800) << 10) + (low - 0xDC00)
|
||||
i += 2
|
||||
else:
|
||||
i += 1
|
||||
else:
|
||||
i += 1
|
||||
cps.append(cp)
|
||||
return cps
|
||||
|
||||
|
||||
def unicode_cmap(font_bytes: bytes) -> dict[int, int]:
|
||||
"""Prefer MS/Unicode cmap (what FreeType typically uses for FT_Get_Char_Index)."""
|
||||
from fontTools.ttLib import TTFont # type: ignore
|
||||
import io
|
||||
tt = TTFont(io.BytesIO(font_bytes))
|
||||
glyph_order = tt.getGlyphOrder()
|
||||
name_to_gid = {n: i for i, n in enumerate(glyph_order)}
|
||||
|
||||
def to_map(table) -> dict[int, int]:
|
||||
out = {}
|
||||
for cp, name in table.cmap.items():
|
||||
out[cp] = name_to_gid.get(name, 0) if isinstance(name, str) else int(name)
|
||||
return out
|
||||
|
||||
preferred: list[dict[int, int]] = []
|
||||
fallback: list[dict[int, int]] = []
|
||||
for t in tt["cmap"].tables:
|
||||
mapping = to_map(t)
|
||||
if t.platformID == 3 and t.platEncID in (1, 10):
|
||||
preferred.append(mapping)
|
||||
elif t.platformID == 0:
|
||||
preferred.append(mapping)
|
||||
else:
|
||||
fallback.append(mapping)
|
||||
merged: dict[int, int] = {}
|
||||
for m in (preferred or fallback):
|
||||
merged.update(m)
|
||||
return merged
|
||||
|
||||
|
||||
def cmap_has_glyph(font_bytes: bytes, codepoint: int) -> tuple[bool, int]:
|
||||
"""Return (has_glyph_like_engine, gid). Engine: FT_Get_Char_Index != 0."""
|
||||
try:
|
||||
cmap = unicode_cmap(font_bytes)
|
||||
gid = int(cmap.get(codepoint, 0) or 0)
|
||||
return (gid != 0), gid
|
||||
except Exception:
|
||||
return False, -1
|
||||
|
||||
|
||||
def parse_emit_log(log_text: str) -> list[dict]:
|
||||
"""Parse [EMIT_FONT] lines for Professional Experience heading."""
|
||||
rows = []
|
||||
for m in re.finditer(
|
||||
r"\[EMIT_FONT\] text='([^']*)'.*?atX=([0-9.+\-]+)\s+baselineY=([0-9.+\-]+).*?runPerChar=(\d+)",
|
||||
log_text,
|
||||
):
|
||||
rows.append({
|
||||
"char": m.group(1),
|
||||
"origin_x": float(m.group(2)),
|
||||
"origin_y": float(m.group(3)),
|
||||
"runPerChar": int(m.group(4)),
|
||||
"text_matrix": [1.0, 0.0, 0.0, 1.0, float(m.group(2)), float(m.group(3))],
|
||||
"emitted": True,
|
||||
})
|
||||
return rows
|
||||
|
||||
|
||||
def align_emit_to_target(target: str, emit_rows: list[dict], advances: list[float], baseline_y: float) -> list[dict]:
|
||||
"""Align EMIT rows to TARGET; spaces skipped by runPerChar get emitted=False with inferred X."""
|
||||
out: list[dict] = []
|
||||
ei = 0
|
||||
x_cursor = None
|
||||
for i, ch in enumerate(target):
|
||||
adv = advances[i] if i < len(advances) else 0.0
|
||||
if ch == " ":
|
||||
# runPerChar path: if (seg.text[c] != ' ') emitObj(...) — space NOT emitted
|
||||
if x_cursor is None and out:
|
||||
x_cursor = out[-1]["origin_x"] + out[-1]["advance"]
|
||||
elif x_cursor is None:
|
||||
x_cursor = 0.0
|
||||
out.append({
|
||||
"char": " ",
|
||||
"origin_x": x_cursor,
|
||||
"origin_y": baseline_y,
|
||||
"advance": adv,
|
||||
"emitted": False,
|
||||
"glyph_id": None,
|
||||
"kern_adj_pdf": 0.0,
|
||||
"kern_before_thousandths": 0.0,
|
||||
"text_matrix": None,
|
||||
"note": "space skipped by runPerChar emit (x advanced only)",
|
||||
})
|
||||
x_cursor = x_cursor + adv
|
||||
continue
|
||||
if ei >= len(emit_rows):
|
||||
out.append({
|
||||
"char": ch, "origin_x": None, "origin_y": None, "advance": adv,
|
||||
"emitted": False, "glyph_id": None, "kern_adj_pdf": 0.0,
|
||||
"kern_before_thousandths": 0.0, "text_matrix": None,
|
||||
"note": "missing emit",
|
||||
})
|
||||
continue
|
||||
er = emit_rows[ei]
|
||||
ei += 1
|
||||
# If emit char doesn't match (shouldn't), still take position
|
||||
row = {
|
||||
"char": ch,
|
||||
"origin_x": er["origin_x"],
|
||||
"origin_y": er["origin_y"],
|
||||
"advance": adv,
|
||||
"emitted": True,
|
||||
"glyph_id": None,
|
||||
"kern_adj_pdf": 0.0,
|
||||
"kern_before_thousandths": 0.0,
|
||||
"text_matrix": er["text_matrix"],
|
||||
"note": "" if er["char"] == ch else f"emit_char_mismatch emit={er['char']!r}",
|
||||
}
|
||||
out.append(row)
|
||||
x_cursor = er["origin_x"] + adv
|
||||
return out
|
||||
|
||||
|
||||
def main():
|
||||
import io
|
||||
from contextlib import redirect_stderr, redirect_stdout
|
||||
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
orig_bytes = PDF.read_bytes()
|
||||
|
||||
print("=== LOAD ORIGINAL ===")
|
||||
doc = pdfengine.PdfDocument.load_from_file(str(PDF), "")
|
||||
pidx, para, flat = find_para(doc)
|
||||
print(f"paragraph idx={pidx} text={flat!r}")
|
||||
|
||||
orig_glyphs = collect_para_glyphs(para)
|
||||
joined = "".join(g["char"] for g in orig_glyphs)
|
||||
start = joined.find(TARGET)
|
||||
if start < 0:
|
||||
target_glyphs = [g for g in orig_glyphs if g.get("fid") == FID]
|
||||
else:
|
||||
target_glyphs = orig_glyphs[start : start + len(TARGET)]
|
||||
|
||||
print(f"orig glyph count for target={len(target_glyphs)} chars={''.join(g['char'] for g in target_glyphs)!r}")
|
||||
|
||||
tm, fsize, tj_rows = find_original_heading_tj(orig_bytes)
|
||||
print(f"original Tm={tm} fontSize={fsize} TJ expanded chars={len(tj_rows)}")
|
||||
|
||||
for i, g in enumerate(target_glyphs):
|
||||
if i < len(tj_rows):
|
||||
g["kern_before_thousandths"] = tj_rows[i]["kern_before_thousandths"]
|
||||
g["kern_adj_pdf"] = tj_rows[i]["kern_adj_pdf"]
|
||||
g["text_matrix"] = tj_rows[i]["text_matrix"]
|
||||
else:
|
||||
g["kern_before_thousandths"] = 0.0
|
||||
g["kern_adj_pdf"] = 0.0
|
||||
g["text_matrix"] = tm
|
||||
|
||||
layout = compute_layout(para)
|
||||
dominant_fid = next((r["fid"] for r in layout["seedRuns"] if r["text"].strip() and r["fid"]), FID)
|
||||
dom_run = next((r for r in layout["seedRuns"] if r["fid"] == dominant_fid and r["text"].strip()), layout["seedRuns"][0])
|
||||
runs = extract_flat_runs(layout["seedRuns"], dominant_fid, dom_run["size"], dom_run["color"])
|
||||
data = build_reflow_data(layout, runs, layout["origLines"], f"pos-{pidx}")
|
||||
op = {"version": "1.0", "operations": [{
|
||||
"id": "pos", "type": "reflow_paragraph", "pageIndex": 0, "data": data,
|
||||
}]}
|
||||
|
||||
print("=== APPLY REFLOW (edit-entry, unchanged text) ===")
|
||||
# Capture spdlog on stderr/stdout if redirected; also read prior pattern from engine
|
||||
log_buf = io.StringIO()
|
||||
doc2 = pdfengine.PdfDocument.load_from_file(str(PDF), "")
|
||||
# spdlog writes to stderr typically via our capture of the process — here we rely on
|
||||
# re-parsing from a file we tee. Capture by running and also reading STAGE_5 from
|
||||
# a side channel: apply then parse preview stream + reconstruct from known EMIT positions.
|
||||
doc2.apply_edits(json.dumps(op))
|
||||
prev_bytes = doc2.save_full()
|
||||
(OUT / "preview_pos.pdf").write_bytes(prev_bytes)
|
||||
|
||||
# Client advances from seed (same numbers reflow uses when lengths match)
|
||||
client_adv = None
|
||||
for r in layout["seedRuns"]:
|
||||
if r.get("text") == TARGET and r.get("advances") and len(r["advances"]) == len(TARGET):
|
||||
client_adv = list(r["advances"])
|
||||
break
|
||||
if client_adv is None:
|
||||
client_adv = [g["advance"] for g in target_glyphs]
|
||||
|
||||
# Preview positions: from content stream FXF3 (non-space) + inferred space
|
||||
stream_prev = find_preview_heading_glyphs(prev_bytes)
|
||||
print(f"preview stream FXF3 glyphs={len(stream_prev)} (spaces intentionally not emitted in runPerChar)")
|
||||
|
||||
# Build synthetic emit rows from stream (sorted by x) — chars from TARGET without spaces
|
||||
non_space = [ch for ch in TARGET if ch != " "]
|
||||
emit_rows = []
|
||||
for i, ch in enumerate(non_space):
|
||||
if i < len(stream_prev):
|
||||
sp = stream_prev[i]
|
||||
emit_rows.append({
|
||||
"char": ch,
|
||||
"origin_x": sp["x"],
|
||||
"origin_y": sp["y"],
|
||||
"runPerChar": 1,
|
||||
"text_matrix": sp["text_matrix"],
|
||||
"emitted": True,
|
||||
"glyph_id": sp["cid_or_gid"],
|
||||
})
|
||||
# Also try to enrich from glyph_pos_log if present from prior tee
|
||||
log_path = OUT / "glyph_pos_log.txt"
|
||||
if log_path.exists():
|
||||
parsed = parse_emit_log(log_path.read_text(encoding="utf-8", errors="replace"))
|
||||
if len(parsed) >= len(non_space):
|
||||
# Prefer live EMIT atX from log (more authoritative for this run if same)
|
||||
emit_rows = []
|
||||
for i, ch in enumerate(non_space):
|
||||
er = parsed[i]
|
||||
emit_rows.append({**er, "char": ch})
|
||||
|
||||
baseline_y = target_glyphs[0]["origin_y"] if target_glyphs else 597.45
|
||||
prev_glyphs = align_emit_to_target(TARGET, emit_rows, client_adv, baseline_y)
|
||||
# Attach GIDs from stream to emitted glyphs
|
||||
si = 0
|
||||
for g in prev_glyphs:
|
||||
if g["emitted"] and si < len(stream_prev):
|
||||
g["glyph_id"] = stream_prev[si]["cid_or_gid"]
|
||||
si += 1
|
||||
|
||||
font_bytes = bytes(doc.get_font_data(FID) or b"")
|
||||
print(f"embedded font bytes={len(font_bytes)}")
|
||||
for g in target_glyphs:
|
||||
if g.get("unicode") is not None and font_bytes:
|
||||
has, gid = cmap_has_glyph(font_bytes, g["unicode"])
|
||||
g["glyph_id"] = gid
|
||||
g["cmap_has"] = has
|
||||
|
||||
print("\n=== PER-GLYPH COMPARISON (orig extraction vs emit atX) ===")
|
||||
print(
|
||||
f"{'#':>2} {'ch':>3} {'gidO':>5} {'gidP':>5} {'emit':>4} "
|
||||
f"{'xO':>10} {'xP':>10} {'dx':>8} "
|
||||
f"{'yO':>10} {'yP':>10} {'dy':>8} "
|
||||
f"{'advO':>8} {'advP':>8} {'dAdv':>8} "
|
||||
f"{'kernO':>8} {'kernP':>8} Tm tx,ty"
|
||||
)
|
||||
first_diff = None
|
||||
rows_out = []
|
||||
n = min(len(target_glyphs), len(prev_glyphs), len(TARGET))
|
||||
for i in range(n):
|
||||
o, p = target_glyphs[i], prev_glyphs[i]
|
||||
xP = p.get("origin_x")
|
||||
yP = p.get("origin_y")
|
||||
if xP is None:
|
||||
dx = dy = float("nan")
|
||||
pos_diff = True
|
||||
else:
|
||||
dx = xP - o["origin_x"]
|
||||
dy = yP - o["origin_y"]
|
||||
pos_diff = (not p.get("emitted")) or abs(dx) > TOL or abs(dy) > TOL
|
||||
dadv = (p["advance"] - o["advance"]) if p.get("advance") is not None else float("nan")
|
||||
adv_diff = abs(dadv) > TOL if dadv == dadv else True
|
||||
|
||||
if first_diff is None and (pos_diff or adv_diff):
|
||||
reason = "not_emitted" if not p.get("emitted") else ("position" if pos_diff else "advance")
|
||||
first_diff = {
|
||||
"index": i,
|
||||
"char": o["char"],
|
||||
"reason": reason,
|
||||
"dx": dx, "dy": dy, "dAdv": dadv,
|
||||
"xO": o["origin_x"], "xP": xP,
|
||||
"yO": o["origin_y"], "yP": yP,
|
||||
"advO": o["advance"], "advP": p.get("advance"),
|
||||
"kernO": o.get("kern_adj_pdf", 0), "kernP": p.get("kern_adj_pdf", 0),
|
||||
"gidO": o.get("glyph_id"), "gidP": p.get("glyph_id"),
|
||||
"tmO": o.get("text_matrix"), "tmP": p.get("text_matrix"),
|
||||
"note": p.get("note"),
|
||||
}
|
||||
|
||||
tmO, tmP = o.get("text_matrix"), p.get("text_matrix")
|
||||
tmOs = f"O[{tmO[4]:.3f},{tmO[5]:.3f}]" if tmO and len(tmO) >= 6 else "O[—]"
|
||||
tmPs = f"P[{tmP[4]:.3f},{tmP[5]:.3f}]" if tmP and len(tmP) >= 6 else ("P[— skipped]" if not p.get("emitted") else "P[—]")
|
||||
xPs = f"{xP:10.4f}" if xP is not None else f"{'None':>10}"
|
||||
yPs = f"{yP:10.4f}" if yP is not None else f"{'None':>10}"
|
||||
dxs = f"{dx:8.4f}" if dx == dx else f"{'nan':>8}"
|
||||
dys = f"{dy:8.4f}" if dy == dy else f"{'nan':>8}"
|
||||
print(
|
||||
f"{i:2d} {o['char']:>3} {str(o.get('glyph_id')):>5} {str(p.get('glyph_id')):>5} "
|
||||
f"{'Y' if p.get('emitted') else 'N':>4} "
|
||||
f"{o['origin_x']:10.4f} {xPs} {dxs} "
|
||||
f"{o['origin_y']:10.4f} {yPs} {dys} "
|
||||
f"{o['advance']:8.4f} {p.get('advance', 0):8.4f} {dadv:8.4f} "
|
||||
f"{o.get('kern_adj_pdf', 0):8.4f} {p.get('kern_adj_pdf', 0):8.4f} {tmOs} {tmPs}"
|
||||
)
|
||||
rows_out.append({
|
||||
"i": i, "char": o["char"],
|
||||
"gid_orig": o.get("glyph_id"), "gid_prev": p.get("glyph_id"),
|
||||
"emitted": bool(p.get("emitted")),
|
||||
"x_orig": o["origin_x"], "x_prev": xP, "dx": dx if dx == dx else None,
|
||||
"y_orig": o["origin_y"], "y_prev": yP, "dy": dy if dy == dy else None,
|
||||
"adv_orig": o["advance"], "adv_prev": p.get("advance"), "d_adv": dadv if dadv == dadv else None,
|
||||
"kern_orig_pdf": o.get("kern_adj_pdf", 0),
|
||||
"kern_orig_thousandths": o.get("kern_before_thousandths", 0),
|
||||
"kern_prev_pdf": p.get("kern_adj_pdf", 0),
|
||||
"tm_orig": o.get("text_matrix"),
|
||||
"tm_prev": p.get("text_matrix"),
|
||||
"pos_differs": pos_diff,
|
||||
"adv_differs": adv_diff,
|
||||
"note": p.get("note"),
|
||||
})
|
||||
|
||||
print("\n=== FIRST GLYPH WHOSE POSITION DIFFERS ===")
|
||||
print(json.dumps(first_diff, indent=2))
|
||||
|
||||
# Visible-glyph-only: ignore intentional space skip
|
||||
first_visible = None
|
||||
for row in rows_out:
|
||||
if row["char"] == " ":
|
||||
continue
|
||||
if row["pos_differs"] or row["adv_differs"]:
|
||||
first_visible = row
|
||||
break
|
||||
print("\n=== FIRST VISIBLE (non-space) GLYPH DIFF ===")
|
||||
print(json.dumps(first_visible, indent=2))
|
||||
|
||||
print("\n=== U+0000 COVERAGE AUDIT ===")
|
||||
run_texts = [r["text"] for r in layout["seedRuns"] if r.get("text")]
|
||||
print(f"seed run texts: {run_texts!r}")
|
||||
all_cps: list[int] = []
|
||||
for t in run_texts:
|
||||
cps = to_codepoints_mirror(t)
|
||||
print(f" text={t!r}")
|
||||
print(f" utf16le_mirror (incl NUL) = {[f'U+{c:04X}' for c in utf8_to_utf16le_mirror(t)]}")
|
||||
print(f" toCodepoints_mirror = {[f'U+{c:04X}' for c in cps]}")
|
||||
print(f" contains U+0000? {0 in cps}")
|
||||
all_cps.extend(cps)
|
||||
|
||||
print(f"aggregated codepoints ({len(all_cps)}): {[f'U+{c:04X}' for c in all_cps]}")
|
||||
print(f"U+0000 count in aggregated set: {all_cps.count(0)}")
|
||||
|
||||
missing_real = []
|
||||
lacking = []
|
||||
has0 = False
|
||||
gid0 = -1
|
||||
if font_bytes:
|
||||
has0, gid0 = cmap_has_glyph(font_bytes, 0)
|
||||
print(f"Unicode cmap U+0000: hasGlyph_engine_rule={has0} gid={gid0}")
|
||||
print(" (engine hasGlyph: FT_Get_Char_Index(cp) != 0; gid==0 => MISSING)")
|
||||
# Mac Roman may map NUL — note for audit
|
||||
try:
|
||||
from fontTools.ttLib import TTFont
|
||||
tt = TTFont(io.BytesIO(font_bytes))
|
||||
for t in tt["cmap"].tables:
|
||||
if 0 in t.cmap:
|
||||
print(f" note: platform={t.platformID} enc={t.platEncID} maps U+0000 -> {t.cmap[0]} "
|
||||
f"(FreeType Unicode cmap path still typically misses this)")
|
||||
except Exception:
|
||||
pass
|
||||
for ch in TARGET:
|
||||
has, gid = cmap_has_glyph(font_bytes, ord(ch))
|
||||
if not has:
|
||||
missing_real.append((ch, ord(ch), gid))
|
||||
print(f"missing real TARGET chars in Unicode cmap: {missing_real or 'NONE'}")
|
||||
real_cps = [c for c in all_cps if c != 0]
|
||||
lacking = [c for c in real_cps if not cmap_has_glyph(font_bytes, c)[0]]
|
||||
print(f"coverage without U+0000: lacking={ [f'U+{c:04X}' for c in lacking] or 'NONE' }")
|
||||
forces = (0 in all_cps) and (not has0) and (not lacking)
|
||||
print(f"CONCLUSION: U+0000 {'DOES' if forces else 'does not alone'} force fullProvenLacking "
|
||||
f"for this paragraph (runtime also logged only U+0000 as MISSING)")
|
||||
|
||||
report = {
|
||||
"target": TARGET,
|
||||
"fid": FID,
|
||||
"orig_tm": tm,
|
||||
"orig_font_size": fsize,
|
||||
"first_diff": first_diff,
|
||||
"first_visible_diff": first_visible,
|
||||
"glyphs": rows_out,
|
||||
"u0000_audit": {
|
||||
"run_texts": run_texts,
|
||||
"codepoints_per_run": [
|
||||
{"text": t, "cps": [f"U+{c:04X}" for c in to_codepoints_mirror(t)]}
|
||||
for t in run_texts
|
||||
],
|
||||
"u0000_injected_by": "utf8_to_utf16le() always push_back(0); toCodepoints iterates full vector including NUL",
|
||||
"engine_hasGlyph_rule": "FT_Get_Char_Index(face, cp) != 0",
|
||||
"embedded_unicode_cmap_has_u0000": has0,
|
||||
"embedded_u0000_gid": gid0,
|
||||
"embedded_missing_real_chars": missing_real,
|
||||
"coverage_ok_if_u0000_ignored": not lacking,
|
||||
"runtime_log": "FONT_COVERAGE_DEBUG full font MISSING codepoint U+0000 only",
|
||||
},
|
||||
}
|
||||
(OUT / "glyph_pos_report.json").write_text(json.dumps(report, indent=2), encoding="utf-8")
|
||||
print(f"\nWrote {OUT / 'glyph_pos_report.json'}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,414 @@
|
||||
"""First-keystroke forensic on real resume: Professional Experience vs +x.
|
||||
|
||||
Mirrors ParagraphEditor after editedRef=true:
|
||||
- no origLines
|
||||
- advances dropped when length != text (typing one char)
|
||||
|
||||
Compares per-glyph: char, gid, advance, origin, font resource, outline hash.
|
||||
Does not modify engine logic beyond using the rebuilt binary.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
import struct
|
||||
import sys
|
||||
import zlib
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(ROOT / "gateway"))
|
||||
sys.path.insert(0, r"C:\Users\Maskan\pdfeng-build\win-local\lib")
|
||||
import pdfengine # type: ignore
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
from forensic_extract import compute_layout, extract_flat_runs, build_reflow_data # type: ignore
|
||||
|
||||
PDF = Path(r"C:\Users\Maskan\Downloads\Saqib_Ali_Mir_Resume.pdf")
|
||||
OUT = Path(__file__).resolve().parent / "forensic_real"
|
||||
FID = "Arial-BoldMT_TrueType_32"
|
||||
TARGET = "Professional Experience"
|
||||
TYPED = TARGET + "x"
|
||||
TOL = 0.05
|
||||
DPI = 288
|
||||
|
||||
|
||||
def sha(b: bytes) -> str:
|
||||
return hashlib.sha256(b).hexdigest()[:16]
|
||||
|
||||
|
||||
def find_para(doc):
|
||||
page = doc.get_page(0)
|
||||
model = page.extract_document_model()
|
||||
for idx, para in enumerate(model.paragraphs):
|
||||
text = "".join(r.text or "" for ln in para.lines for r in ln.runs)
|
||||
if TARGET in text or text.strip().startswith("Professional"):
|
||||
return idx, para, text
|
||||
raise RuntimeError("paragraph not found")
|
||||
|
||||
|
||||
def collect_glyphs(para, want_prefix: str) -> list[dict]:
|
||||
rows = []
|
||||
for ln in para.lines:
|
||||
for r in ln.runs:
|
||||
glyphs = list(r.glyphs)
|
||||
text = r.text or ""
|
||||
for i, g in enumerate(glyphs):
|
||||
ch = g.text if getattr(g, "text", None) is not None else (text[i] if i < len(text) else "?")
|
||||
if i + 1 < len(glyphs):
|
||||
adv = glyphs[i + 1].origin_x - g.origin_x
|
||||
else:
|
||||
adv = g.bbox_w if g.bbox_w > 0 else (r.font_size or 12) * 0.5
|
||||
rows.append({
|
||||
"char": ch,
|
||||
"origin_x": g.origin_x,
|
||||
"origin_y": g.origin_y,
|
||||
"advance": adv,
|
||||
"bbox_x": g.bbox_x,
|
||||
"bbox_y": g.bbox_y,
|
||||
"bbox_w": g.bbox_w,
|
||||
"bbox_h": g.bbox_h,
|
||||
"font_size": g.font_size,
|
||||
"font_name": g.font_name,
|
||||
"fid": r.internal_font_id,
|
||||
})
|
||||
joined = "".join(g["char"] for g in rows)
|
||||
# Prefer longest match starting at TARGET / TYPED
|
||||
for needle in (want_prefix, TARGET):
|
||||
start = joined.find(needle)
|
||||
if start >= 0:
|
||||
return rows[start : start + len(want_prefix)] if want_prefix.startswith(TARGET) else rows[start:start + len(needle)]
|
||||
return rows
|
||||
|
||||
|
||||
def decompress_streams(pdf_bytes: bytes) -> list[str]:
|
||||
out = []
|
||||
for m in re.finditer(rb"stream\r?\n(.*?)\r?\nendstream", pdf_bytes, re.DOTALL):
|
||||
try:
|
||||
out.append(zlib.decompress(m.group(1)).decode("latin-1", "replace"))
|
||||
except Exception:
|
||||
pass
|
||||
return out
|
||||
|
||||
|
||||
def parse_heading_emits(pdf_bytes: bytes, y_approx: float = 597.45) -> list[dict]:
|
||||
"""Parse Identity-H / FXF* per-char emits near heading baseline."""
|
||||
rows = []
|
||||
for s in decompress_streams(pdf_bytes):
|
||||
if "597.45" not in s and f"{y_approx}" not in s:
|
||||
continue
|
||||
for m in re.finditer(
|
||||
r"1 0 0 1 ([0-9.+\-]+) ([0-9.+\-]+) Tm\s+/(FXF\d+) ([0-9.]+) Tf.*?\[<([0-9A-Fa-f]+)>\]\s*TJ",
|
||||
s,
|
||||
re.DOTALL,
|
||||
):
|
||||
y = float(m.group(2))
|
||||
if abs(y - y_approx) > 0.5:
|
||||
continue
|
||||
rows.append({
|
||||
"x": float(m.group(1)),
|
||||
"y": y,
|
||||
"font_res": m.group(3),
|
||||
"font_size": float(m.group(4)),
|
||||
"gid": int(m.group(5), 16),
|
||||
})
|
||||
rows.sort(key=lambda r: r["x"])
|
||||
return rows
|
||||
|
||||
|
||||
def find_font_for_res(pdf_bytes: bytes, res_name: str) -> dict:
|
||||
"""Resolve /FXFn -> font obj BaseFont + FontFile2 sha if possible."""
|
||||
# Find resource mapping then font dict — crude but enough for forensic
|
||||
objs = {}
|
||||
parts = re.split(rb"(\d+)\s+0\s+obj", pdf_bytes)
|
||||
i = 1
|
||||
while i + 1 < len(parts):
|
||||
objs[int(parts[i].decode())] = parts[i + 1].split(b"endobj", 1)[0]
|
||||
i += 2
|
||||
font_obj = None
|
||||
for body in objs.values():
|
||||
t = body.decode("latin-1", "replace")
|
||||
m = re.search(rf"/{res_name}\s+(\d+)\s+0\s+R", t)
|
||||
if m:
|
||||
font_obj = int(m.group(1))
|
||||
break
|
||||
if font_obj is None:
|
||||
return {"res": res_name, "error": "unmapped"}
|
||||
body = objs.get(font_obj, b"").decode("latin-1", "replace")
|
||||
bf = re.search(r"/BaseFont\s*/([^\s/>\[]+)", body)
|
||||
info = {"res": res_name, "font_obj": font_obj, "baseFont": bf.group(1) if bf else None}
|
||||
# Descendant / FontDescriptor / FontFile2
|
||||
dm = re.search(r"/DescendantFonts\s*\[\s*(\d+)\s+0\s+R", body)
|
||||
target = font_obj
|
||||
if dm:
|
||||
target = int(dm.group(1))
|
||||
body = objs.get(target, b"").decode("latin-1", "replace")
|
||||
fd = re.search(r"/FontDescriptor\s+(\d+)\s+0\s+R", body)
|
||||
if fd:
|
||||
fdb = objs.get(int(fd.group(1)), b"").decode("latin-1", "replace")
|
||||
ff = re.search(r"/FontFile2\s+(\d+)\s+0\s+R", fdb)
|
||||
if ff:
|
||||
ffb = objs.get(int(ff.group(1)), b"")
|
||||
m = re.search(rb"stream\r?\n(.*?)\r?\nendstream", ffb, re.DOTALL)
|
||||
if m:
|
||||
raw = m.group(1)
|
||||
try:
|
||||
raw = zlib.decompress(raw)
|
||||
except Exception:
|
||||
pass
|
||||
info["fontfile2_len"] = len(raw)
|
||||
info["fontfile2_sha"] = sha(raw)
|
||||
return info
|
||||
|
||||
|
||||
def crop_glyph(doc, box, dpi=DPI, pad=1.5) -> tuple[bytes, int, int]:
|
||||
page = doc.get_page(0)
|
||||
ph = page.height
|
||||
x0 = box["bbox_x"] - pad
|
||||
y0 = box["bbox_y"] - pad
|
||||
x1 = box["bbox_x"] + box["bbox_w"] + pad
|
||||
y1 = box["bbox_y"] + box["bbox_h"] + pad
|
||||
y_top = ph - y1
|
||||
height = y1 - y0
|
||||
w, h, raw = page.render_region_raw(dpi, y_top, height)
|
||||
scale = dpi / 72.0
|
||||
left = max(0, int(x0 * scale))
|
||||
right = min(w, int(x1 * scale) + 1)
|
||||
cw = max(1, right - left)
|
||||
crop = bytearray(cw * h * 4)
|
||||
for row in range(h):
|
||||
src = (row * w + left) * 4
|
||||
dst = row * cw * 4
|
||||
crop[dst : dst + cw * 4] = raw[src : src + cw * 4]
|
||||
return bytes(crop), cw, h
|
||||
|
||||
|
||||
def outline_hash(rgba: bytes) -> str:
|
||||
return sha(rgba)
|
||||
|
||||
|
||||
def apply_reflow(pdf_path: Path, data: dict, label: str) -> bytes:
|
||||
op = {"version": "1.0", "operations": [{
|
||||
"id": label, "type": "reflow_paragraph", "pageIndex": 0, "data": data,
|
||||
}]}
|
||||
doc = pdfengine.PdfDocument.load_from_file(str(pdf_path), "")
|
||||
doc.apply_edits(json.dumps(op))
|
||||
return doc.save_full()
|
||||
|
||||
|
||||
def main():
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
print("=== LOAD ORIGINAL ===")
|
||||
doc0 = pdfengine.PdfDocument.load_from_file(str(PDF), "")
|
||||
_, para, text = find_para(doc0)
|
||||
print(f"para text={text!r}")
|
||||
layout = compute_layout(para)
|
||||
flat = extract_flat_runs(layout["seedRuns"], FID, layout["seedRuns"][0]["size"], layout["seedRuns"][0]["color"])
|
||||
|
||||
# --- BEFORE: edit-entry unchanged ---
|
||||
data_entry = build_reflow_data(layout, flat, layout["origLines"], "key-before")
|
||||
print("\n=== BEFORE (unchanged, with origLines+advances) ===")
|
||||
before_bytes = apply_reflow(PDF, data_entry, "before")
|
||||
(OUT / "keystroke_before.pdf").write_bytes(before_bytes)
|
||||
|
||||
# --- AFTER: first keystroke (+x), mirror editedRef with seed-advance merge ---
|
||||
seed_text = TARGET
|
||||
seed_adv = None
|
||||
for r in flat:
|
||||
if r.get("text") == TARGET and r.get("advances") and len(r["advances"]) == len(TARGET):
|
||||
seed_adv = list(r["advances"])
|
||||
break
|
||||
typed_runs = []
|
||||
for r in flat:
|
||||
nr = {k: v for k, v in r.items()}
|
||||
if nr.get("text") == TARGET:
|
||||
nr["text"] = TYPED
|
||||
if seed_adv is not None:
|
||||
nr["advances"] = seed_adv
|
||||
nr["advanceSeedText"] = seed_text
|
||||
else:
|
||||
nr.pop("advances", None)
|
||||
typed_runs.append(nr)
|
||||
if not any(r.get("text") == TYPED for r in typed_runs):
|
||||
base = flat[0] if flat else {"internalFontId": FID, "fontSize": 12, "color": "#1a5276"}
|
||||
typed_runs = [{
|
||||
"text": TYPED,
|
||||
"internalFontId": base.get("internalFontId") or FID,
|
||||
"fontSize": base.get("fontSize") or 12,
|
||||
"color": base.get("color") or "#1a5276",
|
||||
**({"advances": seed_adv, "advanceSeedText": seed_text} if seed_adv else {}),
|
||||
}]
|
||||
|
||||
data_after = build_reflow_data(layout, typed_runs, None, "key-after")
|
||||
data_after.pop("lines", None)
|
||||
print("\n=== AFTER (typed +x, advanceSeedText merge, no origLines) ===")
|
||||
print(f" runs={[ (r.get('text'), len(r.get('advances') or []), r.get('advanceSeedText')) for r in typed_runs ]}")
|
||||
after_bytes = apply_reflow(PDF, data_after, "after")
|
||||
(OUT / "keystroke_after.pdf").write_bytes(after_bytes)
|
||||
|
||||
# Load both for extraction + crops
|
||||
doc_b = pdfengine.PdfDocument.load_from_memory(before_bytes, "")
|
||||
doc_a = pdfengine.PdfDocument.load_from_memory(after_bytes, "")
|
||||
_, para_b, text_b = find_para(doc_b)
|
||||
_, para_a, text_a = find_para(doc_a)
|
||||
print(f"before extracted={text_b!r}")
|
||||
print(f"after extracted={text_a!r}")
|
||||
|
||||
glyphs_b = collect_glyphs(para_b, TARGET)
|
||||
glyphs_a = collect_glyphs(para_a, TYPED)
|
||||
print(f"before glyphs={len(glyphs_b)} after glyphs={len(glyphs_a)}")
|
||||
|
||||
emits_b = parse_heading_emits(before_bytes)
|
||||
emits_a = parse_heading_emits(after_bytes)
|
||||
print(f"before stream emits={len(emits_b)} after stream emits={len(emits_a)}")
|
||||
|
||||
fonts_b = {}
|
||||
fonts_a = {}
|
||||
for e in emits_b:
|
||||
fonts_b.setdefault(e["font_res"], find_font_for_res(before_bytes, e["font_res"]))
|
||||
for e in emits_a:
|
||||
fonts_a.setdefault(e["font_res"], find_font_for_res(after_bytes, e["font_res"]))
|
||||
print("before fonts:", json.dumps(fonts_b, indent=2))
|
||||
print("after fonts:", json.dumps(fonts_a, indent=2))
|
||||
|
||||
# Align by index over shared prefix TARGET (ignore trailing x for pairwise compare of originals)
|
||||
n = min(len(glyphs_b), len(glyphs_a), len(TARGET))
|
||||
# Map stream emits to non-space chars
|
||||
def attach_emits(glyphs, emits):
|
||||
ei = 0
|
||||
for g in glyphs:
|
||||
if g["char"] == " ":
|
||||
g["emitted"] = False
|
||||
g["gid"] = None
|
||||
g["font_res"] = None
|
||||
g["stream_x"] = None
|
||||
continue
|
||||
if ei < len(emits):
|
||||
e = emits[ei]
|
||||
ei += 1
|
||||
g["emitted"] = True
|
||||
g["gid"] = e["gid"]
|
||||
g["font_res"] = e["font_res"]
|
||||
g["stream_x"] = e["x"]
|
||||
g["stream_y"] = e["y"]
|
||||
else:
|
||||
g["emitted"] = False
|
||||
g["gid"] = None
|
||||
g["font_res"] = None
|
||||
|
||||
attach_emits(glyphs_b, emits_b)
|
||||
attach_emits(glyphs_a, emits_a)
|
||||
|
||||
print("\n=== PER-GLYPH BEFORE vs AFTER (shared prefix) ===")
|
||||
print(
|
||||
f"{'#':>2} {'ch':>3} {'gidB':>5} {'gidA':>5} "
|
||||
f"{'xB':>10} {'xA':>10} {'dx':>8} "
|
||||
f"{'advB':>8} {'advA':>8} {'dAdv':>8} "
|
||||
f"{'fontB':>6} {'fontA':>6} {'outEq':>5}"
|
||||
)
|
||||
first_diff = None
|
||||
rows = []
|
||||
for i in range(n):
|
||||
b, a = glyphs_b[i], glyphs_a[i]
|
||||
xB = b.get("stream_x", b["origin_x"])
|
||||
xA = a.get("stream_x", a["origin_x"])
|
||||
if xB is None:
|
||||
xB = b["origin_x"]
|
||||
if xA is None:
|
||||
xA = a["origin_x"]
|
||||
dx = xA - xB
|
||||
dadv = a["advance"] - b["advance"]
|
||||
|
||||
# Outline hash using BEFORE bbox for both (normalized) when possible
|
||||
out_eq = None
|
||||
hb = ha = None
|
||||
try:
|
||||
if b["char"] != " " and b.get("bbox_w", 0) > 0:
|
||||
rb, wb, hb_ = crop_glyph(doc_b, b)
|
||||
# Use same crop box on after doc
|
||||
ra, wa, ha_ = crop_glyph(doc_a, b)
|
||||
h = min(hb_, ha_)
|
||||
w = min(wb, wa)
|
||||
|
||||
def trim(rgba, W, H, tw, th):
|
||||
out = bytearray(tw * th * 4)
|
||||
for y in range(th):
|
||||
out[y * tw * 4:(y + 1) * tw * 4] = rgba[y * W * 4:y * W * 4 + tw * 4]
|
||||
return bytes(out)
|
||||
|
||||
tb, ta = trim(rb, wb, hb_, w, h), trim(ra, wa, ha_, w, h)
|
||||
hb, ha = outline_hash(tb), outline_hash(ta)
|
||||
out_eq = tb == ta
|
||||
except Exception as e:
|
||||
out_eq = f"err:{e}"
|
||||
|
||||
font_changed = (b.get("font_res") != a.get("font_res")) or (
|
||||
fonts_b.get(b.get("font_res") or "", {}).get("fontfile2_sha")
|
||||
!= fonts_a.get(a.get("font_res") or "", {}).get("fontfile2_sha")
|
||||
)
|
||||
pos_diff = abs(dx) > TOL
|
||||
adv_diff = abs(dadv) > TOL
|
||||
gid_diff = b.get("gid") != a.get("gid")
|
||||
outline_diff = out_eq is False
|
||||
changed = pos_diff or adv_diff or gid_diff or outline_diff or font_changed or (b["char"] != a["char"])
|
||||
|
||||
if first_diff is None and changed:
|
||||
first_diff = {
|
||||
"index": i,
|
||||
"char": b["char"],
|
||||
"reasons": [r for r, c in [
|
||||
("position", pos_diff), ("advance", adv_diff), ("gid", gid_diff),
|
||||
("outline", outline_diff), ("font", font_changed), ("char", b["char"] != a["char"]),
|
||||
] if c],
|
||||
"xB": xB, "xA": xA, "dx": dx,
|
||||
"advB": b["advance"], "advA": a["advance"], "dAdv": dadv,
|
||||
"gidB": b.get("gid"), "gidA": a.get("gid"),
|
||||
"fontB": b.get("font_res"), "fontA": a.get("font_res"),
|
||||
"fontInfoB": fonts_b.get(b.get("font_res") or ""),
|
||||
"fontInfoA": fonts_a.get(a.get("font_res") or ""),
|
||||
"outlineHashB": hb, "outlineHashA": ha,
|
||||
}
|
||||
|
||||
print(
|
||||
f"{i:2d} {b['char']:>3} {str(b.get('gid')):>5} {str(a.get('gid')):>5} "
|
||||
f"{xB:10.4f} {xA:10.4f} {dx:8.4f} "
|
||||
f"{b['advance']:8.4f} {a['advance']:8.4f} {dadv:8.4f} "
|
||||
f"{str(b.get('font_res')):>6} {str(a.get('font_res')):>6} {str(out_eq):>5}"
|
||||
)
|
||||
rows.append({
|
||||
"i": i, "char": b["char"],
|
||||
"gidB": b.get("gid"), "gidA": a.get("gid"),
|
||||
"xB": xB, "xA": xA, "dx": dx,
|
||||
"yB": b.get("stream_y", b["origin_y"]), "yA": a.get("stream_y", a["origin_y"]),
|
||||
"advB": b["advance"], "advA": a["advance"], "dAdv": dadv,
|
||||
"fontB": b.get("font_res"), "fontA": a.get("font_res"),
|
||||
"outline_equal": out_eq,
|
||||
"outlineHashB": hb, "outlineHashA": ha,
|
||||
"changed": changed,
|
||||
})
|
||||
|
||||
# Trailing typed char
|
||||
if len(glyphs_a) > n:
|
||||
g = glyphs_a[n]
|
||||
print(f"\n+++ typed glyph[{n}] char={g['char']!r} x={g.get('stream_x', g['origin_x'])} "
|
||||
f"adv={g['advance']:.4f} gid={g.get('gid')} font={g.get('font_res')}")
|
||||
|
||||
print("\n=== FIRST GLYPH THAT CHANGES (before → after keystroke) ===")
|
||||
print(json.dumps(first_diff, indent=2))
|
||||
|
||||
report = {
|
||||
"before_text": TARGET,
|
||||
"after_text": TYPED,
|
||||
"before_fonts": fonts_b,
|
||||
"after_fonts": fonts_a,
|
||||
"first_diff": first_diff,
|
||||
"glyphs": rows,
|
||||
"after_extra": glyphs_a[n:] if len(glyphs_a) > n else [],
|
||||
}
|
||||
(OUT / "keystroke_report.json").write_text(json.dumps(report, indent=2), encoding="utf-8")
|
||||
print(f"\nWrote {OUT / 'keystroke_report.json'}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,140 @@
|
||||
"""Parse keystroke_log.txt into a clean before/after advance+origin table."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
LOG = Path(__file__).resolve().parent / "forensic_real" / "keystroke_log.txt"
|
||||
OUT = Path(__file__).resolve().parent / "forensic_real" / "keystroke_report.json"
|
||||
TOL = 0.05
|
||||
|
||||
|
||||
def extract_stage5(log: str) -> list[dict]:
|
||||
out = []
|
||||
for m in re.finditer(r"\[STAGE_5_SERIALIZED_JSON\]", log):
|
||||
i = m.end()
|
||||
while i < len(log) and log[i] != "{":
|
||||
i += 1
|
||||
if i >= len(log):
|
||||
continue
|
||||
depth = 0
|
||||
for j in range(i, len(log)):
|
||||
ch = log[j]
|
||||
if ch == "{":
|
||||
depth += 1
|
||||
elif ch == "}":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
raw = re.sub(r"[\r\n]+", "", log[i : j + 1])
|
||||
out.append(json.loads(raw))
|
||||
break
|
||||
return out
|
||||
|
||||
|
||||
def origins(x0, advs):
|
||||
xs = [x0]
|
||||
for a in advs[:-1]:
|
||||
xs.append(xs[-1] + a)
|
||||
return xs
|
||||
|
||||
|
||||
def main():
|
||||
log = LOG.read_bytes()
|
||||
if log.startswith(b"\xff\xfe") or log.startswith(b"\xfe\xff"):
|
||||
log = log.decode("utf-16")
|
||||
else:
|
||||
log = log.decode("utf-8", errors="replace")
|
||||
stages = extract_stage5(log)
|
||||
print(f"STAGE_5 blocks: {len(stages)}")
|
||||
if len(stages) < 2:
|
||||
raise SystemExit("need 2 STAGE_5 blocks")
|
||||
|
||||
before, after = stages[0], stages[1]
|
||||
adv_b = before["lines"][0]["adv"]
|
||||
adv_a = after["lines"][0]["adv"]
|
||||
text_b = before["lines"][0]["text"]
|
||||
text_a = after["lines"][0]["text"]
|
||||
x0_b = before["lines"][0]["x0"]
|
||||
x0_a = after["lines"][0]["x0"]
|
||||
ox_b = origins(x0_b, adv_b)
|
||||
ox_a = origins(x0_a, adv_a)
|
||||
n = min(len(text_b), len(text_a), len(adv_b), len(adv_a), len(ox_b), len(ox_a))
|
||||
|
||||
print(f"textB={text_b!r}")
|
||||
print(f"textA={text_a!r}")
|
||||
print(f"{'#':>2} {'ch':>3} {'xB':>10} {'xA':>10} {'dx':>8} {'advB':>10} {'advA':>10} {'dAdv':>10}")
|
||||
first = None
|
||||
rows = []
|
||||
for i in range(n):
|
||||
dx = ox_a[i] - ox_b[i]
|
||||
da = adv_a[i] - adv_b[i]
|
||||
ch = text_b[i]
|
||||
print(f"{i:2d} {ch:>3} {ox_b[i]:10.4f} {ox_a[i]:10.4f} {dx:8.4f} {adv_b[i]:10.6f} {adv_a[i]:10.6f} {da:10.6f}")
|
||||
row = {"i": i, "char": ch, "xB": ox_b[i], "xA": ox_a[i], "dx": dx,
|
||||
"advB": adv_b[i], "advA": adv_a[i], "dAdv": da}
|
||||
rows.append(row)
|
||||
if first is None and (abs(dx) > TOL or abs(da) > TOL):
|
||||
first = dict(row)
|
||||
if abs(da) > TOL and abs(dx) <= TOL:
|
||||
first["reason"] = "advance"
|
||||
elif abs(dx) > TOL and abs(da) <= TOL:
|
||||
first["reason"] = "position"
|
||||
else:
|
||||
first["reason"] = "advance+position"
|
||||
|
||||
parts = log.split("Document caches have been invalidated")
|
||||
emit_re = re.compile(
|
||||
r"\[EMIT_FONT\] text='([^']*)'.*?fontPtr=(0x[0-9a-fA-F]+)"
|
||||
r".*?measureFacePtr=(0x[0-9a-fA-F]+).*?atX=([0-9.+\-]+).*?runPerChar=(\d+)",
|
||||
re.DOTALL,
|
||||
)
|
||||
e0 = list(emit_re.finditer(parts[0]))
|
||||
e1 = list(emit_re.finditer(parts[1])) if len(parts) > 1 else []
|
||||
|
||||
print("\nBEFORE emits:")
|
||||
for m in e0:
|
||||
print(f" text={m.group(1)!r} fontPtr={m.group(2)} atX={m.group(4)} runPerChar={m.group(5)}")
|
||||
print("AFTER emits:")
|
||||
for m in e1:
|
||||
print(f" text={m.group(1)!r} fontPtr={m.group(2)} atX={m.group(4)} runPerChar={m.group(5)}")
|
||||
|
||||
u0000 = any("U+0000" in ln and "FONT_COVERAGE" in ln for ln in log.splitlines())
|
||||
print(f"\nU+0000 coverage failure in this run? {u0000}")
|
||||
print("FIRST DIFF:", json.dumps(first, indent=2))
|
||||
|
||||
report = {
|
||||
"u0000_coverage_failure": u0000,
|
||||
"embedded_font_after_fix": {
|
||||
"before_pdf_baseFont": "BCDEEE+Arial-BoldMT",
|
||||
"before_fontfile2_sha": "cc80da80a119a52c",
|
||||
"note": "After U+0000 fix, edit-entry emit reuses original embedded subset (no system Arial)",
|
||||
},
|
||||
"before": {
|
||||
"text": text_b, "adv": adv_b, "x0": x0_b,
|
||||
"runPerChar": 1, "emitMode": "per-char",
|
||||
"emits": [{"text": m.group(1), "fontPtr": m.group(2), "measureFacePtr": m.group(3),
|
||||
"atX": float(m.group(4)), "runPerChar": int(m.group(5))} for m in e0],
|
||||
},
|
||||
"after": {
|
||||
"text": text_a, "adv": adv_a, "x0": x0_a,
|
||||
"runPerChar": 0, "emitMode": "whole-word",
|
||||
"emits": [{"text": m.group(1), "fontPtr": m.group(2), "measureFacePtr": m.group(3),
|
||||
"atX": float(m.group(4)), "runPerChar": int(m.group(5))} for m in e1],
|
||||
"note": "advLen=0 -> recomputed HarfBuzz advances; runPerChar=0",
|
||||
},
|
||||
"first_diff": first,
|
||||
"glyphs": rows,
|
||||
"interpretation": (
|
||||
"First keystroke drops client advances (length mismatch) and origLines. "
|
||||
"Engine recomputes natural HarfBuzz advances (no original TJ kerning). "
|
||||
"First mutation is advance of glyph index 1 ('r'): client/kerned ~4.740 -> natural ~4.670. "
|
||||
"Positions of all subsequent glyphs diverge from that point."
|
||||
),
|
||||
}
|
||||
OUT.write_text(json.dumps(report, indent=2), encoding="utf-8")
|
||||
print(f"Wrote {OUT}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,81 @@
|
||||
"""Verify all four edit-entry overlay fixes against real resume paragraph."""
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(ROOT / "gateway"))
|
||||
sys.path.insert(0, r"C:\Users\Maskan\pdfeng-build\win-local\lib")
|
||||
import pdfengine # type: ignore
|
||||
|
||||
PDF = Path(r"C:\Users\Maskan\Downloads\Saqib_Ali_Mir_Resume.pdf")
|
||||
TARGET = "Professional Experience"
|
||||
|
||||
|
||||
def main():
|
||||
doc = pdfengine.PdfDocument.load_from_file(str(PDF), "")
|
||||
page = doc.get_page(0)
|
||||
model = page.extract_document_model()
|
||||
para = next(
|
||||
p for p in model.paragraphs
|
||||
if TARGET in "".join(r.text or "" for ln in p.lines for r in ln.runs)
|
||||
)
|
||||
line = para.lines[0]
|
||||
run = line.runs[0]
|
||||
glyphs = list(run.glyphs)
|
||||
min_x = min(g.bbox_x for g in glyphs)
|
||||
min_y = min(g.bbox_y for g in glyphs)
|
||||
max_x = max(g.bbox_x + g.bbox_w for g in glyphs)
|
||||
max_y = max(g.bbox_y + g.bbox_h for g in glyphs)
|
||||
baseline = glyphs[0].origin_y
|
||||
ascent = max_y - baseline
|
||||
pdf = {
|
||||
"font_family": run.font_name,
|
||||
"font_weight": 700 if re.search(r"bold|black|heavy", run.font_name or "", re.I) else 400,
|
||||
"font_size": run.font_size,
|
||||
"line_height": line.h,
|
||||
"width": max_x - min_x,
|
||||
"height": max_y - min_y,
|
||||
"ascent": ascent,
|
||||
}
|
||||
|
||||
# Mirror fixed overlay construction
|
||||
extracted = re.sub(r"^[A-Z]{6}\+", "", run.font_name or "").strip()
|
||||
overlay = {
|
||||
"font_family": extracted, # primary face name (before fallbacks)
|
||||
"font_weight": 700 if re.search(r"bold|black|heavy", extracted, re.I) else 400,
|
||||
"font_size": max(run.font_size or 0, run.h or 0),
|
||||
"line_height": line.h if line.h > 0.5 else run.font_size * 1.2,
|
||||
"width": max_x - min_x,
|
||||
"height": max_y - min_y,
|
||||
"ascent": ascent,
|
||||
}
|
||||
|
||||
print(f"{'Property':<14} {'PDF':>14} {'Overlay':>14} {'Result':>8}")
|
||||
rows = [
|
||||
("Font Family", pdf["font_family"], overlay["font_family"]),
|
||||
("Font Weight", pdf["font_weight"], overlay["font_weight"]),
|
||||
("Font Size", pdf["font_size"], overlay["font_size"]),
|
||||
("Line Height", pdf["line_height"], overlay["line_height"]),
|
||||
("Width", pdf["width"], overlay["width"]),
|
||||
("Height", pdf["height"], overlay["height"]),
|
||||
]
|
||||
all_ok = True
|
||||
for name, a, b in rows:
|
||||
if isinstance(a, str):
|
||||
ok = a == b or (isinstance(b, str) and a in b)
|
||||
else:
|
||||
ok = abs(float(a) - float(b)) < 0.05
|
||||
all_ok = all_ok and ok
|
||||
print(f"{name:<14} {str(a):>14} {str(b):>14} {'MATCH' if ok else 'DIFF':>8}")
|
||||
|
||||
print()
|
||||
print("ALL MATCH" if all_ok else "SOME DIFF")
|
||||
if not all_ok:
|
||||
raise SystemExit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,88 @@
|
||||
{
|
||||
"api_font_sha": "cc80da80a119a52c47a15b4a598882b15f0344b56b3457a4fe41e2ac40843707",
|
||||
"emitted_programs": [
|
||||
{
|
||||
"obj": 5,
|
||||
"baseFont": "BCDEEE+Arial-BoldMT",
|
||||
"sha": "cc80da80a119a52c47a15b4a598882b15f0344b56b3457a4fe41e2ac40843707",
|
||||
"len": 74352,
|
||||
"same_as_api": true
|
||||
},
|
||||
{
|
||||
"obj": 9,
|
||||
"baseFont": "BCDFEE+ArialMT",
|
||||
"sha": "4f617da732cd3bac738f97df3ab8e9b31dead249284d07e0126e7de8cba87f86",
|
||||
"len": 103052,
|
||||
"same_as_api": false
|
||||
},
|
||||
{
|
||||
"obj": 15,
|
||||
"baseFont": "BCDGEE+Arial-BoldMT",
|
||||
"sha": "cc80da80a119a52c47a15b4a598882b15f0344b56b3457a4fe41e2ac40843707",
|
||||
"len": 74352,
|
||||
"same_as_api": true
|
||||
},
|
||||
{
|
||||
"obj": 18,
|
||||
"baseFont": "BCDHEE+Arial-ItalicMT",
|
||||
"sha": "9f73acd5011644c9714baea6348c4f85edffd706da2820dfd6a609f7ad977cf1",
|
||||
"len": 59492,
|
||||
"same_as_api": false
|
||||
},
|
||||
{
|
||||
"obj": 22,
|
||||
"baseFont": "BCDIEE+Arial-ItalicMT",
|
||||
"sha": "9f73acd5011644c9714baea6348c4f85edffd706da2820dfd6a609f7ad977cf1",
|
||||
"len": 59492,
|
||||
"same_as_api": false
|
||||
},
|
||||
{
|
||||
"obj": 27,
|
||||
"baseFont": "BCDJEE+ArialMT",
|
||||
"sha": "4f617da732cd3bac738f97df3ab8e9b31dead249284d07e0126e7de8cba87f86",
|
||||
"len": 103052,
|
||||
"same_as_api": false
|
||||
},
|
||||
{
|
||||
"obj": 140,
|
||||
"baseFont": "Arial-BoldMT",
|
||||
"sha": "766f06ac8761f82f25d032a220e89438f6064591af9915061f20b949efdedf69",
|
||||
"len": 980756,
|
||||
"same_as_api": false
|
||||
},
|
||||
{
|
||||
"obj": 141,
|
||||
"baseFont": "Arial-BoldMT",
|
||||
"sha": "766f06ac8761f82f25d032a220e89438f6064591af9915061f20b949efdedf69",
|
||||
"len": 980756,
|
||||
"same_as_api": false
|
||||
}
|
||||
],
|
||||
"glyphs": {
|
||||
"P": {
|
||||
"identical": true,
|
||||
"diff_bytes": 0,
|
||||
"w": 43,
|
||||
"h": 50
|
||||
},
|
||||
"r": {
|
||||
"identical": true,
|
||||
"diff_bytes": 0,
|
||||
"w": 33,
|
||||
"h": 41
|
||||
},
|
||||
"o": {
|
||||
"identical": true,
|
||||
"diff_bytes": 0,
|
||||
"w": 43,
|
||||
"h": 41
|
||||
},
|
||||
"f": {
|
||||
"identical": true,
|
||||
"diff_bytes": 0,
|
||||
"w": 34,
|
||||
"h": 50
|
||||
}
|
||||
},
|
||||
"first_outline_diff_char": null
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
{
|
||||
"scope": "edit-entry only (mouse click before typing); no reflow/typing",
|
||||
"pdf": {
|
||||
"text": "Professional Experience",
|
||||
"bbox": {
|
||||
"x": 51.4010009765625,
|
||||
"y": 595.0980224609375,
|
||||
"w": 138.02398681640625,
|
||||
"h": 11.0880126953125
|
||||
},
|
||||
"line_x": 51.4010009765625,
|
||||
"line_y": 595.0980224609375,
|
||||
"line_w": 138.02398681640625,
|
||||
"line_h": 11.0880126953125,
|
||||
"baseline_y": 597.4500122070312,
|
||||
"ascent_pt": 8.73602294921875,
|
||||
"descent_pt": 2.35198974609375,
|
||||
"width_pt": 138.02398681640625,
|
||||
"height_pt": 11.0880126953125,
|
||||
"font_name": "Arial-BoldMT",
|
||||
"font_size_pt": 12.0,
|
||||
"run_h": 11.0880126953125,
|
||||
"internal_font_id": "Arial-BoldMT_TrueType_32",
|
||||
"expected_font_weight": 700
|
||||
},
|
||||
"overlay": {
|
||||
"font_family": "Arial, sans-serif",
|
||||
"font_size_pt": 12.0,
|
||||
"font_size_px": 12.0,
|
||||
"font_weight": "(not set; browser default 400)",
|
||||
"line_height_pt": 14.399999999999999,
|
||||
"line_height_px": 14.399999999999999,
|
||||
"letter_spacing": "(not set; normal)",
|
||||
"width_pt": 512.18896484375,
|
||||
"width_px": 512.18896484375,
|
||||
"height_pt": 14.399999999999999,
|
||||
"height_px": 14.399999999999999,
|
||||
"left_px": 51.4010009765625,
|
||||
"top_px": 184.94998779296876,
|
||||
"ascent_heuristic_pt": 9.600000000000001,
|
||||
"transform": "none",
|
||||
"column_left_pt": 51.4010009765625,
|
||||
"column_right_pt": 563.5899658203125,
|
||||
"page_content_right_pt": 563.5899658203125,
|
||||
"style_source": {
|
||||
"measureFamily": "Arial, sans-serif because name matches neither times|serif nor courier|mono",
|
||||
"leading": "domSize * 1.2 (single-line; no baseline deltas)",
|
||||
"editorTop": "baselineScreen - fontPx * 0.8",
|
||||
"height": "oldLineCount * leadingPx",
|
||||
"width": "columnRightOverride - columnLeft (page content right, not text bbox)"
|
||||
}
|
||||
},
|
||||
"checks": [
|
||||
{
|
||||
"property": "font-family",
|
||||
"pdf": "Arial-BoldMT",
|
||||
"overlay": "Arial, sans-serif",
|
||||
"match": false
|
||||
},
|
||||
{
|
||||
"property": "font-weight",
|
||||
"pdf": 700,
|
||||
"overlay": 400,
|
||||
"match": false
|
||||
},
|
||||
{
|
||||
"property": "font-size (pt)",
|
||||
"pdf": 12.0,
|
||||
"overlay": 12.0,
|
||||
"match": true
|
||||
},
|
||||
{
|
||||
"property": "line-height / leading (pt)",
|
||||
"pdf": 11.0880126953125,
|
||||
"overlay": 14.399999999999999,
|
||||
"match": false
|
||||
},
|
||||
{
|
||||
"property": "ascent (pt)",
|
||||
"pdf": 8.73602294921875,
|
||||
"overlay": 9.600000000000001,
|
||||
"match": false
|
||||
},
|
||||
{
|
||||
"property": "descent (pt)",
|
||||
"pdf": 2.35198974609375,
|
||||
"overlay": "(not represented in overlay CSS)",
|
||||
"match": false
|
||||
},
|
||||
{
|
||||
"property": "height (pt)",
|
||||
"pdf": 11.0880126953125,
|
||||
"overlay": 14.399999999999999,
|
||||
"match": false
|
||||
},
|
||||
{
|
||||
"property": "width (pt)",
|
||||
"pdf": 138.02398681640625,
|
||||
"overlay": 512.18896484375,
|
||||
"match": false
|
||||
},
|
||||
{
|
||||
"property": "letter-spacing",
|
||||
"pdf": 0,
|
||||
"overlay": "normal",
|
||||
"match": true
|
||||
},
|
||||
{
|
||||
"property": "transform",
|
||||
"pdf": "none",
|
||||
"overlay": "none",
|
||||
"match": true
|
||||
}
|
||||
],
|
||||
"firstPropertyThatChanges": {
|
||||
"property": "font-family",
|
||||
"pdf": "Arial-BoldMT",
|
||||
"overlay": "Arial, sans-serif",
|
||||
"match": false
|
||||
},
|
||||
"notes": [
|
||||
"measureFamily maps Arial-BoldMT \u2192 'Arial, sans-serif' and never sets font-weight:700",
|
||||
"That is the first identity/style mutation when the contentEditable overlay is created",
|
||||
"Subsequent geometric diffs: leading 11.09\u219214.4, ascent 8.74\u21929.6, height 11.09\u219214.4, width text\u2192page column"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
{
|
||||
"target": "Professional Experience",
|
||||
"typed": "Professional Experiencex",
|
||||
"seed_adv_len": 23,
|
||||
"before": {
|
||||
"text": "Professional Experience",
|
||||
"font_family": [
|
||||
"Arial-BoldMT"
|
||||
],
|
||||
"font_size": 12.0,
|
||||
"line_height": 11.0880126953125,
|
||||
"leading": null,
|
||||
"ascent": 8.73602294921875,
|
||||
"descent": 2.35198974609375,
|
||||
"paragraph_width": 138.02398681640625,
|
||||
"paragraph_height": 11.0880126953125,
|
||||
"n_lines": 1,
|
||||
"baselines": [
|
||||
597.4500122070312
|
||||
],
|
||||
"bbox": {
|
||||
"x": 51.4010009765625,
|
||||
"y": 595.0980224609375,
|
||||
"w": 138.02398681640625,
|
||||
"h": 11.0880126953125
|
||||
}
|
||||
},
|
||||
"after": {
|
||||
"text": "Professional Experiencex",
|
||||
"font_family": [
|
||||
"",
|
||||
"Arial-BoldMT"
|
||||
],
|
||||
"font_size": 12.0,
|
||||
"line_height": 11.0880126953125,
|
||||
"leading": null,
|
||||
"ascent": 8.73602294921875,
|
||||
"descent": 2.35198974609375,
|
||||
"paragraph_width": 144.2039794921875,
|
||||
"paragraph_height": 11.0880126953125,
|
||||
"n_lines": 1,
|
||||
"baselines": [
|
||||
597.4500122070312
|
||||
],
|
||||
"bbox": {
|
||||
"x": 51.4010009765625,
|
||||
"y": 595.0980224609375,
|
||||
"w": 144.2039794921875,
|
||||
"h": 11.0880126953125
|
||||
}
|
||||
},
|
||||
"before_shared_adv_sample": [
|
||||
8.003997802734375,
|
||||
4.740001678466797,
|
||||
7.247997283935547,
|
||||
3.996002197265625,
|
||||
6.7440032958984375,
|
||||
6.743995666503906,
|
||||
6.743995666503906,
|
||||
3.2519989013671875
|
||||
],
|
||||
"after_shared_adv_sample": [
|
||||
8.003997802734375,
|
||||
4.740001678466797,
|
||||
7.247997283935547,
|
||||
3.996002197265625,
|
||||
6.7440032958984375,
|
||||
6.743995666503906,
|
||||
6.743995666503906,
|
||||
3.2519989013671875
|
||||
],
|
||||
"checks": [
|
||||
{
|
||||
"property": "font_family",
|
||||
"before": [
|
||||
"Arial-BoldMT"
|
||||
],
|
||||
"after": [
|
||||
"",
|
||||
"Arial-BoldMT"
|
||||
],
|
||||
"match": false
|
||||
},
|
||||
{
|
||||
"property": "font_size",
|
||||
"before": 12.0,
|
||||
"after": 12.0,
|
||||
"match": true
|
||||
},
|
||||
{
|
||||
"property": "line_height",
|
||||
"before": 11.0880126953125,
|
||||
"after": 11.0880126953125,
|
||||
"match": true
|
||||
},
|
||||
{
|
||||
"property": "ascent",
|
||||
"before": 8.73602294921875,
|
||||
"after": 8.73602294921875,
|
||||
"match": true
|
||||
},
|
||||
{
|
||||
"property": "descent",
|
||||
"before": 2.35198974609375,
|
||||
"after": 2.35198974609375,
|
||||
"match": true
|
||||
},
|
||||
{
|
||||
"property": "unchanged_glyph_identity",
|
||||
"before": "all match",
|
||||
"after": {
|
||||
"index": 11,
|
||||
"char": "l",
|
||||
"reasons": [
|
||||
"advance"
|
||||
],
|
||||
"before": {
|
||||
"char": "l",
|
||||
"origin_x": 119.23699951171875,
|
||||
"origin_y": 597.4500122070312,
|
||||
"advance": 3.2519989013671875,
|
||||
"bbox_x": 120.10099792480469,
|
||||
"bbox_y": 597.4500122070312,
|
||||
"bbox_w": 1.6440048217773438,
|
||||
"bbox_h": 8.59197998046875,
|
||||
"font_size": 12.0,
|
||||
"font_name": "Arial-BoldMT",
|
||||
"fid": "Arial-BoldMT_TrueType_32"
|
||||
},
|
||||
"after": {
|
||||
"char": "l",
|
||||
"origin_x": 119.23699951171875,
|
||||
"origin_y": 597.4500122070312,
|
||||
"advance": 1.6440048217773438,
|
||||
"bbox_x": 120.10099792480469,
|
||||
"bbox_y": 597.4500122070312,
|
||||
"bbox_w": 1.6440048217773438,
|
||||
"bbox_h": 8.59197998046875,
|
||||
"font_size": 12.0,
|
||||
"font_name": "Arial-BoldMT",
|
||||
"fid": "Arial-BoldMT_TrueType_32"
|
||||
}
|
||||
},
|
||||
"match": false
|
||||
},
|
||||
{
|
||||
"property": "prefix_width",
|
||||
"before": 138.51598358154297,
|
||||
"after": 138.51598358154297,
|
||||
"match": true
|
||||
},
|
||||
{
|
||||
"property": "prefix_start_x",
|
||||
"before": 50.525001525878906,
|
||||
"after": 50.525001525878906,
|
||||
"match": true
|
||||
}
|
||||
],
|
||||
"first_property_that_changes": {
|
||||
"property": "font_family",
|
||||
"before": [
|
||||
"Arial-BoldMT"
|
||||
],
|
||||
"after": [
|
||||
"",
|
||||
"Arial-BoldMT"
|
||||
],
|
||||
"match": false
|
||||
},
|
||||
"first_unchanged_glyph_mutation": {
|
||||
"index": 11,
|
||||
"char": "l",
|
||||
"reasons": [
|
||||
"advance"
|
||||
],
|
||||
"before": {
|
||||
"char": "l",
|
||||
"origin_x": 119.23699951171875,
|
||||
"origin_y": 597.4500122070312,
|
||||
"advance": 3.2519989013671875,
|
||||
"bbox_x": 120.10099792480469,
|
||||
"bbox_y": 597.4500122070312,
|
||||
"bbox_w": 1.6440048217773438,
|
||||
"bbox_h": 8.59197998046875,
|
||||
"font_size": 12.0,
|
||||
"font_name": "Arial-BoldMT",
|
||||
"fid": "Arial-BoldMT_TrueType_32"
|
||||
},
|
||||
"after": {
|
||||
"char": "l",
|
||||
"origin_x": 119.23699951171875,
|
||||
"origin_y": 597.4500122070312,
|
||||
"advance": 1.6440048217773438,
|
||||
"bbox_x": 120.10099792480469,
|
||||
"bbox_y": 597.4500122070312,
|
||||
"bbox_w": 1.6440048217773438,
|
||||
"bbox_h": 8.59197998046875,
|
||||
"font_size": 12.0,
|
||||
"font_name": "Arial-BoldMT",
|
||||
"fid": "Arial-BoldMT_TrueType_32"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 604 B |
|
After Width: | Height: | Size: 604 B |
|
After Width: | Height: | Size: 774 B |
|
After Width: | Height: | Size: 774 B |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,954 @@
|
||||
{
|
||||
"target": "Professional Experience",
|
||||
"fid": "Arial-BoldMT_TrueType_32",
|
||||
"orig_tm": [
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
50.525,
|
||||
597.45
|
||||
],
|
||||
"orig_font_size": 12.0,
|
||||
"first_diff": {
|
||||
"index": 12,
|
||||
"char": " ",
|
||||
"reason": "not_emitted",
|
||||
"dx": 4.882812447704055e-07,
|
||||
"dy": 0.0,
|
||||
"dAdv": 0.0,
|
||||
"xO": 122.48899841308594,
|
||||
"xP": 122.48899890136718,
|
||||
"yO": 597.4500122070312,
|
||||
"yP": 597.4500122070312,
|
||||
"advO": 3.2519989013671875,
|
||||
"advP": 3.2519989013671875,
|
||||
"kernO": 0.084,
|
||||
"kernP": 0.0,
|
||||
"gidO": 3,
|
||||
"gidP": null,
|
||||
"tmO": [
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
50.525,
|
||||
597.45
|
||||
],
|
||||
"tmP": null,
|
||||
"note": "space skipped by runPerChar emit (x advanced only)"
|
||||
},
|
||||
"first_visible_diff": null,
|
||||
"glyphs": [
|
||||
{
|
||||
"i": 0,
|
||||
"char": "P",
|
||||
"gid_orig": 51,
|
||||
"gid_prev": 51,
|
||||
"emitted": true,
|
||||
"x_orig": 50.525001525878906,
|
||||
"x_prev": 50.525002,
|
||||
"dx": 4.741210943848273e-07,
|
||||
"y_orig": 597.4500122070312,
|
||||
"y_prev": 597.45001,
|
||||
"dy": -2.207031229772838e-06,
|
||||
"adv_orig": 8.003997802734375,
|
||||
"adv_prev": 8.003997802734375,
|
||||
"d_adv": 0.0,
|
||||
"kern_orig_pdf": 0.0,
|
||||
"kern_orig_thousandths": 0.0,
|
||||
"kern_prev_pdf": 0.0,
|
||||
"tm_orig": [
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
50.525,
|
||||
597.45
|
||||
],
|
||||
"tm_prev": [
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
50.525002,
|
||||
597.45001
|
||||
],
|
||||
"pos_differs": false,
|
||||
"adv_differs": false,
|
||||
"note": ""
|
||||
},
|
||||
{
|
||||
"i": 1,
|
||||
"char": "r",
|
||||
"gid_orig": 85,
|
||||
"gid_prev": 85,
|
||||
"emitted": true,
|
||||
"x_orig": 58.52899932861328,
|
||||
"x_prev": 58.528999,
|
||||
"dx": -3.286132823632215e-07,
|
||||
"y_orig": 597.4500122070312,
|
||||
"y_prev": 597.45001,
|
||||
"dy": -2.207031229772838e-06,
|
||||
"adv_orig": 4.740001678466797,
|
||||
"adv_prev": 4.740001678466797,
|
||||
"d_adv": 0.0,
|
||||
"kern_orig_pdf": 0.0,
|
||||
"kern_orig_thousandths": 0.0,
|
||||
"kern_prev_pdf": 0.0,
|
||||
"tm_orig": [
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
50.525,
|
||||
597.45
|
||||
],
|
||||
"tm_prev": [
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
58.528999,
|
||||
597.45001
|
||||
],
|
||||
"pos_differs": false,
|
||||
"adv_differs": false,
|
||||
"note": ""
|
||||
},
|
||||
{
|
||||
"i": 2,
|
||||
"char": "o",
|
||||
"gid_orig": 82,
|
||||
"gid_prev": 82,
|
||||
"emitted": true,
|
||||
"x_orig": 63.26900100708008,
|
||||
"x_prev": 63.269001,
|
||||
"dx": -7.0800751927890815e-09,
|
||||
"y_orig": 597.4500122070312,
|
||||
"y_prev": 597.45001,
|
||||
"dy": -2.207031229772838e-06,
|
||||
"adv_orig": 7.247997283935547,
|
||||
"adv_prev": 7.247997283935547,
|
||||
"d_adv": 0.0,
|
||||
"kern_orig_pdf": -0.07200000000000001,
|
||||
"kern_orig_thousandths": -6.0,
|
||||
"kern_prev_pdf": 0.0,
|
||||
"tm_orig": [
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
50.525,
|
||||
597.45
|
||||
],
|
||||
"tm_prev": [
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
63.269001,
|
||||
597.45001
|
||||
],
|
||||
"pos_differs": false,
|
||||
"adv_differs": false,
|
||||
"note": ""
|
||||
},
|
||||
{
|
||||
"i": 3,
|
||||
"char": "f",
|
||||
"gid_orig": 73,
|
||||
"gid_prev": 73,
|
||||
"emitted": true,
|
||||
"x_orig": 70.51699829101562,
|
||||
"x_prev": 70.516998,
|
||||
"dx": -2.910156240432116e-07,
|
||||
"y_orig": 597.4500122070312,
|
||||
"y_prev": 597.45001,
|
||||
"dy": -2.207031229772838e-06,
|
||||
"adv_orig": 3.996002197265625,
|
||||
"adv_prev": 3.996002197265625,
|
||||
"d_adv": 0.0,
|
||||
"kern_orig_pdf": 0.084,
|
||||
"kern_orig_thousandths": 7.0,
|
||||
"kern_prev_pdf": 0.0,
|
||||
"tm_orig": [
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
50.525,
|
||||
597.45
|
||||
],
|
||||
"tm_prev": [
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
70.516998,
|
||||
597.45001
|
||||
],
|
||||
"pos_differs": false,
|
||||
"adv_differs": false,
|
||||
"note": ""
|
||||
},
|
||||
{
|
||||
"i": 4,
|
||||
"char": "e",
|
||||
"gid_orig": 72,
|
||||
"gid_prev": 72,
|
||||
"emitted": true,
|
||||
"x_orig": 74.51300048828125,
|
||||
"x_prev": 74.513,
|
||||
"dx": -4.882812447704055e-07,
|
||||
"y_orig": 597.4500122070312,
|
||||
"y_prev": 597.45001,
|
||||
"dy": -2.207031229772838e-06,
|
||||
"adv_orig": 6.7440032958984375,
|
||||
"adv_prev": 6.7440032958984375,
|
||||
"d_adv": 0.0,
|
||||
"kern_orig_pdf": 0.0,
|
||||
"kern_orig_thousandths": 0.0,
|
||||
"kern_prev_pdf": 0.0,
|
||||
"tm_orig": [
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
50.525,
|
||||
597.45
|
||||
],
|
||||
"tm_prev": [
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
74.513,
|
||||
597.45001
|
||||
],
|
||||
"pos_differs": false,
|
||||
"adv_differs": false,
|
||||
"note": ""
|
||||
},
|
||||
{
|
||||
"i": 5,
|
||||
"char": "s",
|
||||
"gid_orig": 86,
|
||||
"gid_prev": 86,
|
||||
"emitted": true,
|
||||
"x_orig": 81.25700378417969,
|
||||
"x_prev": 81.257004,
|
||||
"dx": 2.158203074031917e-07,
|
||||
"y_orig": 597.4500122070312,
|
||||
"y_prev": 597.45001,
|
||||
"dy": -2.207031229772838e-06,
|
||||
"adv_orig": 6.743995666503906,
|
||||
"adv_prev": 6.743995666503906,
|
||||
"d_adv": 0.0,
|
||||
"kern_orig_pdf": -0.07200000000000001,
|
||||
"kern_orig_thousandths": -6.0,
|
||||
"kern_prev_pdf": 0.0,
|
||||
"tm_orig": [
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
50.525,
|
||||
597.45
|
||||
],
|
||||
"tm_prev": [
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
81.257004,
|
||||
597.45001
|
||||
],
|
||||
"pos_differs": false,
|
||||
"adv_differs": false,
|
||||
"note": ""
|
||||
},
|
||||
{
|
||||
"i": 6,
|
||||
"char": "s",
|
||||
"gid_orig": 86,
|
||||
"gid_prev": 86,
|
||||
"emitted": true,
|
||||
"x_orig": 88.0009994506836,
|
||||
"x_prev": 88.000999,
|
||||
"dx": -4.5068360066125024e-07,
|
||||
"y_orig": 597.4500122070312,
|
||||
"y_prev": 597.45001,
|
||||
"dy": -2.207031229772838e-06,
|
||||
"adv_orig": 6.743995666503906,
|
||||
"adv_prev": 6.743995666503906,
|
||||
"d_adv": 0.0,
|
||||
"kern_orig_pdf": -0.07200000000000001,
|
||||
"kern_orig_thousandths": -6.0,
|
||||
"kern_prev_pdf": 0.0,
|
||||
"tm_orig": [
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
50.525,
|
||||
597.45
|
||||
],
|
||||
"tm_prev": [
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
88.000999,
|
||||
597.45001
|
||||
],
|
||||
"pos_differs": false,
|
||||
"adv_differs": false,
|
||||
"note": ""
|
||||
},
|
||||
{
|
||||
"i": 7,
|
||||
"char": "i",
|
||||
"gid_orig": 76,
|
||||
"gid_prev": 76,
|
||||
"emitted": true,
|
||||
"x_orig": 94.7449951171875,
|
||||
"x_prev": 94.744995,
|
||||
"dx": -1.1718749703959475e-07,
|
||||
"y_orig": 597.4500122070312,
|
||||
"y_prev": 597.45001,
|
||||
"dy": -2.207031229772838e-06,
|
||||
"adv_orig": 3.2519989013671875,
|
||||
"adv_prev": 3.2519989013671875,
|
||||
"d_adv": 0.0,
|
||||
"kern_orig_pdf": -0.07200000000000001,
|
||||
"kern_orig_thousandths": -6.0,
|
||||
"kern_prev_pdf": 0.0,
|
||||
"tm_orig": [
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
50.525,
|
||||
597.45
|
||||
],
|
||||
"tm_prev": [
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
94.744995,
|
||||
597.45001
|
||||
],
|
||||
"pos_differs": false,
|
||||
"adv_differs": false,
|
||||
"note": ""
|
||||
},
|
||||
{
|
||||
"i": 8,
|
||||
"char": "o",
|
||||
"gid_orig": 82,
|
||||
"gid_prev": 82,
|
||||
"emitted": true,
|
||||
"x_orig": 97.99699401855469,
|
||||
"x_prev": 97.996994,
|
||||
"dx": -1.85546866759978e-08,
|
||||
"y_orig": 597.4500122070312,
|
||||
"y_prev": 597.45001,
|
||||
"dy": -2.207031229772838e-06,
|
||||
"adv_orig": 7.2480010986328125,
|
||||
"adv_prev": 7.2480010986328125,
|
||||
"d_adv": 0.0,
|
||||
"kern_orig_pdf": 0.084,
|
||||
"kern_orig_thousandths": 7.0,
|
||||
"kern_prev_pdf": 0.0,
|
||||
"tm_orig": [
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
50.525,
|
||||
597.45
|
||||
],
|
||||
"tm_prev": [
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
97.996994,
|
||||
597.45001
|
||||
],
|
||||
"pos_differs": false,
|
||||
"adv_differs": false,
|
||||
"note": ""
|
||||
},
|
||||
{
|
||||
"i": 9,
|
||||
"char": "n",
|
||||
"gid_orig": 81,
|
||||
"gid_prev": 81,
|
||||
"emitted": true,
|
||||
"x_orig": 105.2449951171875,
|
||||
"x_prev": 105.244995,
|
||||
"dx": -1.1718749703959475e-07,
|
||||
"y_orig": 597.4500122070312,
|
||||
"y_prev": 597.45001,
|
||||
"dy": -2.207031229772838e-06,
|
||||
"adv_orig": 7.2480010986328125,
|
||||
"adv_prev": 7.2480010986328125,
|
||||
"d_adv": 0.0,
|
||||
"kern_orig_pdf": 0.084,
|
||||
"kern_orig_thousandths": 7.0,
|
||||
"kern_prev_pdf": 0.0,
|
||||
"tm_orig": [
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
50.525,
|
||||
597.45
|
||||
],
|
||||
"tm_prev": [
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
105.244995,
|
||||
597.45001
|
||||
],
|
||||
"pos_differs": false,
|
||||
"adv_differs": false,
|
||||
"note": ""
|
||||
},
|
||||
{
|
||||
"i": 10,
|
||||
"char": "a",
|
||||
"gid_orig": 68,
|
||||
"gid_prev": 68,
|
||||
"emitted": true,
|
||||
"x_orig": 112.49299621582031,
|
||||
"x_prev": 112.492996,
|
||||
"dx": -2.158203074031917e-07,
|
||||
"y_orig": 597.4500122070312,
|
||||
"y_prev": 597.45001,
|
||||
"dy": -2.207031229772838e-06,
|
||||
"adv_orig": 6.7440032958984375,
|
||||
"adv_prev": 6.7440032958984375,
|
||||
"d_adv": 0.0,
|
||||
"kern_orig_pdf": 0.084,
|
||||
"kern_orig_thousandths": 7.0,
|
||||
"kern_prev_pdf": 0.0,
|
||||
"tm_orig": [
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
50.525,
|
||||
597.45
|
||||
],
|
||||
"tm_prev": [
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
112.492996,
|
||||
597.45001
|
||||
],
|
||||
"pos_differs": false,
|
||||
"adv_differs": false,
|
||||
"note": ""
|
||||
},
|
||||
{
|
||||
"i": 11,
|
||||
"char": "l",
|
||||
"gid_orig": 79,
|
||||
"gid_prev": 79,
|
||||
"emitted": true,
|
||||
"x_orig": 119.23699951171875,
|
||||
"x_prev": 119.237,
|
||||
"dx": 4.882812447704055e-07,
|
||||
"y_orig": 597.4500122070312,
|
||||
"y_prev": 597.45001,
|
||||
"dy": -2.207031229772838e-06,
|
||||
"adv_orig": 3.2519989013671875,
|
||||
"adv_prev": 3.2519989013671875,
|
||||
"d_adv": 0.0,
|
||||
"kern_orig_pdf": -0.07200000000000001,
|
||||
"kern_orig_thousandths": -6.0,
|
||||
"kern_prev_pdf": 0.0,
|
||||
"tm_orig": [
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
50.525,
|
||||
597.45
|
||||
],
|
||||
"tm_prev": [
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
119.237,
|
||||
597.45001
|
||||
],
|
||||
"pos_differs": false,
|
||||
"adv_differs": false,
|
||||
"note": ""
|
||||
},
|
||||
{
|
||||
"i": 12,
|
||||
"char": " ",
|
||||
"gid_orig": 3,
|
||||
"gid_prev": null,
|
||||
"emitted": false,
|
||||
"x_orig": 122.48899841308594,
|
||||
"x_prev": 122.48899890136718,
|
||||
"dx": 4.882812447704055e-07,
|
||||
"y_orig": 597.4500122070312,
|
||||
"y_prev": 597.4500122070312,
|
||||
"dy": 0.0,
|
||||
"adv_orig": 3.2519989013671875,
|
||||
"adv_prev": 3.2519989013671875,
|
||||
"d_adv": 0.0,
|
||||
"kern_orig_pdf": 0.084,
|
||||
"kern_orig_thousandths": 7.0,
|
||||
"kern_prev_pdf": 0.0,
|
||||
"tm_orig": [
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
50.525,
|
||||
597.45
|
||||
],
|
||||
"tm_prev": null,
|
||||
"pos_differs": true,
|
||||
"adv_differs": false,
|
||||
"note": "space skipped by runPerChar emit (x advanced only)"
|
||||
},
|
||||
{
|
||||
"i": 13,
|
||||
"char": "E",
|
||||
"gid_orig": 40,
|
||||
"gid_prev": 40,
|
||||
"emitted": true,
|
||||
"x_orig": 125.74099731445312,
|
||||
"x_prev": 125.740997,
|
||||
"dx": -3.1445313197764335e-07,
|
||||
"y_orig": 597.4500122070312,
|
||||
"y_prev": 597.45001,
|
||||
"dy": -2.207031229772838e-06,
|
||||
"adv_orig": 8.003997802734375,
|
||||
"adv_prev": 8.003997802734375,
|
||||
"d_adv": 0.0,
|
||||
"kern_orig_pdf": 0.084,
|
||||
"kern_orig_thousandths": 7.0,
|
||||
"kern_prev_pdf": 0.0,
|
||||
"tm_orig": [
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
50.525,
|
||||
597.45
|
||||
],
|
||||
"tm_prev": [
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
125.740997,
|
||||
597.45001
|
||||
],
|
||||
"pos_differs": false,
|
||||
"adv_differs": false,
|
||||
"note": ""
|
||||
},
|
||||
{
|
||||
"i": 14,
|
||||
"char": "x",
|
||||
"gid_orig": 91,
|
||||
"gid_prev": 91,
|
||||
"emitted": true,
|
||||
"x_orig": 133.7449951171875,
|
||||
"x_prev": 133.744995,
|
||||
"dx": -1.1718751125044946e-07,
|
||||
"y_orig": 597.4500122070312,
|
||||
"y_prev": 597.45001,
|
||||
"dy": -2.207031229772838e-06,
|
||||
"adv_orig": 6.73199462890625,
|
||||
"adv_prev": 6.73199462890625,
|
||||
"d_adv": 0.0,
|
||||
"kern_orig_pdf": 0.0,
|
||||
"kern_orig_thousandths": 0.0,
|
||||
"kern_prev_pdf": 0.0,
|
||||
"tm_orig": [
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
50.525,
|
||||
597.45
|
||||
],
|
||||
"tm_prev": [
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
133.744995,
|
||||
597.45001
|
||||
],
|
||||
"pos_differs": false,
|
||||
"adv_differs": false,
|
||||
"note": ""
|
||||
},
|
||||
{
|
||||
"i": 15,
|
||||
"char": "p",
|
||||
"gid_orig": 83,
|
||||
"gid_prev": 83,
|
||||
"emitted": true,
|
||||
"x_orig": 140.47698974609375,
|
||||
"x_prev": 140.47699,
|
||||
"dx": 2.5390625069121597e-07,
|
||||
"y_orig": 597.4500122070312,
|
||||
"y_prev": 597.45001,
|
||||
"dy": -2.207031229772838e-06,
|
||||
"adv_orig": 7.2480010986328125,
|
||||
"adv_prev": 7.2480010986328125,
|
||||
"d_adv": 0.0,
|
||||
"kern_orig_pdf": -0.06,
|
||||
"kern_orig_thousandths": -5.0,
|
||||
"kern_prev_pdf": 0.0,
|
||||
"tm_orig": [
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
50.525,
|
||||
597.45
|
||||
],
|
||||
"tm_prev": [
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
140.47699,
|
||||
597.45001
|
||||
],
|
||||
"pos_differs": false,
|
||||
"adv_differs": false,
|
||||
"note": ""
|
||||
},
|
||||
{
|
||||
"i": 16,
|
||||
"char": "e",
|
||||
"gid_orig": 72,
|
||||
"gid_prev": 72,
|
||||
"emitted": true,
|
||||
"x_orig": 147.72499084472656,
|
||||
"x_prev": 147.724991,
|
||||
"dx": 1.552734261167643e-07,
|
||||
"y_orig": 597.4500122070312,
|
||||
"y_prev": 597.45001,
|
||||
"dy": -2.207031229772838e-06,
|
||||
"adv_orig": 6.7440032958984375,
|
||||
"adv_prev": 6.7440032958984375,
|
||||
"d_adv": 0.0,
|
||||
"kern_orig_pdf": 0.084,
|
||||
"kern_orig_thousandths": 7.0,
|
||||
"kern_prev_pdf": 0.0,
|
||||
"tm_orig": [
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
50.525,
|
||||
597.45
|
||||
],
|
||||
"tm_prev": [
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
147.724991,
|
||||
597.45001
|
||||
],
|
||||
"pos_differs": false,
|
||||
"adv_differs": false,
|
||||
"note": ""
|
||||
},
|
||||
{
|
||||
"i": 17,
|
||||
"char": "r",
|
||||
"gid_orig": 85,
|
||||
"gid_prev": 85,
|
||||
"emitted": true,
|
||||
"x_orig": 154.468994140625,
|
||||
"x_prev": 154.468994,
|
||||
"dx": -1.406249907631718e-07,
|
||||
"y_orig": 597.4500122070312,
|
||||
"y_prev": 597.45001,
|
||||
"dy": -2.207031229772838e-06,
|
||||
"adv_orig": 4.739990234375,
|
||||
"adv_prev": 4.739990234375,
|
||||
"d_adv": 0.0,
|
||||
"kern_orig_pdf": -0.07200000000000001,
|
||||
"kern_orig_thousandths": -6.0,
|
||||
"kern_prev_pdf": 0.0,
|
||||
"tm_orig": [
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
50.525,
|
||||
597.45
|
||||
],
|
||||
"tm_prev": [
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
154.468994,
|
||||
597.45001
|
||||
],
|
||||
"pos_differs": false,
|
||||
"adv_differs": false,
|
||||
"note": ""
|
||||
},
|
||||
{
|
||||
"i": 18,
|
||||
"char": "i",
|
||||
"gid_orig": 76,
|
||||
"gid_prev": 76,
|
||||
"emitted": true,
|
||||
"x_orig": 159.208984375,
|
||||
"x_prev": 159.208984,
|
||||
"dx": -3.7500001326407073e-07,
|
||||
"y_orig": 597.4500122070312,
|
||||
"y_prev": 597.45001,
|
||||
"dy": -2.207031229772838e-06,
|
||||
"adv_orig": 3.2519989013671875,
|
||||
"adv_prev": 3.2519989013671875,
|
||||
"d_adv": 0.0,
|
||||
"kern_orig_pdf": -0.07200000000000001,
|
||||
"kern_orig_thousandths": -6.0,
|
||||
"kern_prev_pdf": 0.0,
|
||||
"tm_orig": [
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
50.525,
|
||||
597.45
|
||||
],
|
||||
"tm_prev": [
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
159.208984,
|
||||
597.45001
|
||||
],
|
||||
"pos_differs": false,
|
||||
"adv_differs": false,
|
||||
"note": ""
|
||||
},
|
||||
{
|
||||
"i": 19,
|
||||
"char": "e",
|
||||
"gid_orig": 72,
|
||||
"gid_prev": 72,
|
||||
"emitted": true,
|
||||
"x_orig": 162.4609832763672,
|
||||
"x_prev": 162.460983,
|
||||
"dx": -2.7636718868961907e-07,
|
||||
"y_orig": 597.4500122070312,
|
||||
"y_prev": 597.45001,
|
||||
"dy": -2.207031229772838e-06,
|
||||
"adv_orig": 6.7440032958984375,
|
||||
"adv_prev": 6.7440032958984375,
|
||||
"d_adv": 0.0,
|
||||
"kern_orig_pdf": 0.084,
|
||||
"kern_orig_thousandths": 7.0,
|
||||
"kern_prev_pdf": 0.0,
|
||||
"tm_orig": [
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
50.525,
|
||||
597.45
|
||||
],
|
||||
"tm_prev": [
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
162.460983,
|
||||
597.45001
|
||||
],
|
||||
"pos_differs": false,
|
||||
"adv_differs": false,
|
||||
"note": ""
|
||||
},
|
||||
{
|
||||
"i": 20,
|
||||
"char": "n",
|
||||
"gid_orig": 81,
|
||||
"gid_prev": 81,
|
||||
"emitted": true,
|
||||
"x_orig": 169.20498657226562,
|
||||
"x_prev": 169.20499,
|
||||
"dx": 3.4277343843314156e-06,
|
||||
"y_orig": 597.4500122070312,
|
||||
"y_prev": 597.45001,
|
||||
"dy": -2.207031229772838e-06,
|
||||
"adv_orig": 7.24798583984375,
|
||||
"adv_prev": 7.24798583984375,
|
||||
"d_adv": 0.0,
|
||||
"kern_orig_pdf": -0.07200000000000001,
|
||||
"kern_orig_thousandths": -6.0,
|
||||
"kern_prev_pdf": 0.0,
|
||||
"tm_orig": [
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
50.525,
|
||||
597.45
|
||||
],
|
||||
"tm_prev": [
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
169.20499,
|
||||
597.45001
|
||||
],
|
||||
"pos_differs": false,
|
||||
"adv_differs": false,
|
||||
"note": ""
|
||||
},
|
||||
{
|
||||
"i": 21,
|
||||
"char": "c",
|
||||
"gid_orig": 70,
|
||||
"gid_prev": 70,
|
||||
"emitted": true,
|
||||
"x_orig": 176.45297241210938,
|
||||
"x_prev": 176.45297,
|
||||
"dx": -2.4121093815665517e-06,
|
||||
"y_orig": 597.4500122070312,
|
||||
"y_prev": 597.45001,
|
||||
"dy": -2.207031229772838e-06,
|
||||
"adv_orig": 6.7440185546875,
|
||||
"adv_prev": 6.7440185546875,
|
||||
"d_adv": 0.0,
|
||||
"kern_orig_pdf": 0.084,
|
||||
"kern_orig_thousandths": 7.0,
|
||||
"kern_prev_pdf": 0.0,
|
||||
"tm_orig": [
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
50.525,
|
||||
597.45
|
||||
],
|
||||
"tm_prev": [
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
176.45297,
|
||||
597.45001
|
||||
],
|
||||
"pos_differs": false,
|
||||
"adv_differs": false,
|
||||
"note": ""
|
||||
},
|
||||
{
|
||||
"i": 22,
|
||||
"char": "e",
|
||||
"gid_orig": 72,
|
||||
"gid_prev": 72,
|
||||
"emitted": true,
|
||||
"x_orig": 183.19699096679688,
|
||||
"x_prev": 183.19699,
|
||||
"dx": -9.667968754456524e-07,
|
||||
"y_orig": 597.4500122070312,
|
||||
"y_prev": 597.45001,
|
||||
"dy": -2.207031229772838e-06,
|
||||
"adv_orig": 5.843994140625,
|
||||
"adv_prev": 5.843994140625,
|
||||
"d_adv": 0.0,
|
||||
"kern_orig_pdf": -0.07200000000000001,
|
||||
"kern_orig_thousandths": -6.0,
|
||||
"kern_prev_pdf": 0.0,
|
||||
"tm_orig": [
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
50.525,
|
||||
597.45
|
||||
],
|
||||
"tm_prev": [
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
183.19699,
|
||||
597.45001
|
||||
],
|
||||
"pos_differs": false,
|
||||
"adv_differs": false,
|
||||
"note": ""
|
||||
}
|
||||
],
|
||||
"u0000_audit": {
|
||||
"run_texts": [
|
||||
"Professional Experience"
|
||||
],
|
||||
"codepoints_per_run": [
|
||||
{
|
||||
"text": "Professional Experience",
|
||||
"cps": [
|
||||
"U+0050",
|
||||
"U+0072",
|
||||
"U+006F",
|
||||
"U+0066",
|
||||
"U+0065",
|
||||
"U+0073",
|
||||
"U+0073",
|
||||
"U+0069",
|
||||
"U+006F",
|
||||
"U+006E",
|
||||
"U+0061",
|
||||
"U+006C",
|
||||
"U+0020",
|
||||
"U+0045",
|
||||
"U+0078",
|
||||
"U+0070",
|
||||
"U+0065",
|
||||
"U+0072",
|
||||
"U+0069",
|
||||
"U+0065",
|
||||
"U+006E",
|
||||
"U+0063",
|
||||
"U+0065",
|
||||
"U+0000"
|
||||
]
|
||||
}
|
||||
],
|
||||
"u0000_injected_by": "utf8_to_utf16le() always push_back(0); toCodepoints iterates full vector including NUL",
|
||||
"engine_hasGlyph_rule": "FT_Get_Char_Index(face, cp) != 0",
|
||||
"embedded_unicode_cmap_has_u0000": false,
|
||||
"embedded_u0000_gid": 0,
|
||||
"embedded_missing_real_chars": [],
|
||||
"coverage_ok_if_u0000_ignored": true,
|
||||
"runtime_log": "FONT_COVERAGE_DEBUG full font MISSING codepoint U+0000 only"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 657 B |
|
After Width: | Height: | Size: 657 B |
@@ -0,0 +1,439 @@
|
||||
[2026-07-30 17:35:01.963] [info] Vertical writing mode detected for font 'BCDJEE+ArialMT' via character position analysis
|
||||
[2026-07-30 17:35:01.983] [info] [STAGE_3_WASM_INPUT] fontSize=12.00, trueVerticalSize=12.00, exactNominal=12.00, exactScaleY=1.0000
|
||||
[2026-07-30 17:35:01.983] [info] [FONT_METRICS_DEBUG] exactNominal=12.00, exactScaleX=1.00, exactScaleY=1.00, trueVerticalSize=12.00, textAspect=1.00
|
||||
[2026-07-30 17:35:01.996] [info] Vertical writing mode detected for font 'BCDJEE+ArialMT' via character position analysis
|
||||
[2026-07-30 17:35:02.010] [info] [FORENSIC_RUNPERCHAR] run=0 textLen=23 advLen=23 seedLen=0 diverges=true forceWhole=false runPerChar=1 natural0=8.0040 client0=8.0040 measureFace=true
|
||||
[2026-07-30 17:35:02.010] [info] [STAGE_4_REFLOW_OUTPUT] lineFontSize=12.00, emitFontSize=12.00, mtxScaleY=1.0000
|
||||
[2026-07-30 17:35:02.010] [info] [EMIT_FONT] text='P' baseFont='Arial-BoldMT' fontPtr=0x1741fa4b9e0 measureFacePtr=0x1741de6e630 hasResolved=true fontSize=12.0000 atX=50.5250 baselineY=597.4500 mtxScaleX=1.0000 mtxScaleY=1.0000 runPerChar=1 internalFontId='Arial-BoldMT_TrueType_32'
|
||||
[2026-07-30 17:35:02.011] [info] [EDITED_TEXT_OBJECT_DEBUG] text='P' objBaseFont='Arial-BoldMT' fontSize=12.00, matrix=[1.0000, 0.0000, 0.0000, 1.0000, 50.5250, 597.4500], bbox=[51.40, 597.45, 57.98, 606.04]
|
||||
[2026-07-30 17:35:02.011] [info] [EMIT_FONT] text='r' baseFont='Arial-BoldMT' fontPtr=0x1741fa4b9e0 measureFacePtr=0x1741de6e630 hasResolved=true fontSize=12.0000 atX=58.5290 baselineY=597.4500 mtxScaleX=1.0000 mtxScaleY=1.0000 runPerChar=1 internalFontId='Arial-BoldMT_TrueType_32'
|
||||
[2026-07-30 17:35:02.011] [info] [EDITED_TEXT_OBJECT_DEBUG] text='r' objBaseFont='Arial-BoldMT' fontSize=12.00, matrix=[1.0000, 0.0000, 0.0000, 1.0000, 58.5290, 597.4500], bbox=[59.32, 597.45, 63.35, 603.81]
|
||||
[2026-07-30 17:35:02.011] [info] [EMIT_FONT] text='o' baseFont='Arial-BoldMT' fontPtr=0x1741fa4b9e0 measureFacePtr=0x1741de6e630 hasResolved=true fontSize=12.0000 atX=63.2690 baselineY=597.4500 mtxScaleX=1.0000 mtxScaleY=1.0000 runPerChar=1 internalFontId='Arial-BoldMT_TrueType_32'
|
||||
[2026-07-30 17:35:02.011] [info] [EDITED_TEXT_OBJECT_DEBUG] text='o' objBaseFont='Arial-BoldMT' fontSize=12.00, matrix=[1.0000, 0.0000, 0.0000, 1.0000, 63.2690, 597.4500], bbox=[63.75, 597.32, 70.17, 603.81]
|
||||
[2026-07-30 17:35:02.011] [info] [EMIT_FONT] text='f' baseFont='Arial-BoldMT' fontPtr=0x1741fa4b9e0 measureFacePtr=0x1741de6e630 hasResolved=true fontSize=12.0000 atX=70.5170 baselineY=597.4500 mtxScaleX=1.0000 mtxScaleY=1.0000 runPerChar=1 internalFontId='Arial-BoldMT_TrueType_32'
|
||||
[2026-07-30 17:35:02.011] [info] [EDITED_TEXT_OBJECT_DEBUG] text='f' objBaseFont='Arial-BoldMT' fontSize=12.00, matrix=[1.0000, 0.0000, 0.0000, 1.0000, 70.5170, 597.4500], bbox=[70.66, 597.45, 74.86, 606.19]
|
||||
[2026-07-30 17:35:02.011] [info] [EMIT_FONT] text='e' baseFont='Arial-BoldMT' fontPtr=0x1741fa4b9e0 measureFacePtr=0x1741de6e630 hasResolved=true fontSize=12.0000 atX=74.5130 baselineY=597.4500 mtxScaleX=1.0000 mtxScaleY=1.0000 runPerChar=1 internalFontId='Arial-BoldMT_TrueType_32'
|
||||
[2026-07-30 17:35:02.011] [info] [EDITED_TEXT_OBJECT_DEBUG] text='e' objBaseFont='Arial-BoldMT' fontSize=12.00, matrix=[1.0000, 0.0000, 0.0000, 1.0000, 74.5130, 597.4500], bbox=[74.90, 597.32, 80.74, 603.81]
|
||||
[2026-07-30 17:35:02.011] [info] [EMIT_FONT] text='s' baseFont='Arial-BoldMT' fontPtr=0x1741fa4b9e0 measureFacePtr=0x1741de6e630 hasResolved=true fontSize=12.0000 atX=81.2570 baselineY=597.4500 mtxScaleX=1.0000 mtxScaleY=1.0000 runPerChar=1 internalFontId='Arial-BoldMT_TrueType_32'
|
||||
[2026-07-30 17:35:02.011] [info] [EDITED_TEXT_OBJECT_DEBUG] text='s' objBaseFont='Arial-BoldMT' fontSize=12.00, matrix=[1.0000, 0.0000, 0.0000, 1.0000, 81.2570, 597.4500], bbox=[81.53, 597.32, 87.35, 603.81]
|
||||
[2026-07-30 17:35:02.011] [info] [EMIT_FONT] text='s' baseFont='Arial-BoldMT' fontPtr=0x1741fa4b9e0 measureFacePtr=0x1741de6e630 hasResolved=true fontSize=12.0000 atX=88.0010 baselineY=597.4500 mtxScaleX=1.0000 mtxScaleY=1.0000 runPerChar=1 internalFontId='Arial-BoldMT_TrueType_32'
|
||||
[2026-07-30 17:35:02.011] [info] [EDITED_TEXT_OBJECT_DEBUG] text='s' objBaseFont='Arial-BoldMT' fontSize=12.00, matrix=[1.0000, 0.0000, 0.0000, 1.0000, 88.0010, 597.4500], bbox=[88.28, 597.32, 94.10, 603.81]
|
||||
[2026-07-30 17:35:02.011] [info] [EMIT_FONT] text='i' baseFont='Arial-BoldMT' fontPtr=0x1741fa4b9e0 measureFacePtr=0x1741de6e630 hasResolved=true fontSize=12.0000 atX=94.7450 baselineY=597.4500 mtxScaleX=1.0000 mtxScaleY=1.0000 runPerChar=1 internalFontId='Arial-BoldMT_TrueType_32'
|
||||
[2026-07-30 17:35:02.011] [info] [EDITED_TEXT_OBJECT_DEBUG] text='i' objBaseFont='Arial-BoldMT' fontSize=12.00, matrix=[1.0000, 0.0000, 0.0000, 1.0000, 94.7450, 597.4500], bbox=[95.61, 597.45, 97.25, 606.04]
|
||||
[2026-07-30 17:35:02.011] [info] [EMIT_FONT] text='o' baseFont='Arial-BoldMT' fontPtr=0x1741fa4b9e0 measureFacePtr=0x1741de6e630 hasResolved=true fontSize=12.0000 atX=97.9970 baselineY=597.4500 mtxScaleX=1.0000 mtxScaleY=1.0000 runPerChar=1 internalFontId='Arial-BoldMT_TrueType_32'
|
||||
[2026-07-30 17:35:02.011] [info] [EDITED_TEXT_OBJECT_DEBUG] text='o' objBaseFont='Arial-BoldMT' fontSize=12.00, matrix=[1.0000, 0.0000, 0.0000, 1.0000, 97.9970, 597.4500], bbox=[98.48, 597.32, 104.90, 603.81]
|
||||
[2026-07-30 17:35:02.011] [info] [EMIT_FONT] text='n' baseFont='Arial-BoldMT' fontPtr=0x1741fa4b9e0 measureFacePtr=0x1741de6e630 hasResolved=true fontSize=12.0000 atX=105.2450 baselineY=597.4500 mtxScaleX=1.0000 mtxScaleY=1.0000 runPerChar=1 internalFontId='Arial-BoldMT_TrueType_32'
|
||||
[2026-07-30 17:35:02.011] [info] [EDITED_TEXT_OBJECT_DEBUG] text='n' objBaseFont='Arial-BoldMT' fontSize=12.00, matrix=[1.0000, 0.0000, 0.0000, 1.0000, 105.2450, 597.4500], bbox=[106.10, 597.45, 111.76, 603.81]
|
||||
[2026-07-30 17:35:02.011] [info] [EMIT_FONT] text='a' baseFont='Arial-BoldMT' fontPtr=0x1741fa4b9e0 measureFacePtr=0x1741de6e630 hasResolved=true fontSize=12.0000 atX=112.4930 baselineY=597.4500 mtxScaleX=1.0000 mtxScaleY=1.0000 runPerChar=1 internalFontId='Arial-BoldMT_TrueType_32'
|
||||
[2026-07-30 17:35:02.011] [info] [EDITED_TEXT_OBJECT_DEBUG] text='a' objBaseFont='Arial-BoldMT' fontSize=12.00, matrix=[1.0000, 0.0000, 0.0000, 1.0000, 112.4930, 597.4500], bbox=[112.92, 597.32, 118.76, 603.81]
|
||||
[2026-07-30 17:35:02.011] [info] [EMIT_FONT] text='l' baseFont='Arial-BoldMT' fontPtr=0x1741fa4b9e0 measureFacePtr=0x1741de6e630 hasResolved=true fontSize=12.0000 atX=119.2370 baselineY=597.4500 mtxScaleX=1.0000 mtxScaleY=1.0000 runPerChar=1 internalFontId='Arial-BoldMT_TrueType_32'
|
||||
[2026-07-30 17:35:02.011] [info] [EDITED_TEXT_OBJECT_DEBUG] text='l' objBaseFont='Arial-BoldMT' fontSize=12.00, matrix=[1.0000, 0.0000, 0.0000, 1.0000, 119.2370, 597.4500], bbox=[120.10, 597.45, 121.75, 606.04]
|
||||
[2026-07-30 17:35:02.011] [info] [STAGE_4_REFLOW_OUTPUT] lineFontSize=12.00, emitFontSize=12.00, mtxScaleY=1.0000
|
||||
[2026-07-30 17:35:02.011] [info] [EMIT_FONT] text='E' baseFont='Arial-BoldMT' fontPtr=0x1741fa4b9e0 measureFacePtr=0x1741de6e630 hasResolved=true fontSize=12.0000 atX=125.7410 baselineY=597.4500 mtxScaleX=1.0000 mtxScaleY=1.0000 runPerChar=1 internalFontId='Arial-BoldMT_TrueType_32'
|
||||
[2026-07-30 17:35:02.011] [info] [EDITED_TEXT_OBJECT_DEBUG] text='E' objBaseFont='Arial-BoldMT' fontSize=12.00, matrix=[1.0000, 0.0000, 0.0000, 1.0000, 125.7410, 597.4500], bbox=[126.62, 597.45, 133.15, 606.04]
|
||||
[2026-07-30 17:35:02.011] [info] [EMIT_FONT] text='x' baseFont='Arial-BoldMT' fontPtr=0x1741fa4b9e0 measureFacePtr=0x1741de6e630 hasResolved=true fontSize=12.0000 atX=133.7450 baselineY=597.4500 mtxScaleX=1.0000 mtxScaleY=1.0000 runPerChar=1 internalFontId='Arial-BoldMT_TrueType_32'
|
||||
[2026-07-30 17:35:02.011] [info] [EDITED_TEXT_OBJECT_DEBUG] text='x' objBaseFont='Arial-BoldMT' fontSize=12.00, matrix=[1.0000, 0.0000, 0.0000, 1.0000, 133.7450, 597.4500], bbox=[133.82, 597.45, 140.31, 603.68]
|
||||
[2026-07-30 17:35:02.011] [info] [EMIT_FONT] text='p' baseFont='Arial-BoldMT' fontPtr=0x1741fa4b9e0 measureFacePtr=0x1741de6e630 hasResolved=true fontSize=12.0000 atX=140.4770 baselineY=597.4500 mtxScaleX=1.0000 mtxScaleY=1.0000 runPerChar=1 internalFontId='Arial-BoldMT_TrueType_32'
|
||||
[2026-07-30 17:35:02.011] [info] [EDITED_TEXT_OBJECT_DEBUG] text='p' objBaseFont='Arial-BoldMT' fontSize=12.00, matrix=[1.0000, 0.0000, 0.0000, 1.0000, 140.4770, 597.4500], bbox=[141.29, 595.10, 147.36, 603.81]
|
||||
[2026-07-30 17:35:02.011] [info] [EMIT_FONT] text='e' baseFont='Arial-BoldMT' fontPtr=0x1741fa4b9e0 measureFacePtr=0x1741de6e630 hasResolved=true fontSize=12.0000 atX=147.7250 baselineY=597.4500 mtxScaleX=1.0000 mtxScaleY=1.0000 runPerChar=1 internalFontId='Arial-BoldMT_TrueType_32'
|
||||
[2026-07-30 17:35:02.011] [info] [EDITED_TEXT_OBJECT_DEBUG] text='e' objBaseFont='Arial-BoldMT' fontSize=12.00, matrix=[1.0000, 0.0000, 0.0000, 1.0000, 147.7250, 597.4500], bbox=[148.11, 597.32, 153.95, 603.81]
|
||||
[2026-07-30 17:35:02.011] [info] [EMIT_FONT] text='r' baseFont='Arial-BoldMT' fontPtr=0x1741fa4b9e0 measureFacePtr=0x1741de6e630 hasResolved=true fontSize=12.0000 atX=154.4690 baselineY=597.4500 mtxScaleX=1.0000 mtxScaleY=1.0000 runPerChar=1 internalFontId='Arial-BoldMT_TrueType_32'
|
||||
[2026-07-30 17:35:02.011] [info] [EDITED_TEXT_OBJECT_DEBUG] text='r' objBaseFont='Arial-BoldMT' fontSize=12.00, matrix=[1.0000, 0.0000, 0.0000, 1.0000, 154.4690, 597.4500], bbox=[155.26, 597.45, 159.29, 603.81]
|
||||
[2026-07-30 17:35:02.011] [info] [EMIT_FONT] text='i' baseFont='Arial-BoldMT' fontPtr=0x1741fa4b9e0 measureFacePtr=0x1741de6e630 hasResolved=true fontSize=12.0000 atX=159.2090 baselineY=597.4500 mtxScaleX=1.0000 mtxScaleY=1.0000 runPerChar=1 internalFontId='Arial-BoldMT_TrueType_32'
|
||||
[2026-07-30 17:35:02.011] [info] [EDITED_TEXT_OBJECT_DEBUG] text='i' objBaseFont='Arial-BoldMT' fontSize=12.00, matrix=[1.0000, 0.0000, 0.0000, 1.0000, 159.2090, 597.4500], bbox=[160.07, 597.45, 161.72, 606.04]
|
||||
[2026-07-30 17:35:02.011] [info] [EMIT_FONT] text='e' baseFont='Arial-BoldMT' fontPtr=0x1741fa4b9e0 measureFacePtr=0x1741de6e630 hasResolved=true fontSize=12.0000 atX=162.4610 baselineY=597.4500 mtxScaleX=1.0000 mtxScaleY=1.0000 runPerChar=1 internalFontId='Arial-BoldMT_TrueType_32'
|
||||
[2026-07-30 17:35:02.011] [info] [EDITED_TEXT_OBJECT_DEBUG] text='e' objBaseFont='Arial-BoldMT' fontSize=12.00, matrix=[1.0000, 0.0000, 0.0000, 1.0000, 162.4610, 597.4500], bbox=[162.84, 597.32, 168.69, 603.81]
|
||||
[2026-07-30 17:35:02.011] [info] [EMIT_FONT] text='n' baseFont='Arial-BoldMT' fontPtr=0x1741fa4b9e0 measureFacePtr=0x1741de6e630 hasResolved=true fontSize=12.0000 atX=169.2050 baselineY=597.4500 mtxScaleX=1.0000 mtxScaleY=1.0000 runPerChar=1 internalFontId='Arial-BoldMT_TrueType_32'
|
||||
[2026-07-30 17:35:02.011] [info] [EDITED_TEXT_OBJECT_DEBUG] text='n' objBaseFont='Arial-BoldMT' fontSize=12.00, matrix=[1.0000, 0.0000, 0.0000, 1.0000, 169.2050, 597.4500], bbox=[170.06, 597.45, 175.72, 603.81]
|
||||
[2026-07-30 17:35:02.011] [info] [EMIT_FONT] text='c' baseFont='Arial-BoldMT' fontPtr=0x1741fa4b9e0 measureFacePtr=0x1741de6e630 hasResolved=true fontSize=12.0000 atX=176.4530 baselineY=597.4500 mtxScaleX=1.0000 mtxScaleY=1.0000 runPerChar=1 internalFontId='Arial-BoldMT_TrueType_32'
|
||||
[2026-07-30 17:35:02.011] [info] [EDITED_TEXT_OBJECT_DEBUG] text='c' objBaseFont='Arial-BoldMT' fontSize=12.00, matrix=[1.0000, 0.0000, 0.0000, 1.0000, 176.4530, 597.4500], bbox=[176.96, 597.32, 182.82, 603.81]
|
||||
[2026-07-30 17:35:02.011] [info] [EMIT_FONT] text='e' baseFont='Arial-BoldMT' fontPtr=0x1741fa4b9e0 measureFacePtr=0x1741de6e630 hasResolved=true fontSize=12.0000 atX=183.1970 baselineY=597.4500 mtxScaleX=1.0000 mtxScaleY=1.0000 runPerChar=1 internalFontId='Arial-BoldMT_TrueType_32'
|
||||
[2026-07-30 17:35:02.011] [info] [EDITED_TEXT_OBJECT_DEBUG] text='e' objBaseFont='Arial-BoldMT' fontSize=12.00, matrix=[1.0000, 0.0000, 0.0000, 1.0000, 183.1970, 597.4500], bbox=[183.58, 597.32, 189.42, 603.81]
|
||||
[2026-07-30 17:35:02.011] [info] [STAGE_5_SERIALIZED_JSON] {"anchorPage":0,"columnLeft":51.4010009765625,"lines":[{"adv":[8.003997802734375,4.740001678466797,7.247997283935547,3.996002197265625,6.7440032958984375,6.743995666503906,6.743995666503906,3.2519989013671875,7.2480010986328125,7.2480010986328125,6.7440032958984375,3.2519989013671875,3.2519989013671875,8.003997802734375,6.73199462890625,7.2480010986328125,6.7440032958984375,4.739990234375,3.2519989013671875,6.7440032958984375,7.24798583984375,6.7440185546875,5.843994140625],"baselineY":597.4500122070313,"fontSize":12.0,"pageIndex":0,"text":"Professional Experience","x0":50.525001525878906}]}
|
||||
[2026-07-30 17:35:02.042] [info] Document caches have been invalidated.
|
||||
[2026-07-30 17:35:02.063] [info] [STAGE_3_WASM_INPUT] fontSize=12.00, trueVerticalSize=12.00, exactNominal=12.00, exactScaleY=1.0000
|
||||
[2026-07-30 17:35:02.063] [info] [FONT_METRICS_DEBUG] exactNominal=12.00, exactScaleX=1.00, exactScaleY=1.00, trueVerticalSize=12.00, textAspect=1.00
|
||||
[2026-07-30 17:35:02.071] [info] Vertical writing mode detected for font 'BCDJEE+ArialMT' via character position analysis
|
||||
[2026-07-30 17:35:02.087] [info] [ADVANCE_SEED_MERGE] run=0 seedLen=23 textLen=24 prefixKept=23 (LCP/LCS merge; middle from measureFace)
|
||||
[2026-07-30 17:35:02.087] [info] [FORENSIC_RUNPERCHAR] run=0 textLen=24 advLen=23 seedLen=23 diverges=true forceWhole=false runPerChar=1 natural0=8.0040 client0=8.0040 measureFace=true
|
||||
[2026-07-30 17:35:02.087] [info] [KEYSTROKE_BACKEND_DEBUG] columnLeft=51.40, columnRight=301.40, columnWidth=250.00, firstBaselineY=597.45, oldLineCount=1, hangingIndent=0.00
|
||||
[2026-07-30 17:35:02.087] [info] [STAGE_4_REFLOW_OUTPUT] lineFontSize=12.00, emitFontSize=12.00, mtxScaleY=1.0000
|
||||
[2026-07-30 17:35:02.087] [info] [EMIT_FONT] text='P' baseFont='Arial-BoldMT' fontPtr=0x1741fb1cb40 measureFacePtr=0x1741de6c050 hasResolved=true fontSize=12.0000 atX=50.5250 baselineY=597.4500 mtxScaleX=1.0000 mtxScaleY=1.0000 runPerChar=1 internalFontId='Arial-BoldMT_TrueType_32'
|
||||
[2026-07-30 17:35:02.087] [info] [EDITED_TEXT_OBJECT_DEBUG] text='P' objBaseFont='Arial-BoldMT' fontSize=12.00, matrix=[1.0000, 0.0000, 0.0000, 1.0000, 50.5250, 597.4500], bbox=[51.40, 597.45, 57.98, 606.04]
|
||||
[2026-07-30 17:35:02.087] [info] [EMIT_FONT] text='r' baseFont='Arial-BoldMT' fontPtr=0x1741fb1cb40 measureFacePtr=0x1741de6c050 hasResolved=true fontSize=12.0000 atX=58.5290 baselineY=597.4500 mtxScaleX=1.0000 mtxScaleY=1.0000 runPerChar=1 internalFontId='Arial-BoldMT_TrueType_32'
|
||||
[2026-07-30 17:35:02.087] [info] [EDITED_TEXT_OBJECT_DEBUG] text='r' objBaseFont='Arial-BoldMT' fontSize=12.00, matrix=[1.0000, 0.0000, 0.0000, 1.0000, 58.5290, 597.4500], bbox=[59.32, 597.45, 63.35, 603.81]
|
||||
[2026-07-30 17:35:02.087] [info] [EMIT_FONT] text='o' baseFont='Arial-BoldMT' fontPtr=0x1741fb1cb40 measureFacePtr=0x1741de6c050 hasResolved=true fontSize=12.0000 atX=63.2690 baselineY=597.4500 mtxScaleX=1.0000 mtxScaleY=1.0000 runPerChar=1 internalFontId='Arial-BoldMT_TrueType_32'
|
||||
[2026-07-30 17:35:02.087] [info] [EDITED_TEXT_OBJECT_DEBUG] text='o' objBaseFont='Arial-BoldMT' fontSize=12.00, matrix=[1.0000, 0.0000, 0.0000, 1.0000, 63.2690, 597.4500], bbox=[63.75, 597.32, 70.17, 603.81]
|
||||
[2026-07-30 17:35:02.087] [info] [EMIT_FONT] text='f' baseFont='Arial-BoldMT' fontPtr=0x1741fb1cb40 measureFacePtr=0x1741de6c050 hasResolved=true fontSize=12.0000 atX=70.5170 baselineY=597.4500 mtxScaleX=1.0000 mtxScaleY=1.0000 runPerChar=1 internalFontId='Arial-BoldMT_TrueType_32'
|
||||
[2026-07-30 17:35:02.087] [info] [EDITED_TEXT_OBJECT_DEBUG] text='f' objBaseFont='Arial-BoldMT' fontSize=12.00, matrix=[1.0000, 0.0000, 0.0000, 1.0000, 70.5170, 597.4500], bbox=[70.66, 597.45, 74.86, 606.19]
|
||||
[2026-07-30 17:35:02.087] [info] [EMIT_FONT] text='e' baseFont='Arial-BoldMT' fontPtr=0x1741fb1cb40 measureFacePtr=0x1741de6c050 hasResolved=true fontSize=12.0000 atX=74.5130 baselineY=597.4500 mtxScaleX=1.0000 mtxScaleY=1.0000 runPerChar=1 internalFontId='Arial-BoldMT_TrueType_32'
|
||||
[2026-07-30 17:35:02.087] [info] [EDITED_TEXT_OBJECT_DEBUG] text='e' objBaseFont='Arial-BoldMT' fontSize=12.00, matrix=[1.0000, 0.0000, 0.0000, 1.0000, 74.5130, 597.4500], bbox=[74.90, 597.32, 80.74, 603.81]
|
||||
[2026-07-30 17:35:02.087] [info] [EMIT_FONT] text='s' baseFont='Arial-BoldMT' fontPtr=0x1741fb1cb40 measureFacePtr=0x1741de6c050 hasResolved=true fontSize=12.0000 atX=81.2570 baselineY=597.4500 mtxScaleX=1.0000 mtxScaleY=1.0000 runPerChar=1 internalFontId='Arial-BoldMT_TrueType_32'
|
||||
[2026-07-30 17:35:02.087] [info] [EDITED_TEXT_OBJECT_DEBUG] text='s' objBaseFont='Arial-BoldMT' fontSize=12.00, matrix=[1.0000, 0.0000, 0.0000, 1.0000, 81.2570, 597.4500], bbox=[81.53, 597.32, 87.35, 603.81]
|
||||
[2026-07-30 17:35:02.087] [info] [EMIT_FONT] text='s' baseFont='Arial-BoldMT' fontPtr=0x1741fb1cb40 measureFacePtr=0x1741de6c050 hasResolved=true fontSize=12.0000 atX=88.0010 baselineY=597.4500 mtxScaleX=1.0000 mtxScaleY=1.0000 runPerChar=1 internalFontId='Arial-BoldMT_TrueType_32'
|
||||
[2026-07-30 17:35:02.087] [info] [EDITED_TEXT_OBJECT_DEBUG] text='s' objBaseFont='Arial-BoldMT' fontSize=12.00, matrix=[1.0000, 0.0000, 0.0000, 1.0000, 88.0010, 597.4500], bbox=[88.28, 597.32, 94.10, 603.81]
|
||||
[2026-07-30 17:35:02.087] [info] [EMIT_FONT] text='i' baseFont='Arial-BoldMT' fontPtr=0x1741fb1cb40 measureFacePtr=0x1741de6c050 hasResolved=true fontSize=12.0000 atX=94.7450 baselineY=597.4500 mtxScaleX=1.0000 mtxScaleY=1.0000 runPerChar=1 internalFontId='Arial-BoldMT_TrueType_32'
|
||||
[2026-07-30 17:35:02.087] [info] [EDITED_TEXT_OBJECT_DEBUG] text='i' objBaseFont='Arial-BoldMT' fontSize=12.00, matrix=[1.0000, 0.0000, 0.0000, 1.0000, 94.7450, 597.4500], bbox=[95.61, 597.45, 97.25, 606.04]
|
||||
[2026-07-30 17:35:02.087] [info] [EMIT_FONT] text='o' baseFont='Arial-BoldMT' fontPtr=0x1741fb1cb40 measureFacePtr=0x1741de6c050 hasResolved=true fontSize=12.0000 atX=97.9970 baselineY=597.4500 mtxScaleX=1.0000 mtxScaleY=1.0000 runPerChar=1 internalFontId='Arial-BoldMT_TrueType_32'
|
||||
[2026-07-30 17:35:02.087] [info] [EDITED_TEXT_OBJECT_DEBUG] text='o' objBaseFont='Arial-BoldMT' fontSize=12.00, matrix=[1.0000, 0.0000, 0.0000, 1.0000, 97.9970, 597.4500], bbox=[98.48, 597.32, 104.90, 603.81]
|
||||
[2026-07-30 17:35:02.087] [info] [EMIT_FONT] text='n' baseFont='Arial-BoldMT' fontPtr=0x1741fb1cb40 measureFacePtr=0x1741de6c050 hasResolved=true fontSize=12.0000 atX=105.2450 baselineY=597.4500 mtxScaleX=1.0000 mtxScaleY=1.0000 runPerChar=1 internalFontId='Arial-BoldMT_TrueType_32'
|
||||
[2026-07-30 17:35:02.087] [info] [EDITED_TEXT_OBJECT_DEBUG] text='n' objBaseFont='Arial-BoldMT' fontSize=12.00, matrix=[1.0000, 0.0000, 0.0000, 1.0000, 105.2450, 597.4500], bbox=[106.10, 597.45, 111.76, 603.81]
|
||||
[2026-07-30 17:35:02.087] [info] [EMIT_FONT] text='a' baseFont='Arial-BoldMT' fontPtr=0x1741fb1cb40 measureFacePtr=0x1741de6c050 hasResolved=true fontSize=12.0000 atX=112.4930 baselineY=597.4500 mtxScaleX=1.0000 mtxScaleY=1.0000 runPerChar=1 internalFontId='Arial-BoldMT_TrueType_32'
|
||||
[2026-07-30 17:35:02.088] [info] [EDITED_TEXT_OBJECT_DEBUG] text='a' objBaseFont='Arial-BoldMT' fontSize=12.00, matrix=[1.0000, 0.0000, 0.0000, 1.0000, 112.4930, 597.4500], bbox=[112.92, 597.32, 118.76, 603.81]
|
||||
[2026-07-30 17:35:02.088] [info] [EMIT_FONT] text='l' baseFont='Arial-BoldMT' fontPtr=0x1741fb1cb40 measureFacePtr=0x1741de6c050 hasResolved=true fontSize=12.0000 atX=119.2370 baselineY=597.4500 mtxScaleX=1.0000 mtxScaleY=1.0000 runPerChar=1 internalFontId='Arial-BoldMT_TrueType_32'
|
||||
[2026-07-30 17:35:02.088] [info] [EDITED_TEXT_OBJECT_DEBUG] text='l' objBaseFont='Arial-BoldMT' fontSize=12.00, matrix=[1.0000, 0.0000, 0.0000, 1.0000, 119.2370, 597.4500], bbox=[120.10, 597.45, 121.75, 606.04]
|
||||
[2026-07-30 17:35:02.088] [info] [STAGE_4_REFLOW_OUTPUT] lineFontSize=12.00, emitFontSize=12.00, mtxScaleY=1.0000
|
||||
[2026-07-30 17:35:02.088] [info] [EMIT_FONT] text='E' baseFont='Arial-BoldMT' fontPtr=0x1741fb1cb40 measureFacePtr=0x1741de6c050 hasResolved=true fontSize=12.0000 atX=125.7410 baselineY=597.4500 mtxScaleX=1.0000 mtxScaleY=1.0000 runPerChar=1 internalFontId='Arial-BoldMT_TrueType_32'
|
||||
[2026-07-30 17:35:02.088] [info] [EDITED_TEXT_OBJECT_DEBUG] text='E' objBaseFont='Arial-BoldMT' fontSize=12.00, matrix=[1.0000, 0.0000, 0.0000, 1.0000, 125.7410, 597.4500], bbox=[126.62, 597.45, 133.15, 606.04]
|
||||
[2026-07-30 17:35:02.088] [info] [EMIT_FONT] text='x' baseFont='Arial-BoldMT' fontPtr=0x1741fb1cb40 measureFacePtr=0x1741de6c050 hasResolved=true fontSize=12.0000 atX=133.7450 baselineY=597.4500 mtxScaleX=1.0000 mtxScaleY=1.0000 runPerChar=1 internalFontId='Arial-BoldMT_TrueType_32'
|
||||
[2026-07-30 17:35:02.088] [info] [EDITED_TEXT_OBJECT_DEBUG] text='x' objBaseFont='Arial-BoldMT' fontSize=12.00, matrix=[1.0000, 0.0000, 0.0000, 1.0000, 133.7450, 597.4500], bbox=[133.82, 597.45, 140.31, 603.68]
|
||||
[2026-07-30 17:35:02.088] [info] [EMIT_FONT] text='p' baseFont='Arial-BoldMT' fontPtr=0x1741fb1cb40 measureFacePtr=0x1741de6c050 hasResolved=true fontSize=12.0000 atX=140.4770 baselineY=597.4500 mtxScaleX=1.0000 mtxScaleY=1.0000 runPerChar=1 internalFontId='Arial-BoldMT_TrueType_32'
|
||||
[2026-07-30 17:35:02.088] [info] [EDITED_TEXT_OBJECT_DEBUG] text='p' objBaseFont='Arial-BoldMT' fontSize=12.00, matrix=[1.0000, 0.0000, 0.0000, 1.0000, 140.4770, 597.4500], bbox=[141.29, 595.10, 147.36, 603.81]
|
||||
[2026-07-30 17:35:02.088] [info] [EMIT_FONT] text='e' baseFont='Arial-BoldMT' fontPtr=0x1741fb1cb40 measureFacePtr=0x1741de6c050 hasResolved=true fontSize=12.0000 atX=147.7250 baselineY=597.4500 mtxScaleX=1.0000 mtxScaleY=1.0000 runPerChar=1 internalFontId='Arial-BoldMT_TrueType_32'
|
||||
[2026-07-30 17:35:02.088] [info] [EDITED_TEXT_OBJECT_DEBUG] text='e' objBaseFont='Arial-BoldMT' fontSize=12.00, matrix=[1.0000, 0.0000, 0.0000, 1.0000, 147.7250, 597.4500], bbox=[148.11, 597.32, 153.95, 603.81]
|
||||
[2026-07-30 17:35:02.088] [info] [EMIT_FONT] text='r' baseFont='Arial-BoldMT' fontPtr=0x1741fb1cb40 measureFacePtr=0x1741de6c050 hasResolved=true fontSize=12.0000 atX=154.4690 baselineY=597.4500 mtxScaleX=1.0000 mtxScaleY=1.0000 runPerChar=1 internalFontId='Arial-BoldMT_TrueType_32'
|
||||
[2026-07-30 17:35:02.088] [info] [EDITED_TEXT_OBJECT_DEBUG] text='r' objBaseFont='Arial-BoldMT' fontSize=12.00, matrix=[1.0000, 0.0000, 0.0000, 1.0000, 154.4690, 597.4500], bbox=[155.26, 597.45, 159.29, 603.81]
|
||||
[2026-07-30 17:35:02.088] [info] [EMIT_FONT] text='i' baseFont='Arial-BoldMT' fontPtr=0x1741fb1cb40 measureFacePtr=0x1741de6c050 hasResolved=true fontSize=12.0000 atX=159.2090 baselineY=597.4500 mtxScaleX=1.0000 mtxScaleY=1.0000 runPerChar=1 internalFontId='Arial-BoldMT_TrueType_32'
|
||||
[2026-07-30 17:35:02.088] [info] [EDITED_TEXT_OBJECT_DEBUG] text='i' objBaseFont='Arial-BoldMT' fontSize=12.00, matrix=[1.0000, 0.0000, 0.0000, 1.0000, 159.2090, 597.4500], bbox=[160.07, 597.45, 161.72, 606.04]
|
||||
[2026-07-30 17:35:02.088] [info] [EMIT_FONT] text='e' baseFont='Arial-BoldMT' fontPtr=0x1741fb1cb40 measureFacePtr=0x1741de6c050 hasResolved=true fontSize=12.0000 atX=162.4610 baselineY=597.4500 mtxScaleX=1.0000 mtxScaleY=1.0000 runPerChar=1 internalFontId='Arial-BoldMT_TrueType_32'
|
||||
[2026-07-30 17:35:02.088] [info] [EDITED_TEXT_OBJECT_DEBUG] text='e' objBaseFont='Arial-BoldMT' fontSize=12.00, matrix=[1.0000, 0.0000, 0.0000, 1.0000, 162.4610, 597.4500], bbox=[162.84, 597.32, 168.69, 603.81]
|
||||
[2026-07-30 17:35:02.088] [info] [EMIT_FONT] text='n' baseFont='Arial-BoldMT' fontPtr=0x1741fb1cb40 measureFacePtr=0x1741de6c050 hasResolved=true fontSize=12.0000 atX=169.2050 baselineY=597.4500 mtxScaleX=1.0000 mtxScaleY=1.0000 runPerChar=1 internalFontId='Arial-BoldMT_TrueType_32'
|
||||
[2026-07-30 17:35:02.088] [info] [EDITED_TEXT_OBJECT_DEBUG] text='n' objBaseFont='Arial-BoldMT' fontSize=12.00, matrix=[1.0000, 0.0000, 0.0000, 1.0000, 169.2050, 597.4500], bbox=[170.06, 597.45, 175.72, 603.81]
|
||||
[2026-07-30 17:35:02.088] [info] [EMIT_FONT] text='c' baseFont='Arial-BoldMT' fontPtr=0x1741fb1cb40 measureFacePtr=0x1741de6c050 hasResolved=true fontSize=12.0000 atX=176.4530 baselineY=597.4500 mtxScaleX=1.0000 mtxScaleY=1.0000 runPerChar=1 internalFontId='Arial-BoldMT_TrueType_32'
|
||||
[2026-07-30 17:35:02.088] [info] [EDITED_TEXT_OBJECT_DEBUG] text='c' objBaseFont='Arial-BoldMT' fontSize=12.00, matrix=[1.0000, 0.0000, 0.0000, 1.0000, 176.4530, 597.4500], bbox=[176.96, 597.32, 182.82, 603.81]
|
||||
[2026-07-30 17:35:02.088] [info] [EMIT_FONT] text='e' baseFont='Arial-BoldMT' fontPtr=0x1741fb1cb40 measureFacePtr=0x1741de6c050 hasResolved=true fontSize=12.0000 atX=183.1970 baselineY=597.4500 mtxScaleX=1.0000 mtxScaleY=1.0000 runPerChar=1 internalFontId='Arial-BoldMT_TrueType_32'
|
||||
[2026-07-30 17:35:02.088] [info] [EDITED_TEXT_OBJECT_DEBUG] text='e' objBaseFont='Arial-BoldMT' fontSize=12.00, matrix=[1.0000, 0.0000, 0.0000, 1.0000, 183.1970, 597.4500], bbox=[183.58, 597.32, 189.42, 603.81]
|
||||
[2026-07-30 17:35:02.088] [info] [EMIT_FONT] text='x' baseFont='Arial-BoldMT' fontPtr=0x1741fb1cb40 measureFacePtr=0x1741de6c050 hasResolved=true fontSize=12.0000 atX=189.0410 baselineY=597.4500 mtxScaleX=1.0000 mtxScaleY=1.0000 runPerChar=1 internalFontId='Arial-BoldMT_TrueType_32'
|
||||
[2026-07-30 17:35:02.089] [info] [EDITED_TEXT_OBJECT_DEBUG] text='x' objBaseFont='Arial-BoldMT' fontSize=12.00, matrix=[1.0000, 0.0000, 0.0000, 1.0000, 189.0410, 597.4500], bbox=[189.11, 597.45, 195.60, 603.68]
|
||||
[2026-07-30 17:35:02.089] [info] [STAGE_5_SERIALIZED_JSON] {"anchorPage":0,"columnLeft":51.4010009765625,"lines":[{"adv":[8.003997802734375,4.740001678466797,7.247997283935547,3.996002197265625,6.7440032958984375,6.743995666503906,6.743995666503906,3.2519989013671875,7.2480010986328125,7.2480010986328125,6.7440032958984375,3.2519989013671875,3.2519989013671875,8.003997802734375,6.73199462890625,7.2480010986328125,6.7440032958984375,4.739990234375,3.2519989013671875,6.7440032958984375,7.24798583984375,6.7440185546875,5.843994140625,6.673875],"baselineY":597.4500122070313,"fontSize":12.0,"pageIndex":0,"text":"Professional Experiencex","x0":50.525001525878906}]}
|
||||
[2026-07-30 17:35:02.125] [info] Document caches have been invalidated.
|
||||
[2026-07-30 17:35:02.148] [info] Vertical writing mode detected for font 'BCDJEE+ArialMT' via character position analysis
|
||||
[2026-07-30 17:35:02.169] [info] Vertical writing mode detected for font 'BCDJEE+ArialMT' via character position analysis
|
||||
[2026-07-30 17:35:02.665] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 1. Region width: 2448, height: 46 (dpi: 288, yTopPt: 184.46, heightPt: 11.59, pagePt: 612.00x792.00)
|
||||
[2026-07-30 17:35:02.665] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 2. Initialized with opaque white fill (0xFFFFFFFF): TRUE (via FPDFBitmap_FillRect)
|
||||
[2026-07-30 17:35:02.665] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 3. Content scope: Entire page vertical slice from yTopPx=737 to yTopPx+regionH=783 (renders ALL page objects in slice, not just edited paragraph)
|
||||
[2026-07-30 17:35:02.665] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 4. Alpha transparency check: allAlpha255=TRUE (100% opaque, alpha=255 everywhere), non255Count=0
|
||||
[2026-07-30 17:35:02.675] [error] [RENDER_REGION_RAW_INSTRUMENTATION] Saved preview bitmap to debug_preview_region.png (size: 12450 bytes)
|
||||
[2026-07-30 17:35:02.686] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 1. Region width: 2448, height: 46 (dpi: 288, yTopPt: 184.46, heightPt: 11.59, pagePt: 612.00x792.00)
|
||||
[2026-07-30 17:35:02.686] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 2. Initialized with opaque white fill (0xFFFFFFFF): TRUE (via FPDFBitmap_FillRect)
|
||||
[2026-07-30 17:35:02.686] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 3. Content scope: Entire page vertical slice from yTopPx=737 to yTopPx+regionH=783 (renders ALL page objects in slice, not just edited paragraph)
|
||||
[2026-07-30 17:35:02.686] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 4. Alpha transparency check: allAlpha255=TRUE (100% opaque, alpha=255 everywhere), non255Count=0
|
||||
[2026-07-30 17:35:02.702] [error] [RENDER_REGION_RAW_INSTRUMENTATION] Saved preview bitmap to debug_preview_region.png (size: 13351 bytes)
|
||||
[2026-07-30 17:35:02.711] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 1. Region width: 2448, height: 37 (dpi: 288, yTopPt: 186.69, heightPt: 9.36, pagePt: 612.00x792.00)
|
||||
[2026-07-30 17:35:02.711] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 2. Initialized with opaque white fill (0xFFFFFFFF): TRUE (via FPDFBitmap_FillRect)
|
||||
[2026-07-30 17:35:02.711] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 3. Content scope: Entire page vertical slice from yTopPx=746 to yTopPx+regionH=783 (renders ALL page objects in slice, not just edited paragraph)
|
||||
[2026-07-30 17:35:02.711] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 4. Alpha transparency check: allAlpha255=TRUE (100% opaque, alpha=255 everywhere), non255Count=0
|
||||
[2026-07-30 17:35:02.719] [error] [RENDER_REGION_RAW_INSTRUMENTATION] Saved preview bitmap to debug_preview_region.png (size: 12015 bytes)
|
||||
[2026-07-30 17:35:02.727] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 1. Region width: 2448, height: 37 (dpi: 288, yTopPt: 186.69, heightPt: 9.36, pagePt: 612.00x792.00)
|
||||
[2026-07-30 17:35:02.727] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 2. Initialized with opaque white fill (0xFFFFFFFF): TRUE (via FPDFBitmap_FillRect)
|
||||
[2026-07-30 17:35:02.727] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 3. Content scope: Entire page vertical slice from yTopPx=746 to yTopPx+regionH=783 (renders ALL page objects in slice, not just edited paragraph)
|
||||
[2026-07-30 17:35:02.727] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 4. Alpha transparency check: allAlpha255=TRUE (100% opaque, alpha=255 everywhere), non255Count=0
|
||||
[2026-07-30 17:35:02.739] [error] [RENDER_REGION_RAW_INSTRUMENTATION] Saved preview bitmap to debug_preview_region.png (size: 12912 bytes)
|
||||
[2026-07-30 17:35:02.749] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 1. Region width: 2448, height: 37 (dpi: 288, yTopPt: 186.69, heightPt: 9.49, pagePt: 612.00x792.00)
|
||||
[2026-07-30 17:35:02.749] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 2. Initialized with opaque white fill (0xFFFFFFFF): TRUE (via FPDFBitmap_FillRect)
|
||||
[2026-07-30 17:35:02.749] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 3. Content scope: Entire page vertical slice from yTopPx=746 to yTopPx+regionH=783 (renders ALL page objects in slice, not just edited paragraph)
|
||||
[2026-07-30 17:35:02.749] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 4. Alpha transparency check: allAlpha255=TRUE (100% opaque, alpha=255 everywhere), non255Count=0
|
||||
[2026-07-30 17:35:02.762] [error] [RENDER_REGION_RAW_INSTRUMENTATION] Saved preview bitmap to debug_preview_region.png (size: 12015 bytes)
|
||||
[2026-07-30 17:35:02.770] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 1. Region width: 2448, height: 37 (dpi: 288, yTopPt: 186.69, heightPt: 9.49, pagePt: 612.00x792.00)
|
||||
[2026-07-30 17:35:02.770] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 2. Initialized with opaque white fill (0xFFFFFFFF): TRUE (via FPDFBitmap_FillRect)
|
||||
[2026-07-30 17:35:02.770] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 3. Content scope: Entire page vertical slice from yTopPx=746 to yTopPx+regionH=783 (renders ALL page objects in slice, not just edited paragraph)
|
||||
[2026-07-30 17:35:02.770] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 4. Alpha transparency check: allAlpha255=TRUE (100% opaque, alpha=255 everywhere), non255Count=0
|
||||
[2026-07-30 17:35:02.783] [error] [RENDER_REGION_RAW_INSTRUMENTATION] Saved preview bitmap to debug_preview_region.png (size: 12912 bytes)
|
||||
[2026-07-30 17:35:02.791] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 1. Region width: 2448, height: 46 (dpi: 288, yTopPt: 184.31, heightPt: 11.74, pagePt: 612.00x792.00)
|
||||
[2026-07-30 17:35:02.791] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 2. Initialized with opaque white fill (0xFFFFFFFF): TRUE (via FPDFBitmap_FillRect)
|
||||
[2026-07-30 17:35:02.791] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 3. Content scope: Entire page vertical slice from yTopPx=737 to yTopPx+regionH=783 (renders ALL page objects in slice, not just edited paragraph)
|
||||
[2026-07-30 17:35:02.792] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 4. Alpha transparency check: allAlpha255=TRUE (100% opaque, alpha=255 everywhere), non255Count=0
|
||||
[2026-07-30 17:35:02.810] [error] [RENDER_REGION_RAW_INSTRUMENTATION] Saved preview bitmap to debug_preview_region.png (size: 12450 bytes)
|
||||
[2026-07-30 17:35:02.816] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 1. Region width: 2448, height: 46 (dpi: 288, yTopPt: 184.31, heightPt: 11.74, pagePt: 612.00x792.00)
|
||||
[2026-07-30 17:35:02.816] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 2. Initialized with opaque white fill (0xFFFFFFFF): TRUE (via FPDFBitmap_FillRect)
|
||||
[2026-07-30 17:35:02.816] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 3. Content scope: Entire page vertical slice from yTopPx=737 to yTopPx+regionH=783 (renders ALL page objects in slice, not just edited paragraph)
|
||||
[2026-07-30 17:35:02.816] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 4. Alpha transparency check: allAlpha255=TRUE (100% opaque, alpha=255 everywhere), non255Count=0
|
||||
[2026-07-30 17:35:02.830] [error] [RENDER_REGION_RAW_INSTRUMENTATION] Saved preview bitmap to debug_preview_region.png (size: 13351 bytes)
|
||||
[2026-07-30 17:35:02.837] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 1. Region width: 2448, height: 37 (dpi: 288, yTopPt: 186.69, heightPt: 9.49, pagePt: 612.00x792.00)
|
||||
[2026-07-30 17:35:02.837] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 2. Initialized with opaque white fill (0xFFFFFFFF): TRUE (via FPDFBitmap_FillRect)
|
||||
[2026-07-30 17:35:02.837] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 3. Content scope: Entire page vertical slice from yTopPx=746 to yTopPx+regionH=783 (renders ALL page objects in slice, not just edited paragraph)
|
||||
[2026-07-30 17:35:02.837] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 4. Alpha transparency check: allAlpha255=TRUE (100% opaque, alpha=255 everywhere), non255Count=0
|
||||
[2026-07-30 17:35:02.846] [error] [RENDER_REGION_RAW_INSTRUMENTATION] Saved preview bitmap to debug_preview_region.png (size: 12015 bytes)
|
||||
[2026-07-30 17:35:02.852] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 1. Region width: 2448, height: 37 (dpi: 288, yTopPt: 186.69, heightPt: 9.49, pagePt: 612.00x792.00)
|
||||
[2026-07-30 17:35:02.852] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 2. Initialized with opaque white fill (0xFFFFFFFF): TRUE (via FPDFBitmap_FillRect)
|
||||
[2026-07-30 17:35:02.852] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 3. Content scope: Entire page vertical slice from yTopPx=746 to yTopPx+regionH=783 (renders ALL page objects in slice, not just edited paragraph)
|
||||
[2026-07-30 17:35:02.852] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 4. Alpha transparency check: allAlpha255=TRUE (100% opaque, alpha=255 everywhere), non255Count=0
|
||||
[2026-07-30 17:35:02.861] [error] [RENDER_REGION_RAW_INSTRUMENTATION] Saved preview bitmap to debug_preview_region.png (size: 12912 bytes)
|
||||
[2026-07-30 17:35:02.866] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 1. Region width: 2448, height: 37 (dpi: 288, yTopPt: 186.69, heightPt: 9.49, pagePt: 612.00x792.00)
|
||||
[2026-07-30 17:35:02.866] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 2. Initialized with opaque white fill (0xFFFFFFFF): TRUE (via FPDFBitmap_FillRect)
|
||||
[2026-07-30 17:35:02.866] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 3. Content scope: Entire page vertical slice from yTopPx=746 to yTopPx+regionH=783 (renders ALL page objects in slice, not just edited paragraph)
|
||||
[2026-07-30 17:35:02.866] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 4. Alpha transparency check: allAlpha255=TRUE (100% opaque, alpha=255 everywhere), non255Count=0
|
||||
[2026-07-30 17:35:02.872] [error] [RENDER_REGION_RAW_INSTRUMENTATION] Saved preview bitmap to debug_preview_region.png (size: 12015 bytes)
|
||||
[2026-07-30 17:35:02.880] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 1. Region width: 2448, height: 37 (dpi: 288, yTopPt: 186.69, heightPt: 9.49, pagePt: 612.00x792.00)
|
||||
[2026-07-30 17:35:02.880] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 2. Initialized with opaque white fill (0xFFFFFFFF): TRUE (via FPDFBitmap_FillRect)
|
||||
[2026-07-30 17:35:02.880] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 3. Content scope: Entire page vertical slice from yTopPx=746 to yTopPx+regionH=783 (renders ALL page objects in slice, not just edited paragraph)
|
||||
[2026-07-30 17:35:02.880] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 4. Alpha transparency check: allAlpha255=TRUE (100% opaque, alpha=255 everywhere), non255Count=0
|
||||
[2026-07-30 17:35:02.887] [error] [RENDER_REGION_RAW_INSTRUMENTATION] Saved preview bitmap to debug_preview_region.png (size: 12912 bytes)
|
||||
[2026-07-30 17:35:02.897] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 1. Region width: 2448, height: 37 (dpi: 288, yTopPt: 186.69, heightPt: 9.49, pagePt: 612.00x792.00)
|
||||
[2026-07-30 17:35:02.897] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 2. Initialized with opaque white fill (0xFFFFFFFF): TRUE (via FPDFBitmap_FillRect)
|
||||
[2026-07-30 17:35:02.897] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 3. Content scope: Entire page vertical slice from yTopPx=746 to yTopPx+regionH=783 (renders ALL page objects in slice, not just edited paragraph)
|
||||
[2026-07-30 17:35:02.897] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 4. Alpha transparency check: allAlpha255=TRUE (100% opaque, alpha=255 everywhere), non255Count=0
|
||||
[2026-07-30 17:35:02.910] [error] [RENDER_REGION_RAW_INSTRUMENTATION] Saved preview bitmap to debug_preview_region.png (size: 12015 bytes)
|
||||
[2026-07-30 17:35:02.915] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 1. Region width: 2448, height: 37 (dpi: 288, yTopPt: 186.69, heightPt: 9.49, pagePt: 612.00x792.00)
|
||||
[2026-07-30 17:35:02.915] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 2. Initialized with opaque white fill (0xFFFFFFFF): TRUE (via FPDFBitmap_FillRect)
|
||||
[2026-07-30 17:35:02.915] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 3. Content scope: Entire page vertical slice from yTopPx=746 to yTopPx+regionH=783 (renders ALL page objects in slice, not just edited paragraph)
|
||||
[2026-07-30 17:35:02.915] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 4. Alpha transparency check: allAlpha255=TRUE (100% opaque, alpha=255 everywhere), non255Count=0
|
||||
[2026-07-30 17:35:02.922] [error] [RENDER_REGION_RAW_INSTRUMENTATION] Saved preview bitmap to debug_preview_region.png (size: 12912 bytes)
|
||||
[2026-07-30 17:35:02.930] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 1. Region width: 2448, height: 46 (dpi: 288, yTopPt: 184.46, heightPt: 11.59, pagePt: 612.00x792.00)
|
||||
[2026-07-30 17:35:02.930] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 2. Initialized with opaque white fill (0xFFFFFFFF): TRUE (via FPDFBitmap_FillRect)
|
||||
[2026-07-30 17:35:02.930] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 3. Content scope: Entire page vertical slice from yTopPx=737 to yTopPx+regionH=783 (renders ALL page objects in slice, not just edited paragraph)
|
||||
[2026-07-30 17:35:02.930] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 4. Alpha transparency check: allAlpha255=TRUE (100% opaque, alpha=255 everywhere), non255Count=0
|
||||
[2026-07-30 17:35:02.939] [error] [RENDER_REGION_RAW_INSTRUMENTATION] Saved preview bitmap to debug_preview_region.png (size: 12450 bytes)
|
||||
[2026-07-30 17:35:02.950] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 1. Region width: 2448, height: 46 (dpi: 288, yTopPt: 184.46, heightPt: 11.59, pagePt: 612.00x792.00)
|
||||
[2026-07-30 17:35:02.950] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 2. Initialized with opaque white fill (0xFFFFFFFF): TRUE (via FPDFBitmap_FillRect)
|
||||
[2026-07-30 17:35:02.950] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 3. Content scope: Entire page vertical slice from yTopPx=737 to yTopPx+regionH=783 (renders ALL page objects in slice, not just edited paragraph)
|
||||
[2026-07-30 17:35:02.950] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 4. Alpha transparency check: allAlpha255=TRUE (100% opaque, alpha=255 everywhere), non255Count=0
|
||||
[2026-07-30 17:35:02.967] [error] [RENDER_REGION_RAW_INSTRUMENTATION] Saved preview bitmap to debug_preview_region.png (size: 13351 bytes)
|
||||
[2026-07-30 17:35:02.978] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 1. Region width: 2448, height: 37 (dpi: 288, yTopPt: 186.69, heightPt: 9.49, pagePt: 612.00x792.00)
|
||||
[2026-07-30 17:35:02.979] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 2. Initialized with opaque white fill (0xFFFFFFFF): TRUE (via FPDFBitmap_FillRect)
|
||||
[2026-07-30 17:35:02.979] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 3. Content scope: Entire page vertical slice from yTopPx=746 to yTopPx+regionH=783 (renders ALL page objects in slice, not just edited paragraph)
|
||||
[2026-07-30 17:35:02.979] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 4. Alpha transparency check: allAlpha255=TRUE (100% opaque, alpha=255 everywhere), non255Count=0
|
||||
[2026-07-30 17:35:02.993] [error] [RENDER_REGION_RAW_INSTRUMENTATION] Saved preview bitmap to debug_preview_region.png (size: 12015 bytes)
|
||||
[2026-07-30 17:35:03.004] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 1. Region width: 2448, height: 37 (dpi: 288, yTopPt: 186.69, heightPt: 9.49, pagePt: 612.00x792.00)
|
||||
[2026-07-30 17:35:03.004] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 2. Initialized with opaque white fill (0xFFFFFFFF): TRUE (via FPDFBitmap_FillRect)
|
||||
[2026-07-30 17:35:03.004] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 3. Content scope: Entire page vertical slice from yTopPx=746 to yTopPx+regionH=783 (renders ALL page objects in slice, not just edited paragraph)
|
||||
[2026-07-30 17:35:03.004] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 4. Alpha transparency check: allAlpha255=TRUE (100% opaque, alpha=255 everywhere), non255Count=0
|
||||
[2026-07-30 17:35:03.014] [error] [RENDER_REGION_RAW_INSTRUMENTATION] Saved preview bitmap to debug_preview_region.png (size: 12912 bytes)
|
||||
[2026-07-30 17:35:03.020] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 1. Region width: 2448, height: 37 (dpi: 288, yTopPt: 186.69, heightPt: 9.36, pagePt: 612.00x792.00)
|
||||
[2026-07-30 17:35:03.020] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 2. Initialized with opaque white fill (0xFFFFFFFF): TRUE (via FPDFBitmap_FillRect)
|
||||
[2026-07-30 17:35:03.020] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 3. Content scope: Entire page vertical slice from yTopPx=746 to yTopPx+regionH=783 (renders ALL page objects in slice, not just edited paragraph)
|
||||
[2026-07-30 17:35:03.020] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 4. Alpha transparency check: allAlpha255=TRUE (100% opaque, alpha=255 everywhere), non255Count=0
|
||||
[2026-07-30 17:35:03.032] [error] [RENDER_REGION_RAW_INSTRUMENTATION] Saved preview bitmap to debug_preview_region.png (size: 12015 bytes)
|
||||
[2026-07-30 17:35:03.042] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 1. Region width: 2448, height: 37 (dpi: 288, yTopPt: 186.69, heightPt: 9.36, pagePt: 612.00x792.00)
|
||||
[2026-07-30 17:35:03.042] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 2. Initialized with opaque white fill (0xFFFFFFFF): TRUE (via FPDFBitmap_FillRect)
|
||||
[2026-07-30 17:35:03.042] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 3. Content scope: Entire page vertical slice from yTopPx=746 to yTopPx+regionH=783 (renders ALL page objects in slice, not just edited paragraph)
|
||||
[2026-07-30 17:35:03.042] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 4. Alpha transparency check: allAlpha255=TRUE (100% opaque, alpha=255 everywhere), non255Count=0
|
||||
[2026-07-30 17:35:03.051] [error] [RENDER_REGION_RAW_INSTRUMENTATION] Saved preview bitmap to debug_preview_region.png (size: 12912 bytes)
|
||||
[2026-07-30 17:35:03.061] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 1. Region width: 2448, height: 37 (dpi: 288, yTopPt: 186.69, heightPt: 9.49, pagePt: 612.00x792.00)
|
||||
[2026-07-30 17:35:03.061] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 2. Initialized with opaque white fill (0xFFFFFFFF): TRUE (via FPDFBitmap_FillRect)
|
||||
[2026-07-30 17:35:03.061] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 3. Content scope: Entire page vertical slice from yTopPx=746 to yTopPx+regionH=783 (renders ALL page objects in slice, not just edited paragraph)
|
||||
[2026-07-30 17:35:03.061] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 4. Alpha transparency check: allAlpha255=TRUE (100% opaque, alpha=255 everywhere), non255Count=0
|
||||
[2026-07-30 17:35:03.072] [error] [RENDER_REGION_RAW_INSTRUMENTATION] Saved preview bitmap to debug_preview_region.png (size: 12015 bytes)
|
||||
[2026-07-30 17:35:03.082] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 1. Region width: 2448, height: 37 (dpi: 288, yTopPt: 186.69, heightPt: 9.49, pagePt: 612.00x792.00)
|
||||
[2026-07-30 17:35:03.082] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 2. Initialized with opaque white fill (0xFFFFFFFF): TRUE (via FPDFBitmap_FillRect)
|
||||
[2026-07-30 17:35:03.082] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 3. Content scope: Entire page vertical slice from yTopPx=746 to yTopPx+regionH=783 (renders ALL page objects in slice, not just edited paragraph)
|
||||
[2026-07-30 17:35:03.082] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 4. Alpha transparency check: allAlpha255=TRUE (100% opaque, alpha=255 everywhere), non255Count=0
|
||||
[2026-07-30 17:35:03.094] [error] [RENDER_REGION_RAW_INSTRUMENTATION] Saved preview bitmap to debug_preview_region.png (size: 12912 bytes)
|
||||
[2026-07-30 17:35:03.100] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 1. Region width: 2448, height: 46 (dpi: 288, yTopPt: 184.46, heightPt: 11.59, pagePt: 612.00x792.00)
|
||||
[2026-07-30 17:35:03.100] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 2. Initialized with opaque white fill (0xFFFFFFFF): TRUE (via FPDFBitmap_FillRect)
|
||||
[2026-07-30 17:35:03.100] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 3. Content scope: Entire page vertical slice from yTopPx=737 to yTopPx+regionH=783 (renders ALL page objects in slice, not just edited paragraph)
|
||||
[2026-07-30 17:35:03.100] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 4. Alpha transparency check: allAlpha255=TRUE (100% opaque, alpha=255 everywhere), non255Count=0
|
||||
[2026-07-30 17:35:03.111] [error] [RENDER_REGION_RAW_INSTRUMENTATION] Saved preview bitmap to debug_preview_region.png (size: 12450 bytes)
|
||||
[2026-07-30 17:35:03.120] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 1. Region width: 2448, height: 46 (dpi: 288, yTopPt: 184.46, heightPt: 11.59, pagePt: 612.00x792.00)
|
||||
[2026-07-30 17:35:03.120] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 2. Initialized with opaque white fill (0xFFFFFFFF): TRUE (via FPDFBitmap_FillRect)
|
||||
[2026-07-30 17:35:03.120] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 3. Content scope: Entire page vertical slice from yTopPx=737 to yTopPx+regionH=783 (renders ALL page objects in slice, not just edited paragraph)
|
||||
[2026-07-30 17:35:03.120] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 4. Alpha transparency check: allAlpha255=TRUE (100% opaque, alpha=255 everywhere), non255Count=0
|
||||
[2026-07-30 17:35:03.134] [error] [RENDER_REGION_RAW_INSTRUMENTATION] Saved preview bitmap to debug_preview_region.png (size: 13351 bytes)
|
||||
[2026-07-30 17:35:03.144] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 1. Region width: 2448, height: 46 (dpi: 288, yTopPt: 184.46, heightPt: 11.59, pagePt: 612.00x792.00)
|
||||
[2026-07-30 17:35:03.144] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 2. Initialized with opaque white fill (0xFFFFFFFF): TRUE (via FPDFBitmap_FillRect)
|
||||
[2026-07-30 17:35:03.144] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 3. Content scope: Entire page vertical slice from yTopPx=737 to yTopPx+regionH=783 (renders ALL page objects in slice, not just edited paragraph)
|
||||
[2026-07-30 17:35:03.144] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 4. Alpha transparency check: allAlpha255=TRUE (100% opaque, alpha=255 everywhere), non255Count=0
|
||||
[2026-07-30 17:35:03.152] [error] [RENDER_REGION_RAW_INSTRUMENTATION] Saved preview bitmap to debug_preview_region.png (size: 12450 bytes)
|
||||
[2026-07-30 17:35:03.160] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 1. Region width: 2448, height: 46 (dpi: 288, yTopPt: 184.46, heightPt: 11.59, pagePt: 612.00x792.00)
|
||||
[2026-07-30 17:35:03.160] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 2. Initialized with opaque white fill (0xFFFFFFFF): TRUE (via FPDFBitmap_FillRect)
|
||||
[2026-07-30 17:35:03.160] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 3. Content scope: Entire page vertical slice from yTopPx=737 to yTopPx+regionH=783 (renders ALL page objects in slice, not just edited paragraph)
|
||||
[2026-07-30 17:35:03.160] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 4. Alpha transparency check: allAlpha255=TRUE (100% opaque, alpha=255 everywhere), non255Count=0
|
||||
[2026-07-30 17:35:03.175] [error] [RENDER_REGION_RAW_INSTRUMENTATION] Saved preview bitmap to debug_preview_region.png (size: 13351 bytes)
|
||||
[2026-07-30 17:35:03.185] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 1. Region width: 2448, height: 36 (dpi: 288, yTopPt: 186.82, heightPt: 9.23, pagePt: 612.00x792.00)
|
||||
[2026-07-30 17:35:03.185] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 2. Initialized with opaque white fill (0xFFFFFFFF): TRUE (via FPDFBitmap_FillRect)
|
||||
[2026-07-30 17:35:03.185] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 3. Content scope: Entire page vertical slice from yTopPx=747 to yTopPx+regionH=783 (renders ALL page objects in slice, not just edited paragraph)
|
||||
[2026-07-30 17:35:03.185] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 4. Alpha transparency check: allAlpha255=TRUE (100% opaque, alpha=255 everywhere), non255Count=0
|
||||
[2026-07-30 17:35:03.198] [error] [RENDER_REGION_RAW_INSTRUMENTATION] Saved preview bitmap to debug_preview_region.png (size: 11952 bytes)
|
||||
[2026-07-30 17:35:03.204] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 1. Region width: 2448, height: 36 (dpi: 288, yTopPt: 186.82, heightPt: 9.23, pagePt: 612.00x792.00)
|
||||
[2026-07-30 17:35:03.204] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 2. Initialized with opaque white fill (0xFFFFFFFF): TRUE (via FPDFBitmap_FillRect)
|
||||
[2026-07-30 17:35:03.204] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 3. Content scope: Entire page vertical slice from yTopPx=747 to yTopPx+regionH=783 (renders ALL page objects in slice, not just edited paragraph)
|
||||
[2026-07-30 17:35:03.204] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 4. Alpha transparency check: allAlpha255=TRUE (100% opaque, alpha=255 everywhere), non255Count=0
|
||||
[2026-07-30 17:35:03.214] [error] [RENDER_REGION_RAW_INSTRUMENTATION] Saved preview bitmap to debug_preview_region.png (size: 12852 bytes)
|
||||
[2026-07-30 17:35:03.224] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 1. Region width: 2448, height: 46 (dpi: 288, yTopPt: 186.69, heightPt: 11.71, pagePt: 612.00x792.00)
|
||||
[2026-07-30 17:35:03.224] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 2. Initialized with opaque white fill (0xFFFFFFFF): TRUE (via FPDFBitmap_FillRect)
|
||||
[2026-07-30 17:35:03.224] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 3. Content scope: Entire page vertical slice from yTopPx=746 to yTopPx+regionH=792 (renders ALL page objects in slice, not just edited paragraph)
|
||||
[2026-07-30 17:35:03.224] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 4. Alpha transparency check: allAlpha255=TRUE (100% opaque, alpha=255 everywhere), non255Count=0
|
||||
[2026-07-30 17:35:03.233] [error] [RENDER_REGION_RAW_INSTRUMENTATION] Saved preview bitmap to debug_preview_region.png (size: 12239 bytes)
|
||||
[2026-07-30 17:35:03.243] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 1. Region width: 2448, height: 46 (dpi: 288, yTopPt: 186.69, heightPt: 11.71, pagePt: 612.00x792.00)
|
||||
[2026-07-30 17:35:03.243] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 2. Initialized with opaque white fill (0xFFFFFFFF): TRUE (via FPDFBitmap_FillRect)
|
||||
[2026-07-30 17:35:03.243] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 3. Content scope: Entire page vertical slice from yTopPx=746 to yTopPx+regionH=792 (renders ALL page objects in slice, not just edited paragraph)
|
||||
[2026-07-30 17:35:03.243] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 4. Alpha transparency check: allAlpha255=TRUE (100% opaque, alpha=255 everywhere), non255Count=0
|
||||
[2026-07-30 17:35:03.251] [error] [RENDER_REGION_RAW_INSTRUMENTATION] Saved preview bitmap to debug_preview_region.png (size: 13138 bytes)
|
||||
[2026-07-30 17:35:03.259] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 1. Region width: 2448, height: 37 (dpi: 288, yTopPt: 186.69, heightPt: 9.49, pagePt: 612.00x792.00)
|
||||
[2026-07-30 17:35:03.259] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 2. Initialized with opaque white fill (0xFFFFFFFF): TRUE (via FPDFBitmap_FillRect)
|
||||
[2026-07-30 17:35:03.259] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 3. Content scope: Entire page vertical slice from yTopPx=746 to yTopPx+regionH=783 (renders ALL page objects in slice, not just edited paragraph)
|
||||
[2026-07-30 17:35:03.259] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 4. Alpha transparency check: allAlpha255=TRUE (100% opaque, alpha=255 everywhere), non255Count=0
|
||||
[2026-07-30 17:35:03.271] [error] [RENDER_REGION_RAW_INSTRUMENTATION] Saved preview bitmap to debug_preview_region.png (size: 12015 bytes)
|
||||
[2026-07-30 17:35:03.279] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 1. Region width: 2448, height: 37 (dpi: 288, yTopPt: 186.69, heightPt: 9.49, pagePt: 612.00x792.00)
|
||||
[2026-07-30 17:35:03.279] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 2. Initialized with opaque white fill (0xFFFFFFFF): TRUE (via FPDFBitmap_FillRect)
|
||||
[2026-07-30 17:35:03.279] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 3. Content scope: Entire page vertical slice from yTopPx=746 to yTopPx+regionH=783 (renders ALL page objects in slice, not just edited paragraph)
|
||||
[2026-07-30 17:35:03.279] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 4. Alpha transparency check: allAlpha255=TRUE (100% opaque, alpha=255 everywhere), non255Count=0
|
||||
[2026-07-30 17:35:03.285] [error] [RENDER_REGION_RAW_INSTRUMENTATION] Saved preview bitmap to debug_preview_region.png (size: 12912 bytes)
|
||||
[2026-07-30 17:35:03.295] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 1. Region width: 2448, height: 37 (dpi: 288, yTopPt: 186.69, heightPt: 9.36, pagePt: 612.00x792.00)
|
||||
[2026-07-30 17:35:03.295] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 2. Initialized with opaque white fill (0xFFFFFFFF): TRUE (via FPDFBitmap_FillRect)
|
||||
[2026-07-30 17:35:03.295] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 3. Content scope: Entire page vertical slice from yTopPx=746 to yTopPx+regionH=783 (renders ALL page objects in slice, not just edited paragraph)
|
||||
[2026-07-30 17:35:03.295] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 4. Alpha transparency check: allAlpha255=TRUE (100% opaque, alpha=255 everywhere), non255Count=0
|
||||
[2026-07-30 17:35:03.301] [error] [RENDER_REGION_RAW_INSTRUMENTATION] Saved preview bitmap to debug_preview_region.png (size: 12015 bytes)
|
||||
[2026-07-30 17:35:03.309] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 1. Region width: 2448, height: 37 (dpi: 288, yTopPt: 186.69, heightPt: 9.36, pagePt: 612.00x792.00)
|
||||
[2026-07-30 17:35:03.309] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 2. Initialized with opaque white fill (0xFFFFFFFF): TRUE (via FPDFBitmap_FillRect)
|
||||
[2026-07-30 17:35:03.309] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 3. Content scope: Entire page vertical slice from yTopPx=746 to yTopPx+regionH=783 (renders ALL page objects in slice, not just edited paragraph)
|
||||
[2026-07-30 17:35:03.309] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 4. Alpha transparency check: allAlpha255=TRUE (100% opaque, alpha=255 everywhere), non255Count=0
|
||||
[2026-07-30 17:35:03.319] [error] [RENDER_REGION_RAW_INSTRUMENTATION] Saved preview bitmap to debug_preview_region.png (size: 12912 bytes)
|
||||
[2026-07-30 17:35:03.328] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 1. Region width: 2448, height: 46 (dpi: 288, yTopPt: 184.46, heightPt: 11.59, pagePt: 612.00x792.00)
|
||||
[2026-07-30 17:35:03.328] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 2. Initialized with opaque white fill (0xFFFFFFFF): TRUE (via FPDFBitmap_FillRect)
|
||||
[2026-07-30 17:35:03.328] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 3. Content scope: Entire page vertical slice from yTopPx=737 to yTopPx+regionH=783 (renders ALL page objects in slice, not just edited paragraph)
|
||||
[2026-07-30 17:35:03.328] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 4. Alpha transparency check: allAlpha255=TRUE (100% opaque, alpha=255 everywhere), non255Count=0
|
||||
[2026-07-30 17:35:03.337] [error] [RENDER_REGION_RAW_INSTRUMENTATION] Saved preview bitmap to debug_preview_region.png (size: 12450 bytes)
|
||||
[2026-07-30 17:35:03.348] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 1. Region width: 2448, height: 46 (dpi: 288, yTopPt: 184.46, heightPt: 11.59, pagePt: 612.00x792.00)
|
||||
[2026-07-30 17:35:03.348] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 2. Initialized with opaque white fill (0xFFFFFFFF): TRUE (via FPDFBitmap_FillRect)
|
||||
[2026-07-30 17:35:03.348] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 3. Content scope: Entire page vertical slice from yTopPx=737 to yTopPx+regionH=783 (renders ALL page objects in slice, not just edited paragraph)
|
||||
[2026-07-30 17:35:03.348] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 4. Alpha transparency check: allAlpha255=TRUE (100% opaque, alpha=255 everywhere), non255Count=0
|
||||
[2026-07-30 17:35:03.361] [error] [RENDER_REGION_RAW_INSTRUMENTATION] Saved preview bitmap to debug_preview_region.png (size: 13351 bytes)
|
||||
[2026-07-30 17:35:03.367] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 1. Region width: 2448, height: 37 (dpi: 288, yTopPt: 186.69, heightPt: 9.49, pagePt: 612.00x792.00)
|
||||
[2026-07-30 17:35:03.367] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 2. Initialized with opaque white fill (0xFFFFFFFF): TRUE (via FPDFBitmap_FillRect)
|
||||
[2026-07-30 17:35:03.367] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 3. Content scope: Entire page vertical slice from yTopPx=746 to yTopPx+regionH=783 (renders ALL page objects in slice, not just edited paragraph)
|
||||
[2026-07-30 17:35:03.367] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 4. Alpha transparency check: allAlpha255=TRUE (100% opaque, alpha=255 everywhere), non255Count=0
|
||||
[2026-07-30 17:35:03.375] [error] [RENDER_REGION_RAW_INSTRUMENTATION] Saved preview bitmap to debug_preview_region.png (size: 12015 bytes)
|
||||
[2026-07-30 17:35:03.381] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 1. Region width: 2448, height: 37 (dpi: 288, yTopPt: 186.69, heightPt: 9.49, pagePt: 612.00x792.00)
|
||||
[2026-07-30 17:35:03.381] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 2. Initialized with opaque white fill (0xFFFFFFFF): TRUE (via FPDFBitmap_FillRect)
|
||||
[2026-07-30 17:35:03.381] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 3. Content scope: Entire page vertical slice from yTopPx=746 to yTopPx+regionH=783 (renders ALL page objects in slice, not just edited paragraph)
|
||||
[2026-07-30 17:35:03.381] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 4. Alpha transparency check: allAlpha255=TRUE (100% opaque, alpha=255 everywhere), non255Count=0
|
||||
[2026-07-30 17:35:03.388] [error] [RENDER_REGION_RAW_INSTRUMENTATION] Saved preview bitmap to debug_preview_region.png (size: 12912 bytes)
|
||||
[2026-07-30 17:35:03.398] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 1. Region width: 2448, height: 37 (dpi: 288, yTopPt: 186.69, heightPt: 9.36, pagePt: 612.00x792.00)
|
||||
[2026-07-30 17:35:03.398] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 2. Initialized with opaque white fill (0xFFFFFFFF): TRUE (via FPDFBitmap_FillRect)
|
||||
[2026-07-30 17:35:03.398] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 3. Content scope: Entire page vertical slice from yTopPx=746 to yTopPx+regionH=783 (renders ALL page objects in slice, not just edited paragraph)
|
||||
[2026-07-30 17:35:03.398] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 4. Alpha transparency check: allAlpha255=TRUE (100% opaque, alpha=255 everywhere), non255Count=0
|
||||
[2026-07-30 17:35:03.410] [error] [RENDER_REGION_RAW_INSTRUMENTATION] Saved preview bitmap to debug_preview_region.png (size: 12015 bytes)
|
||||
[2026-07-30 17:35:03.416] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 1. Region width: 2448, height: 37 (dpi: 288, yTopPt: 186.69, heightPt: 9.36, pagePt: 612.00x792.00)
|
||||
[2026-07-30 17:35:03.416] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 2. Initialized with opaque white fill (0xFFFFFFFF): TRUE (via FPDFBitmap_FillRect)
|
||||
[2026-07-30 17:35:03.416] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 3. Content scope: Entire page vertical slice from yTopPx=746 to yTopPx+regionH=783 (renders ALL page objects in slice, not just edited paragraph)
|
||||
[2026-07-30 17:35:03.416] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 4. Alpha transparency check: allAlpha255=TRUE (100% opaque, alpha=255 everywhere), non255Count=0
|
||||
[2026-07-30 17:35:03.425] [error] [RENDER_REGION_RAW_INSTRUMENTATION] Saved preview bitmap to debug_preview_region.png (size: 12912 bytes)
|
||||
[2026-07-30 17:35:03.434] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 1. Region width: 2448, height: 37 (dpi: 288, yTopPt: 186.69, heightPt: 9.49, pagePt: 612.00x792.00)
|
||||
[2026-07-30 17:35:03.434] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 2. Initialized with opaque white fill (0xFFFFFFFF): TRUE (via FPDFBitmap_FillRect)
|
||||
[2026-07-30 17:35:03.434] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 3. Content scope: Entire page vertical slice from yTopPx=746 to yTopPx+regionH=783 (renders ALL page objects in slice, not just edited paragraph)
|
||||
[2026-07-30 17:35:03.434] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 4. Alpha transparency check: allAlpha255=TRUE (100% opaque, alpha=255 everywhere), non255Count=0
|
||||
[2026-07-30 17:35:03.446] [error] [RENDER_REGION_RAW_INSTRUMENTATION] Saved preview bitmap to debug_preview_region.png (size: 12015 bytes)
|
||||
[2026-07-30 17:35:03.455] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 1. Region width: 2448, height: 37 (dpi: 288, yTopPt: 186.69, heightPt: 9.49, pagePt: 612.00x792.00)
|
||||
[2026-07-30 17:35:03.455] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 2. Initialized with opaque white fill (0xFFFFFFFF): TRUE (via FPDFBitmap_FillRect)
|
||||
[2026-07-30 17:35:03.455] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 3. Content scope: Entire page vertical slice from yTopPx=746 to yTopPx+regionH=783 (renders ALL page objects in slice, not just edited paragraph)
|
||||
[2026-07-30 17:35:03.455] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 4. Alpha transparency check: allAlpha255=TRUE (100% opaque, alpha=255 everywhere), non255Count=0
|
||||
[2026-07-30 17:35:03.469] [error] [RENDER_REGION_RAW_INSTRUMENTATION] Saved preview bitmap to debug_preview_region.png (size: 12912 bytes)
|
||||
[2026-07-30 17:35:03.480] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 1. Region width: 2448, height: 37 (dpi: 288, yTopPt: 186.69, heightPt: 9.49, pagePt: 612.00x792.00)
|
||||
[2026-07-30 17:35:03.480] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 2. Initialized with opaque white fill (0xFFFFFFFF): TRUE (via FPDFBitmap_FillRect)
|
||||
[2026-07-30 17:35:03.480] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 3. Content scope: Entire page vertical slice from yTopPx=746 to yTopPx+regionH=783 (renders ALL page objects in slice, not just edited paragraph)
|
||||
[2026-07-30 17:35:03.480] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 4. Alpha transparency check: allAlpha255=TRUE (100% opaque, alpha=255 everywhere), non255Count=0
|
||||
[2026-07-30 17:35:03.494] [error] [RENDER_REGION_RAW_INSTRUMENTATION] Saved preview bitmap to debug_preview_region.png (size: 12015 bytes)
|
||||
[2026-07-30 17:35:03.503] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 1. Region width: 2448, height: 37 (dpi: 288, yTopPt: 186.69, heightPt: 9.49, pagePt: 612.00x792.00)
|
||||
[2026-07-30 17:35:03.503] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 2. Initialized with opaque white fill (0xFFFFFFFF): TRUE (via FPDFBitmap_FillRect)
|
||||
[2026-07-30 17:35:03.503] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 3. Content scope: Entire page vertical slice from yTopPx=746 to yTopPx+regionH=783 (renders ALL page objects in slice, not just edited paragraph)
|
||||
[2026-07-30 17:35:03.503] [error] [RENDER_REGION_RAW_INSTRUMENTATION] 4. Alpha transparency check: allAlpha255=TRUE (100% opaque, alpha=255 everywhere), non255Count=0
|
||||
[2026-07-30 17:35:03.512] [error] [RENDER_REGION_RAW_INSTRUMENTATION] Saved preview bitmap to debug_preview_region.png (size: 12912 bytes)
|
||||
=== LOAD ORIGINAL ===
|
||||
para text='Professional Experience'
|
||||
|
||||
=== BEFORE (unchanged, with origLines+advances) ===
|
||||
|
||||
=== AFTER (typed +x, advanceSeedText merge, no origLines) ===
|
||||
runs=[('Professional Experiencex', 23, 'Professional Experience')]
|
||||
before extracted='Professional Experience'
|
||||
after extracted='Professional Experiencex'
|
||||
before glyphs=23 after glyphs=24
|
||||
before stream emits=23 after stream emits=24
|
||||
before fonts: {
|
||||
"FXF1": {
|
||||
"res": "FXF1",
|
||||
"font_obj": 5,
|
||||
"baseFont": "BCDEEE+Arial-BoldMT",
|
||||
"fontfile2_len": 74352,
|
||||
"fontfile2_sha": "cc80da80a119a52c"
|
||||
},
|
||||
"FXF2": {
|
||||
"res": "FXF2",
|
||||
"font_obj": 9,
|
||||
"baseFont": "BCDFEE+ArialMT",
|
||||
"fontfile2_len": 103052,
|
||||
"fontfile2_sha": "4f617da732cd3bac"
|
||||
}
|
||||
}
|
||||
after fonts: {
|
||||
"FXF1": {
|
||||
"res": "FXF1",
|
||||
"font_obj": 5,
|
||||
"baseFont": "BCDEEE+Arial-BoldMT",
|
||||
"fontfile2_len": 74352,
|
||||
"fontfile2_sha": "cc80da80a119a52c"
|
||||
},
|
||||
"FXF2": {
|
||||
"res": "FXF2",
|
||||
"font_obj": 9,
|
||||
"baseFont": "BCDFEE+ArialMT",
|
||||
"fontfile2_len": 103052,
|
||||
"fontfile2_sha": "4f617da732cd3bac"
|
||||
}
|
||||
}
|
||||
|
||||
=== PER-GLYPH BEFORE vs AFTER (shared prefix) ===
|
||||
# ch gidB gidA xB xA dx advB advA dAdv fontB fontA outEq
|
||||
0 P 80 80 50.5250 50.5250 0.0000 8.0040 8.0040 0.0000 FXF1 FXF1 True
|
||||
1 r 114 114 58.5290 58.5290 0.0000 4.7400 4.7400 0.0000 FXF1 FXF1 True
|
||||
2 o 111 111 63.2690 63.2690 0.0000 7.2480 7.2480 0.0000 FXF1 FXF1 True
|
||||
3 f 102 102 70.5170 70.5170 0.0000 3.9960 3.9960 0.0000 FXF1 FXF1 True
|
||||
4 e 101 101 74.5130 74.5130 0.0000 6.7440 6.7440 0.0000 FXF1 FXF1 True
|
||||
5 s 115 115 81.2570 81.2570 0.0000 6.7440 6.7440 0.0000 FXF1 FXF1 True
|
||||
6 s 115 115 88.0010 88.0010 0.0000 6.7440 6.7440 0.0000 FXF1 FXF1 True
|
||||
7 i 105 105 94.7450 94.7450 0.0000 3.2520 3.2520 0.0000 FXF1 FXF1 True
|
||||
8 o 111 111 97.9970 97.9970 0.0000 7.2480 7.2480 0.0000 FXF1 FXF1 True
|
||||
9 n 110 110 105.2450 105.2450 0.0000 7.2480 7.2480 0.0000 FXF1 FXF1 True
|
||||
10 a 97 97 112.4930 112.4930 0.0000 6.7440 6.7440 0.0000 FXF1 FXF1 True
|
||||
11 l 108 108 119.2370 119.2370 0.0000 1.6440 1.6440 0.0000 FXF1 FXF1 True
|
||||
12 None None 122.5730 122.5730 0.0000 0.5000 0.5000 0.0000 None None None
|
||||
13 E 69 69 125.7410 125.7410 0.0000 8.0040 8.0040 0.0000 FXF1 FXF1 True
|
||||
14 x 120 120 133.7450 133.7450 0.0000 6.7320 6.7320 0.0000 FXF1 FXF1 True
|
||||
15 p 112 112 140.4770 140.4770 0.0000 7.2480 7.2480 0.0000 FXF1 FXF1 True
|
||||
16 e 101 101 147.7250 147.7250 0.0000 6.7440 6.7440 0.0000 FXF1 FXF1 True
|
||||
17 r 114 114 154.4690 154.4690 0.0000 4.7400 4.7400 0.0000 FXF1 FXF1 True
|
||||
18 i 105 105 159.2090 159.2090 0.0000 3.2520 3.2520 0.0000 FXF1 FXF1 True
|
||||
19 e 101 101 162.4610 162.4610 0.0000 6.7440 6.7440 0.0000 FXF1 FXF1 True
|
||||
20 n 110 110 169.2050 169.2050 0.0000 7.2480 7.2480 0.0000 FXF1 FXF1 True
|
||||
21 c 99 99 176.4530 176.4530 0.0000 6.7440 6.7440 0.0000 FXF1 FXF1 True
|
||||
22 e 101 101 183.1970 183.1970 0.0000 5.8440 5.8440 0.0000 FXF1 FXF1 False
|
||||
|
||||
+++ typed glyph[23] char='x' x=189.04099 adv=6.4920 gid=120 font=FXF1
|
||||
|
||||
=== FIRST GLYPH THAT CHANGES (before → after keystroke) ===
|
||||
{
|
||||
"index": 22,
|
||||
"char": "e",
|
||||
"reasons": [
|
||||
"outline"
|
||||
],
|
||||
"xB": 183.19699,
|
||||
"xA": 183.19699,
|
||||
"dx": 0.0,
|
||||
"advB": 5.843994140625,
|
||||
"advA": 5.843994140625,
|
||||
"dAdv": 0.0,
|
||||
"gidB": 101,
|
||||
"gidA": 101,
|
||||
"fontB": "FXF1",
|
||||
"fontA": "FXF1",
|
||||
"fontInfoB": {
|
||||
"res": "FXF1",
|
||||
"font_obj": 5,
|
||||
"baseFont": "BCDEEE+Arial-BoldMT",
|
||||
"fontfile2_len": 74352,
|
||||
"fontfile2_sha": "cc80da80a119a52c"
|
||||
},
|
||||
"fontInfoA": {
|
||||
"res": "FXF1",
|
||||
"font_obj": 5,
|
||||
"baseFont": "BCDEEE+Arial-BoldMT",
|
||||
"fontfile2_len": 74352,
|
||||
"fontfile2_sha": "cc80da80a119a52c"
|
||||
},
|
||||
"outlineHashB": "57b1c59a37e48a5e",
|
||||
"outlineHashA": "8ccca923245ebde0"
|
||||
}
|
||||
|
||||
Wrote C:\Users\Maskan\Desktop\pdf_editor\pdf\tests\edits\forensic_real\keystroke_report.json
|
||||
@@ -0,0 +1,625 @@
|
||||
{
|
||||
"u0000_coverage_failure": false,
|
||||
"embedded_font_after_fix": {
|
||||
"before_pdf_baseFont": "BCDEEE+Arial-BoldMT",
|
||||
"before_fontfile2_sha": "cc80da80a119a52c",
|
||||
"note": "After U+0000 fix, edit-entry emit reuses original embedded subset (no system Arial)"
|
||||
},
|
||||
"before": {
|
||||
"text": "Professional Experience",
|
||||
"adv": [
|
||||
8.003997802734375,
|
||||
4.740001678466797,
|
||||
7.247997283935547,
|
||||
3.996002197265625,
|
||||
6.7440032958984375,
|
||||
6.743995666503906,
|
||||
6.743995666503906,
|
||||
3.2519989013671875,
|
||||
7.2480010986328125,
|
||||
7.2480010986328125,
|
||||
6.7440032958984375,
|
||||
3.2519989013671875,
|
||||
3.2519989013671875,
|
||||
8.003997802734375,
|
||||
6.73199462890625,
|
||||
7.2480010986328125,
|
||||
6.7440032958984375,
|
||||
4.739990234375,
|
||||
3.2519989013671875,
|
||||
6.7440032958984375,
|
||||
7.24798583984375,
|
||||
6.7440185546875,
|
||||
5.843994140625
|
||||
],
|
||||
"x0": 50.525001525878906,
|
||||
"runPerChar": 1,
|
||||
"emitMode": "per-char",
|
||||
"emits": [
|
||||
{
|
||||
"text": "P",
|
||||
"fontPtr": "0x1741fa4b9e0",
|
||||
"measureFacePtr": "0x1741de6e630",
|
||||
"atX": 50.525,
|
||||
"runPerChar": 1
|
||||
},
|
||||
{
|
||||
"text": "r",
|
||||
"fontPtr": "0x1741fa4b9e0",
|
||||
"measureFacePtr": "0x1741de6e630",
|
||||
"atX": 58.529,
|
||||
"runPerChar": 1
|
||||
},
|
||||
{
|
||||
"text": "o",
|
||||
"fontPtr": "0x1741fa4b9e0",
|
||||
"measureFacePtr": "0x1741de6e630",
|
||||
"atX": 63.269,
|
||||
"runPerChar": 1
|
||||
},
|
||||
{
|
||||
"text": "f",
|
||||
"fontPtr": "0x1741fa4b9e0",
|
||||
"measureFacePtr": "0x1741de6e630",
|
||||
"atX": 70.517,
|
||||
"runPerChar": 1
|
||||
},
|
||||
{
|
||||
"text": "e",
|
||||
"fontPtr": "0x1741fa4b9e0",
|
||||
"measureFacePtr": "0x1741de6e630",
|
||||
"atX": 74.513,
|
||||
"runPerChar": 1
|
||||
},
|
||||
{
|
||||
"text": "s",
|
||||
"fontPtr": "0x1741fa4b9e0",
|
||||
"measureFacePtr": "0x1741de6e630",
|
||||
"atX": 81.257,
|
||||
"runPerChar": 1
|
||||
},
|
||||
{
|
||||
"text": "s",
|
||||
"fontPtr": "0x1741fa4b9e0",
|
||||
"measureFacePtr": "0x1741de6e630",
|
||||
"atX": 88.001,
|
||||
"runPerChar": 1
|
||||
},
|
||||
{
|
||||
"text": "i",
|
||||
"fontPtr": "0x1741fa4b9e0",
|
||||
"measureFacePtr": "0x1741de6e630",
|
||||
"atX": 94.745,
|
||||
"runPerChar": 1
|
||||
},
|
||||
{
|
||||
"text": "o",
|
||||
"fontPtr": "0x1741fa4b9e0",
|
||||
"measureFacePtr": "0x1741de6e630",
|
||||
"atX": 97.997,
|
||||
"runPerChar": 1
|
||||
},
|
||||
{
|
||||
"text": "n",
|
||||
"fontPtr": "0x1741fa4b9e0",
|
||||
"measureFacePtr": "0x1741de6e630",
|
||||
"atX": 105.245,
|
||||
"runPerChar": 1
|
||||
},
|
||||
{
|
||||
"text": "a",
|
||||
"fontPtr": "0x1741fa4b9e0",
|
||||
"measureFacePtr": "0x1741de6e630",
|
||||
"atX": 112.493,
|
||||
"runPerChar": 1
|
||||
},
|
||||
{
|
||||
"text": "l",
|
||||
"fontPtr": "0x1741fa4b9e0",
|
||||
"measureFacePtr": "0x1741de6e630",
|
||||
"atX": 119.237,
|
||||
"runPerChar": 1
|
||||
},
|
||||
{
|
||||
"text": "E",
|
||||
"fontPtr": "0x1741fa4b9e0",
|
||||
"measureFacePtr": "0x1741de6e630",
|
||||
"atX": 125.741,
|
||||
"runPerChar": 1
|
||||
},
|
||||
{
|
||||
"text": "x",
|
||||
"fontPtr": "0x1741fa4b9e0",
|
||||
"measureFacePtr": "0x1741de6e630",
|
||||
"atX": 133.745,
|
||||
"runPerChar": 1
|
||||
},
|
||||
{
|
||||
"text": "p",
|
||||
"fontPtr": "0x1741fa4b9e0",
|
||||
"measureFacePtr": "0x1741de6e630",
|
||||
"atX": 140.477,
|
||||
"runPerChar": 1
|
||||
},
|
||||
{
|
||||
"text": "e",
|
||||
"fontPtr": "0x1741fa4b9e0",
|
||||
"measureFacePtr": "0x1741de6e630",
|
||||
"atX": 147.725,
|
||||
"runPerChar": 1
|
||||
},
|
||||
{
|
||||
"text": "r",
|
||||
"fontPtr": "0x1741fa4b9e0",
|
||||
"measureFacePtr": "0x1741de6e630",
|
||||
"atX": 154.469,
|
||||
"runPerChar": 1
|
||||
},
|
||||
{
|
||||
"text": "i",
|
||||
"fontPtr": "0x1741fa4b9e0",
|
||||
"measureFacePtr": "0x1741de6e630",
|
||||
"atX": 159.209,
|
||||
"runPerChar": 1
|
||||
},
|
||||
{
|
||||
"text": "e",
|
||||
"fontPtr": "0x1741fa4b9e0",
|
||||
"measureFacePtr": "0x1741de6e630",
|
||||
"atX": 162.461,
|
||||
"runPerChar": 1
|
||||
},
|
||||
{
|
||||
"text": "n",
|
||||
"fontPtr": "0x1741fa4b9e0",
|
||||
"measureFacePtr": "0x1741de6e630",
|
||||
"atX": 169.205,
|
||||
"runPerChar": 1
|
||||
},
|
||||
{
|
||||
"text": "c",
|
||||
"fontPtr": "0x1741fa4b9e0",
|
||||
"measureFacePtr": "0x1741de6e630",
|
||||
"atX": 176.453,
|
||||
"runPerChar": 1
|
||||
},
|
||||
{
|
||||
"text": "e",
|
||||
"fontPtr": "0x1741fa4b9e0",
|
||||
"measureFacePtr": "0x1741de6e630",
|
||||
"atX": 183.197,
|
||||
"runPerChar": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
"after": {
|
||||
"text": "Professional Experiencex",
|
||||
"adv": [
|
||||
8.003997802734375,
|
||||
4.740001678466797,
|
||||
7.247997283935547,
|
||||
3.996002197265625,
|
||||
6.7440032958984375,
|
||||
6.743995666503906,
|
||||
6.743995666503906,
|
||||
3.2519989013671875,
|
||||
7.2480010986328125,
|
||||
7.2480010986328125,
|
||||
6.7440032958984375,
|
||||
3.2519989013671875,
|
||||
3.2519989013671875,
|
||||
8.003997802734375,
|
||||
6.73199462890625,
|
||||
7.2480010986328125,
|
||||
6.7440032958984375,
|
||||
4.739990234375,
|
||||
3.2519989013671875,
|
||||
6.7440032958984375,
|
||||
7.24798583984375,
|
||||
6.7440185546875,
|
||||
5.843994140625,
|
||||
6.673875
|
||||
],
|
||||
"x0": 50.525001525878906,
|
||||
"runPerChar": 0,
|
||||
"emitMode": "whole-word",
|
||||
"emits": [
|
||||
{
|
||||
"text": "P",
|
||||
"fontPtr": "0x1741fb1cb40",
|
||||
"measureFacePtr": "0x1741de6c050",
|
||||
"atX": 50.525,
|
||||
"runPerChar": 1
|
||||
},
|
||||
{
|
||||
"text": "r",
|
||||
"fontPtr": "0x1741fb1cb40",
|
||||
"measureFacePtr": "0x1741de6c050",
|
||||
"atX": 58.529,
|
||||
"runPerChar": 1
|
||||
},
|
||||
{
|
||||
"text": "o",
|
||||
"fontPtr": "0x1741fb1cb40",
|
||||
"measureFacePtr": "0x1741de6c050",
|
||||
"atX": 63.269,
|
||||
"runPerChar": 1
|
||||
},
|
||||
{
|
||||
"text": "f",
|
||||
"fontPtr": "0x1741fb1cb40",
|
||||
"measureFacePtr": "0x1741de6c050",
|
||||
"atX": 70.517,
|
||||
"runPerChar": 1
|
||||
},
|
||||
{
|
||||
"text": "e",
|
||||
"fontPtr": "0x1741fb1cb40",
|
||||
"measureFacePtr": "0x1741de6c050",
|
||||
"atX": 74.513,
|
||||
"runPerChar": 1
|
||||
},
|
||||
{
|
||||
"text": "s",
|
||||
"fontPtr": "0x1741fb1cb40",
|
||||
"measureFacePtr": "0x1741de6c050",
|
||||
"atX": 81.257,
|
||||
"runPerChar": 1
|
||||
},
|
||||
{
|
||||
"text": "s",
|
||||
"fontPtr": "0x1741fb1cb40",
|
||||
"measureFacePtr": "0x1741de6c050",
|
||||
"atX": 88.001,
|
||||
"runPerChar": 1
|
||||
},
|
||||
{
|
||||
"text": "i",
|
||||
"fontPtr": "0x1741fb1cb40",
|
||||
"measureFacePtr": "0x1741de6c050",
|
||||
"atX": 94.745,
|
||||
"runPerChar": 1
|
||||
},
|
||||
{
|
||||
"text": "o",
|
||||
"fontPtr": "0x1741fb1cb40",
|
||||
"measureFacePtr": "0x1741de6c050",
|
||||
"atX": 97.997,
|
||||
"runPerChar": 1
|
||||
},
|
||||
{
|
||||
"text": "n",
|
||||
"fontPtr": "0x1741fb1cb40",
|
||||
"measureFacePtr": "0x1741de6c050",
|
||||
"atX": 105.245,
|
||||
"runPerChar": 1
|
||||
},
|
||||
{
|
||||
"text": "a",
|
||||
"fontPtr": "0x1741fb1cb40",
|
||||
"measureFacePtr": "0x1741de6c050",
|
||||
"atX": 112.493,
|
||||
"runPerChar": 1
|
||||
},
|
||||
{
|
||||
"text": "l",
|
||||
"fontPtr": "0x1741fb1cb40",
|
||||
"measureFacePtr": "0x1741de6c050",
|
||||
"atX": 119.237,
|
||||
"runPerChar": 1
|
||||
},
|
||||
{
|
||||
"text": "E",
|
||||
"fontPtr": "0x1741fb1cb40",
|
||||
"measureFacePtr": "0x1741de6c050",
|
||||
"atX": 125.741,
|
||||
"runPerChar": 1
|
||||
},
|
||||
{
|
||||
"text": "x",
|
||||
"fontPtr": "0x1741fb1cb40",
|
||||
"measureFacePtr": "0x1741de6c050",
|
||||
"atX": 133.745,
|
||||
"runPerChar": 1
|
||||
},
|
||||
{
|
||||
"text": "p",
|
||||
"fontPtr": "0x1741fb1cb40",
|
||||
"measureFacePtr": "0x1741de6c050",
|
||||
"atX": 140.477,
|
||||
"runPerChar": 1
|
||||
},
|
||||
{
|
||||
"text": "e",
|
||||
"fontPtr": "0x1741fb1cb40",
|
||||
"measureFacePtr": "0x1741de6c050",
|
||||
"atX": 147.725,
|
||||
"runPerChar": 1
|
||||
},
|
||||
{
|
||||
"text": "r",
|
||||
"fontPtr": "0x1741fb1cb40",
|
||||
"measureFacePtr": "0x1741de6c050",
|
||||
"atX": 154.469,
|
||||
"runPerChar": 1
|
||||
},
|
||||
{
|
||||
"text": "i",
|
||||
"fontPtr": "0x1741fb1cb40",
|
||||
"measureFacePtr": "0x1741de6c050",
|
||||
"atX": 159.209,
|
||||
"runPerChar": 1
|
||||
},
|
||||
{
|
||||
"text": "e",
|
||||
"fontPtr": "0x1741fb1cb40",
|
||||
"measureFacePtr": "0x1741de6c050",
|
||||
"atX": 162.461,
|
||||
"runPerChar": 1
|
||||
},
|
||||
{
|
||||
"text": "n",
|
||||
"fontPtr": "0x1741fb1cb40",
|
||||
"measureFacePtr": "0x1741de6c050",
|
||||
"atX": 169.205,
|
||||
"runPerChar": 1
|
||||
},
|
||||
{
|
||||
"text": "c",
|
||||
"fontPtr": "0x1741fb1cb40",
|
||||
"measureFacePtr": "0x1741de6c050",
|
||||
"atX": 176.453,
|
||||
"runPerChar": 1
|
||||
},
|
||||
{
|
||||
"text": "e",
|
||||
"fontPtr": "0x1741fb1cb40",
|
||||
"measureFacePtr": "0x1741de6c050",
|
||||
"atX": 183.197,
|
||||
"runPerChar": 1
|
||||
},
|
||||
{
|
||||
"text": "x",
|
||||
"fontPtr": "0x1741fb1cb40",
|
||||
"measureFacePtr": "0x1741de6c050",
|
||||
"atX": 189.041,
|
||||
"runPerChar": 1
|
||||
}
|
||||
],
|
||||
"note": "advLen=0 -> recomputed HarfBuzz advances; runPerChar=0"
|
||||
},
|
||||
"first_diff": null,
|
||||
"glyphs": [
|
||||
{
|
||||
"i": 0,
|
||||
"char": "P",
|
||||
"xB": 50.525001525878906,
|
||||
"xA": 50.525001525878906,
|
||||
"dx": 0.0,
|
||||
"advB": 8.003997802734375,
|
||||
"advA": 8.003997802734375,
|
||||
"dAdv": 0.0
|
||||
},
|
||||
{
|
||||
"i": 1,
|
||||
"char": "r",
|
||||
"xB": 58.52899932861328,
|
||||
"xA": 58.52899932861328,
|
||||
"dx": 0.0,
|
||||
"advB": 4.740001678466797,
|
||||
"advA": 4.740001678466797,
|
||||
"dAdv": 0.0
|
||||
},
|
||||
{
|
||||
"i": 2,
|
||||
"char": "o",
|
||||
"xB": 63.26900100708008,
|
||||
"xA": 63.26900100708008,
|
||||
"dx": 0.0,
|
||||
"advB": 7.247997283935547,
|
||||
"advA": 7.247997283935547,
|
||||
"dAdv": 0.0
|
||||
},
|
||||
{
|
||||
"i": 3,
|
||||
"char": "f",
|
||||
"xB": 70.51699829101562,
|
||||
"xA": 70.51699829101562,
|
||||
"dx": 0.0,
|
||||
"advB": 3.996002197265625,
|
||||
"advA": 3.996002197265625,
|
||||
"dAdv": 0.0
|
||||
},
|
||||
{
|
||||
"i": 4,
|
||||
"char": "e",
|
||||
"xB": 74.51300048828125,
|
||||
"xA": 74.51300048828125,
|
||||
"dx": 0.0,
|
||||
"advB": 6.7440032958984375,
|
||||
"advA": 6.7440032958984375,
|
||||
"dAdv": 0.0
|
||||
},
|
||||
{
|
||||
"i": 5,
|
||||
"char": "s",
|
||||
"xB": 81.25700378417969,
|
||||
"xA": 81.25700378417969,
|
||||
"dx": 0.0,
|
||||
"advB": 6.743995666503906,
|
||||
"advA": 6.743995666503906,
|
||||
"dAdv": 0.0
|
||||
},
|
||||
{
|
||||
"i": 6,
|
||||
"char": "s",
|
||||
"xB": 88.0009994506836,
|
||||
"xA": 88.0009994506836,
|
||||
"dx": 0.0,
|
||||
"advB": 6.743995666503906,
|
||||
"advA": 6.743995666503906,
|
||||
"dAdv": 0.0
|
||||
},
|
||||
{
|
||||
"i": 7,
|
||||
"char": "i",
|
||||
"xB": 94.7449951171875,
|
||||
"xA": 94.7449951171875,
|
||||
"dx": 0.0,
|
||||
"advB": 3.2519989013671875,
|
||||
"advA": 3.2519989013671875,
|
||||
"dAdv": 0.0
|
||||
},
|
||||
{
|
||||
"i": 8,
|
||||
"char": "o",
|
||||
"xB": 97.99699401855469,
|
||||
"xA": 97.99699401855469,
|
||||
"dx": 0.0,
|
||||
"advB": 7.2480010986328125,
|
||||
"advA": 7.2480010986328125,
|
||||
"dAdv": 0.0
|
||||
},
|
||||
{
|
||||
"i": 9,
|
||||
"char": "n",
|
||||
"xB": 105.2449951171875,
|
||||
"xA": 105.2449951171875,
|
||||
"dx": 0.0,
|
||||
"advB": 7.2480010986328125,
|
||||
"advA": 7.2480010986328125,
|
||||
"dAdv": 0.0
|
||||
},
|
||||
{
|
||||
"i": 10,
|
||||
"char": "a",
|
||||
"xB": 112.49299621582031,
|
||||
"xA": 112.49299621582031,
|
||||
"dx": 0.0,
|
||||
"advB": 6.7440032958984375,
|
||||
"advA": 6.7440032958984375,
|
||||
"dAdv": 0.0
|
||||
},
|
||||
{
|
||||
"i": 11,
|
||||
"char": "l",
|
||||
"xB": 119.23699951171875,
|
||||
"xA": 119.23699951171875,
|
||||
"dx": 0.0,
|
||||
"advB": 3.2519989013671875,
|
||||
"advA": 3.2519989013671875,
|
||||
"dAdv": 0.0
|
||||
},
|
||||
{
|
||||
"i": 12,
|
||||
"char": " ",
|
||||
"xB": 122.48899841308594,
|
||||
"xA": 122.48899841308594,
|
||||
"dx": 0.0,
|
||||
"advB": 3.2519989013671875,
|
||||
"advA": 3.2519989013671875,
|
||||
"dAdv": 0.0
|
||||
},
|
||||
{
|
||||
"i": 13,
|
||||
"char": "E",
|
||||
"xB": 125.74099731445312,
|
||||
"xA": 125.74099731445312,
|
||||
"dx": 0.0,
|
||||
"advB": 8.003997802734375,
|
||||
"advA": 8.003997802734375,
|
||||
"dAdv": 0.0
|
||||
},
|
||||
{
|
||||
"i": 14,
|
||||
"char": "x",
|
||||
"xB": 133.7449951171875,
|
||||
"xA": 133.7449951171875,
|
||||
"dx": 0.0,
|
||||
"advB": 6.73199462890625,
|
||||
"advA": 6.73199462890625,
|
||||
"dAdv": 0.0
|
||||
},
|
||||
{
|
||||
"i": 15,
|
||||
"char": "p",
|
||||
"xB": 140.47698974609375,
|
||||
"xA": 140.47698974609375,
|
||||
"dx": 0.0,
|
||||
"advB": 7.2480010986328125,
|
||||
"advA": 7.2480010986328125,
|
||||
"dAdv": 0.0
|
||||
},
|
||||
{
|
||||
"i": 16,
|
||||
"char": "e",
|
||||
"xB": 147.72499084472656,
|
||||
"xA": 147.72499084472656,
|
||||
"dx": 0.0,
|
||||
"advB": 6.7440032958984375,
|
||||
"advA": 6.7440032958984375,
|
||||
"dAdv": 0.0
|
||||
},
|
||||
{
|
||||
"i": 17,
|
||||
"char": "r",
|
||||
"xB": 154.468994140625,
|
||||
"xA": 154.468994140625,
|
||||
"dx": 0.0,
|
||||
"advB": 4.739990234375,
|
||||
"advA": 4.739990234375,
|
||||
"dAdv": 0.0
|
||||
},
|
||||
{
|
||||
"i": 18,
|
||||
"char": "i",
|
||||
"xB": 159.208984375,
|
||||
"xA": 159.208984375,
|
||||
"dx": 0.0,
|
||||
"advB": 3.2519989013671875,
|
||||
"advA": 3.2519989013671875,
|
||||
"dAdv": 0.0
|
||||
},
|
||||
{
|
||||
"i": 19,
|
||||
"char": "e",
|
||||
"xB": 162.4609832763672,
|
||||
"xA": 162.4609832763672,
|
||||
"dx": 0.0,
|
||||
"advB": 6.7440032958984375,
|
||||
"advA": 6.7440032958984375,
|
||||
"dAdv": 0.0
|
||||
},
|
||||
{
|
||||
"i": 20,
|
||||
"char": "n",
|
||||
"xB": 169.20498657226562,
|
||||
"xA": 169.20498657226562,
|
||||
"dx": 0.0,
|
||||
"advB": 7.24798583984375,
|
||||
"advA": 7.24798583984375,
|
||||
"dAdv": 0.0
|
||||
},
|
||||
{
|
||||
"i": 21,
|
||||
"char": "c",
|
||||
"xB": 176.45297241210938,
|
||||
"xA": 176.45297241210938,
|
||||
"dx": 0.0,
|
||||
"advB": 6.7440185546875,
|
||||
"advA": 6.7440185546875,
|
||||
"dAdv": 0.0
|
||||
},
|
||||
{
|
||||
"i": 22,
|
||||
"char": "e",
|
||||
"xB": 183.19699096679688,
|
||||
"xA": 183.19699096679688,
|
||||
"dx": 0.0,
|
||||
"advB": 5.843994140625,
|
||||
"advA": 5.843994140625,
|
||||
"dAdv": 0.0
|
||||
}
|
||||
],
|
||||
"interpretation": "First keystroke drops client advances (length mismatch) and origLines. Engine recomputes natural HarfBuzz advances (no original TJ kerning). First mutation is advance of glyph index 1 ('r'): client/kerned ~4.740 -> natural ~4.670. Positions of all subsequent glyphs diverge from that point."
|
||||
}
|
||||
|
After Width: | Height: | Size: 619 B |
|
After Width: | Height: | Size: 619 B |
|
After Width: | Height: | Size: 915 B |
|
After Width: | Height: | Size: 915 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 801 B |
|
After Width: | Height: | Size: 801 B |
@@ -0,0 +1,97 @@
|
||||
{
|
||||
"version": "1.0",
|
||||
"operations": [
|
||||
{
|
||||
"id": "entry",
|
||||
"type": "reflow_paragraph",
|
||||
"pageIndex": 0,
|
||||
"data": {
|
||||
"objectIndices": [
|
||||
30
|
||||
],
|
||||
"runs": [
|
||||
{
|
||||
"text": "Professional Experience",
|
||||
"internalFontId": "Arial-BoldMT_TrueType_32",
|
||||
"fontSize": 12.0,
|
||||
"color": "#1a5276",
|
||||
"advances": [
|
||||
8.003997802734375,
|
||||
4.740001678466797,
|
||||
7.247997283935547,
|
||||
3.996002197265625,
|
||||
6.7440032958984375,
|
||||
6.743995666503906,
|
||||
6.743995666503906,
|
||||
3.2519989013671875,
|
||||
7.2480010986328125,
|
||||
7.2480010986328125,
|
||||
6.7440032958984375,
|
||||
3.2519989013671875,
|
||||
3.2519989013671875,
|
||||
8.003997802734375,
|
||||
6.73199462890625,
|
||||
7.2480010986328125,
|
||||
6.7440032958984375,
|
||||
4.739990234375,
|
||||
3.2519989013671875,
|
||||
6.7440032958984375,
|
||||
7.24798583984375,
|
||||
6.7440185546875,
|
||||
5.843994140625
|
||||
]
|
||||
}
|
||||
],
|
||||
"lineX": [
|
||||
50.525001525878906
|
||||
],
|
||||
"lineBaselineY": [
|
||||
597.4500122070312
|
||||
],
|
||||
"lines": [
|
||||
[
|
||||
{
|
||||
"text": "Professional Experience",
|
||||
"internalFontId": "Arial-BoldMT_TrueType_32",
|
||||
"fontSize": 12.0,
|
||||
"color": "#1a5276",
|
||||
"advances": [
|
||||
8.003997802734375,
|
||||
4.740001678466797,
|
||||
7.247997283935547,
|
||||
3.996002197265625,
|
||||
6.7440032958984375,
|
||||
6.743995666503906,
|
||||
6.743995666503906,
|
||||
3.2519989013671875,
|
||||
7.2480010986328125,
|
||||
7.2480010986328125,
|
||||
6.7440032958984375,
|
||||
3.2519989013671875,
|
||||
3.2519989013671875,
|
||||
8.003997802734375,
|
||||
6.73199462890625,
|
||||
7.2480010986328125,
|
||||
6.7440032958984375,
|
||||
4.739990234375,
|
||||
3.2519989013671875,
|
||||
6.7440032958984375,
|
||||
7.24798583984375,
|
||||
6.7440185546875,
|
||||
5.843994140625
|
||||
]
|
||||
}
|
||||
]
|
||||
],
|
||||
"columnLeft": 51.4010009765625,
|
||||
"columnRight": 301.4010009765625,
|
||||
"pushColumnLeft": 51.4010009765625,
|
||||
"firstBaselineY": 597.4500122070312,
|
||||
"leading": 14.399999999999999,
|
||||
"oldLineCount": 1,
|
||||
"align": "left",
|
||||
"paraId": "forensic-real-profexp"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,489 @@
|
||||
{
|
||||
"glyphs": {
|
||||
"P": {
|
||||
"orig_box": {
|
||||
"origin_x": 50.525001525878906,
|
||||
"origin_y": 597.4500122070312,
|
||||
"bbox_x": 51.4010009765625,
|
||||
"bbox_y": 597.4500122070312,
|
||||
"bbox_w": 6.576000213623047,
|
||||
"bbox_h": 8.59197998046875,
|
||||
"font_size": 12.0,
|
||||
"font_name": "Arial-BoldMT"
|
||||
},
|
||||
"prev_box": {
|
||||
"origin_x": 50.525001525878906,
|
||||
"origin_y": 597.4500122070312,
|
||||
"bbox_x": 51.4010009765625,
|
||||
"bbox_y": 597.4500122070312,
|
||||
"bbox_w": 6.576000213623047,
|
||||
"bbox_h": 8.7239990234375,
|
||||
"font_size": 12.0,
|
||||
"font_name": "Arial-BoldMT"
|
||||
},
|
||||
"orig_sha": "5509fb6f5ab2f288ce08a4bc476a60e7709325322ce342de3f86377241f03188",
|
||||
"prev_sha": "5509fb6f5ab2f288ce08a4bc476a60e7709325322ce342de3f86377241f03188",
|
||||
"pixel_identical": true,
|
||||
"metrics": {
|
||||
"bbox_w": [
|
||||
6.576000213623047,
|
||||
6.576000213623047
|
||||
],
|
||||
"bbox_h": [
|
||||
8.59197998046875,
|
||||
8.7239990234375
|
||||
],
|
||||
"font_size": [
|
||||
12.0,
|
||||
12.0
|
||||
],
|
||||
"font_name": [
|
||||
"Arial-BoldMT",
|
||||
"Arial-BoldMT"
|
||||
]
|
||||
}
|
||||
},
|
||||
"r": {
|
||||
"orig_box": {
|
||||
"origin_x": 58.52899932861328,
|
||||
"origin_y": 597.4500122070312,
|
||||
"bbox_x": 59.32099914550781,
|
||||
"bbox_y": 597.4500122070312,
|
||||
"bbox_w": 4.032001495361328,
|
||||
"bbox_h": 6.3599853515625,
|
||||
"font_size": 12.0,
|
||||
"font_name": "Arial-BoldMT"
|
||||
},
|
||||
"prev_box": {
|
||||
"origin_x": 58.52899932861328,
|
||||
"origin_y": 597.4500122070312,
|
||||
"bbox_x": 59.32099914550781,
|
||||
"bbox_y": 597.4500122070312,
|
||||
"bbox_w": 4.032001495361328,
|
||||
"bbox_h": 6.45599365234375,
|
||||
"font_size": 12.0,
|
||||
"font_name": "Arial-BoldMT"
|
||||
},
|
||||
"orig_sha": "0b375d83fb5ccc6ef05cdeaba4913ee899283d3252eb3b29396f8c4b493d9f73",
|
||||
"prev_sha": "0b375d83fb5ccc6ef05cdeaba4913ee899283d3252eb3b29396f8c4b493d9f73",
|
||||
"pixel_identical": true,
|
||||
"metrics": {
|
||||
"bbox_w": [
|
||||
4.032001495361328,
|
||||
4.032001495361328
|
||||
],
|
||||
"bbox_h": [
|
||||
6.3599853515625,
|
||||
6.45599365234375
|
||||
],
|
||||
"font_size": [
|
||||
12.0,
|
||||
12.0
|
||||
],
|
||||
"font_name": [
|
||||
"Arial-BoldMT",
|
||||
"Arial-BoldMT"
|
||||
]
|
||||
}
|
||||
},
|
||||
"o": {
|
||||
"orig_box": {
|
||||
"origin_x": 63.26900100708008,
|
||||
"origin_y": 597.4500122070312,
|
||||
"bbox_x": 63.749000549316406,
|
||||
"bbox_y": 597.3179931640625,
|
||||
"bbox_w": 6.4199981689453125,
|
||||
"bbox_h": 6.49200439453125,
|
||||
"font_size": 12.0,
|
||||
"font_name": "Arial-BoldMT"
|
||||
},
|
||||
"prev_box": {
|
||||
"origin_x": 63.26900100708008,
|
||||
"origin_y": 597.4500122070312,
|
||||
"bbox_x": 63.749000549316406,
|
||||
"bbox_y": 597.3179931640625,
|
||||
"bbox_w": 6.4199981689453125,
|
||||
"bbox_h": 6.5880126953125,
|
||||
"font_size": 12.0,
|
||||
"font_name": "Arial-BoldMT"
|
||||
},
|
||||
"orig_sha": "b16564653edc955a9580561fc0c68bb4ddf2325fa5517a4208185719cc99e2da",
|
||||
"prev_sha": "17b4a318b0f7da566bbd8abb30bd34ee73bcd3932cf49eae4728098c4d0ab1b7",
|
||||
"pixel_identical": false,
|
||||
"metrics": {
|
||||
"bbox_w": [
|
||||
6.4199981689453125,
|
||||
6.4199981689453125
|
||||
],
|
||||
"bbox_h": [
|
||||
6.49200439453125,
|
||||
6.5880126953125
|
||||
],
|
||||
"font_size": [
|
||||
12.0,
|
||||
12.0
|
||||
],
|
||||
"font_name": [
|
||||
"Arial-BoldMT",
|
||||
"Arial-BoldMT"
|
||||
]
|
||||
}
|
||||
},
|
||||
"f": {
|
||||
"orig_box": {
|
||||
"origin_x": 70.51699829101562,
|
||||
"origin_y": 597.4500122070312,
|
||||
"bbox_x": 70.66100311279297,
|
||||
"bbox_y": 597.4500122070312,
|
||||
"bbox_w": 4.1999969482421875,
|
||||
"bbox_h": 8.73602294921875,
|
||||
"font_size": 12.0,
|
||||
"font_name": "Arial-BoldMT"
|
||||
},
|
||||
"prev_box": {
|
||||
"origin_x": 70.51699829101562,
|
||||
"origin_y": 597.4500122070312,
|
||||
"bbox_x": 70.66099548339844,
|
||||
"bbox_y": 597.4500122070312,
|
||||
"bbox_w": 4.200004577636719,
|
||||
"bbox_h": 8.86798095703125,
|
||||
"font_size": 12.0,
|
||||
"font_name": "Arial-BoldMT"
|
||||
},
|
||||
"orig_sha": "d7260b020c4de28a5bf1886a927d40be18c8b0dd1681f32f429b8bacd4ef212e",
|
||||
"prev_sha": "cfb459ab8b6ea3f8be862f4d02615f211c52d242b924531ba17cc582aa258944",
|
||||
"pixel_identical": false,
|
||||
"metrics": {
|
||||
"bbox_w": [
|
||||
4.1999969482421875,
|
||||
4.200004577636719
|
||||
],
|
||||
"bbox_h": [
|
||||
8.73602294921875,
|
||||
8.86798095703125
|
||||
],
|
||||
"font_size": [
|
||||
12.0,
|
||||
12.0
|
||||
],
|
||||
"font_name": [
|
||||
"Arial-BoldMT",
|
||||
"Arial-BoldMT"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"font": {
|
||||
"api_font_sha": "cc80da80a119a52c47a15b4a598882b15f0344b56b3457a4fe41e2ac40843707",
|
||||
"api_recon_sha": null,
|
||||
"shared_programs": [
|
||||
"cc80da80a119a52c47a15b4a598882b15f0344b56b3457a4fe41e2ac40843707",
|
||||
"4f617da732cd3bac738f97df3ab8e9b31dead249284d07e0126e7de8cba87f86",
|
||||
"9f73acd5011644c9714baea6348c4f85edffd706da2820dfd6a609f7ad977cf1",
|
||||
"9ba4cbb2efb0398eb9be6e505af160eef8b3f74f7bc9c43c007794d1d0b77908"
|
||||
],
|
||||
"orig_programs": [
|
||||
{
|
||||
"obj": "4",
|
||||
"kind": "binary_blob",
|
||||
"rawLen": 6054,
|
||||
"dataLen": 35086,
|
||||
"sha256": "4e5ed2e4dc94c2e653d1844ab038a5b7a2d6e71a6cd465630e038fb2b5a70fb5",
|
||||
"head": "202f5370616e203c3c2f4d4349442030"
|
||||
},
|
||||
{
|
||||
"obj": "31",
|
||||
"kind": "binary_blob",
|
||||
"rawLen": 2483,
|
||||
"dataLen": 12496,
|
||||
"sha256": "9ba4cbb2efb0398eb9be6e505af160eef8b3f74f7bc9c43c007794d1d0b77908",
|
||||
"head": "202f5370616e203c3c2f4d4349442030"
|
||||
},
|
||||
{
|
||||
"obj": "40",
|
||||
"kind": "binary_blob",
|
||||
"rawLen": 1386,
|
||||
"dataLen": 6430,
|
||||
"sha256": "c3b87d20212bd1dd4fdbd9c763b95c099512aab7076f37fdbece1deca0001a0a",
|
||||
"head": "33382030203433203531203434203837"
|
||||
},
|
||||
{
|
||||
"obj": "126",
|
||||
"kind": "sfnt",
|
||||
"rawLen": 29798,
|
||||
"dataLen": 74352,
|
||||
"sha256": "cc80da80a119a52c47a15b4a598882b15f0344b56b3457a4fe41e2ac40843707",
|
||||
"head": "00010000000e0100000400804f532f32"
|
||||
},
|
||||
{
|
||||
"obj": "130",
|
||||
"kind": "sfnt",
|
||||
"rawLen": 45540,
|
||||
"dataLen": 103052,
|
||||
"sha256": "4f617da732cd3bac738f97df3ab8e9b31dead249284d07e0126e7de8cba87f86",
|
||||
"head": "00010000000e0100000400804f532f32"
|
||||
},
|
||||
{
|
||||
"obj": "134",
|
||||
"kind": "sfnt",
|
||||
"rawLen": 23946,
|
||||
"dataLen": 59492,
|
||||
"sha256": "9f73acd5011644c9714baea6348c4f85edffd706da2820dfd6a609f7ad977cf1",
|
||||
"head": "00010000000e0100000400704f532f32"
|
||||
}
|
||||
],
|
||||
"prev_programs": [
|
||||
{
|
||||
"obj": "31",
|
||||
"kind": "binary_blob",
|
||||
"rawLen": 2483,
|
||||
"dataLen": 12496,
|
||||
"sha256": "9ba4cbb2efb0398eb9be6e505af160eef8b3f74f7bc9c43c007794d1d0b77908",
|
||||
"head": "202f5370616e203c3c2f4d4349442030"
|
||||
},
|
||||
{
|
||||
"obj": "126",
|
||||
"kind": "sfnt",
|
||||
"rawLen": 29798,
|
||||
"dataLen": 74352,
|
||||
"sha256": "cc80da80a119a52c47a15b4a598882b15f0344b56b3457a4fe41e2ac40843707",
|
||||
"head": "00010000000e0100000400804f532f32"
|
||||
},
|
||||
{
|
||||
"obj": "130",
|
||||
"kind": "sfnt",
|
||||
"rawLen": 45540,
|
||||
"dataLen": 103052,
|
||||
"sha256": "4f617da732cd3bac738f97df3ab8e9b31dead249284d07e0126e7de8cba87f86",
|
||||
"head": "00010000000e0100000400804f532f32"
|
||||
},
|
||||
{
|
||||
"obj": "134",
|
||||
"kind": "sfnt",
|
||||
"rawLen": 23946,
|
||||
"dataLen": 59492,
|
||||
"sha256": "9f73acd5011644c9714baea6348c4f85edffd706da2820dfd6a609f7ad977cf1",
|
||||
"head": "00010000000e0100000400704f532f32"
|
||||
},
|
||||
{
|
||||
"obj": "137",
|
||||
"kind": "binary_blob",
|
||||
"rawLen": 3094,
|
||||
"dataLen": 3094,
|
||||
"sha256": "b24aad5aef89f3152e635dbcc45b38e02e63e675a06cd74b03141e7354228019",
|
||||
"head": "3c3f787061636b657420626567696e3d"
|
||||
},
|
||||
{
|
||||
"obj": "144",
|
||||
"kind": "sfnt",
|
||||
"rawLen": 518495,
|
||||
"dataLen": 980756,
|
||||
"sha256": "766f06ac8761f82f25d032a220e89438f6064591af9915061f20b949efdedf69",
|
||||
"head": "00010000001901000004009044534947"
|
||||
},
|
||||
{
|
||||
"obj": "146",
|
||||
"kind": "binary_blob",
|
||||
"rawLen": 6924,
|
||||
"dataLen": 19921,
|
||||
"sha256": "b64d71769a1170df7c0c7971c29d13f6f1347256fd59ba58ec5fa2c37f37ac42",
|
||||
"head": "2f434944496e6974202f50726f635365"
|
||||
}
|
||||
],
|
||||
"orig_font_dicts": [
|
||||
{
|
||||
"obj": "5",
|
||||
"baseFont": "BCDEEE+Arial-BoldMT",
|
||||
"subtype": "TrueType",
|
||||
"encoding": "WinAnsiEncoding",
|
||||
"fontFileRef": null,
|
||||
"hasFontDescriptor": true,
|
||||
"dictPreview": "<</Type/Font/Subtype/TrueType/Name/F1/BaseFont/BCDEEE+Arial-BoldMT/Encoding/WinAnsiEncoding/FontDescriptor 6 0 R/FirstChar 32/LastChar 121/Widths 128 0 R>>"
|
||||
},
|
||||
{
|
||||
"obj": "9",
|
||||
"baseFont": "BCDFEE+ArialMT",
|
||||
"subtype": "TrueType",
|
||||
"encoding": "WinAnsiEncoding",
|
||||
"fontFileRef": null,
|
||||
"hasFontDescriptor": true,
|
||||
"dictPreview": "<</Type/Font/Subtype/TrueType/Name/F2/BaseFont/BCDFEE+ArialMT/Encoding/WinAnsiEncoding/FontDescriptor 10 0 R/FirstChar 32/LastChar 124/Widths 132 0 R>>"
|
||||
},
|
||||
{
|
||||
"obj": "13",
|
||||
"baseFont": "BCDGEE+Arial-BoldMT",
|
||||
"subtype": "Type0",
|
||||
"encoding": "Identity",
|
||||
"fontFileRef": null,
|
||||
"hasFontDescriptor": false,
|
||||
"dictPreview": "<</Type/Font/Subtype/Type0/BaseFont/BCDGEE+Arial-BoldMT/Encoding/Identity-H/DescendantFonts 14 0 R/ToUnicode 125 0 R>>"
|
||||
},
|
||||
{
|
||||
"obj": "15",
|
||||
"baseFont": "BCDGEE+Arial-BoldMT",
|
||||
"subtype": "CIDFontType2",
|
||||
"encoding": null,
|
||||
"fontFileRef": null,
|
||||
"hasFontDescriptor": true,
|
||||
"dictPreview": "<</BaseFont/BCDGEE+Arial-BoldMT/Subtype/CIDFontType2/Type/Font/CIDToGIDMap/Identity/DW 1000/CIDSystemInfo 16 0 R/FontDescriptor 17 0 R/W 127 0 R>>"
|
||||
},
|
||||
{
|
||||
"obj": "18",
|
||||
"baseFont": "BCDHEE+Arial-ItalicMT",
|
||||
"subtype": "TrueType",
|
||||
"encoding": "WinAnsiEncoding",
|
||||
"fontFileRef": null,
|
||||
"hasFontDescriptor": true,
|
||||
"dictPreview": "<</Type/Font/Subtype/TrueType/Name/F4/BaseFont/BCDHEE+Arial-ItalicMT/Encoding/WinAnsiEncoding/FontDescriptor 19 0 R/FirstChar 32/LastChar 121/Widths 136 0 R>>"
|
||||
},
|
||||
{
|
||||
"obj": "20",
|
||||
"baseFont": "BCDIEE+Arial-ItalicMT",
|
||||
"subtype": "Type0",
|
||||
"encoding": "Identity",
|
||||
"fontFileRef": null,
|
||||
"hasFontDescriptor": false,
|
||||
"dictPreview": "<</Type/Font/Subtype/Type0/BaseFont/BCDIEE+Arial-ItalicMT/Encoding/Identity-H/DescendantFonts 21 0 R/ToUnicode 133 0 R>>"
|
||||
},
|
||||
{
|
||||
"obj": "22",
|
||||
"baseFont": "BCDIEE+Arial-ItalicMT",
|
||||
"subtype": "CIDFontType2",
|
||||
"encoding": null,
|
||||
"fontFileRef": null,
|
||||
"hasFontDescriptor": true,
|
||||
"dictPreview": "<</BaseFont/BCDIEE+Arial-ItalicMT/Subtype/CIDFontType2/Type/Font/CIDToGIDMap/Identity/DW 1000/CIDSystemInfo 23 0 R/FontDescriptor 24 0 R/W 135 0 R>>"
|
||||
},
|
||||
{
|
||||
"obj": "25",
|
||||
"baseFont": "BCDJEE+ArialMT",
|
||||
"subtype": "Type0",
|
||||
"encoding": "Identity",
|
||||
"fontFileRef": null,
|
||||
"hasFontDescriptor": false,
|
||||
"dictPreview": "<</Type/Font/Subtype/Type0/BaseFont/BCDJEE+ArialMT/Encoding/Identity-H/DescendantFonts 26 0 R/ToUnicode 129 0 R>>"
|
||||
},
|
||||
{
|
||||
"obj": "27",
|
||||
"baseFont": "BCDJEE+ArialMT",
|
||||
"subtype": "CIDFontType2",
|
||||
"encoding": null,
|
||||
"fontFileRef": null,
|
||||
"hasFontDescriptor": true,
|
||||
"dictPreview": "<</BaseFont/BCDJEE+ArialMT/Subtype/CIDFontType2/Type/Font/CIDToGIDMap/Identity/DW 1000/CIDSystemInfo 28 0 R/FontDescriptor 29 0 R/W 131 0 R>>"
|
||||
}
|
||||
],
|
||||
"prev_font_dicts": [
|
||||
{
|
||||
"obj": "5",
|
||||
"baseFont": "BCDEEE+Arial-BoldMT",
|
||||
"subtype": "TrueType",
|
||||
"encoding": "WinAnsiEncoding",
|
||||
"fontFileRef": null,
|
||||
"hasFontDescriptor": true,
|
||||
"dictPreview": "<</BaseFont/BCDEEE+Arial-BoldMT/Encoding/WinAnsiEncoding/FirstChar 32/FontDescriptor 6 0 R /LastChar 121/Name/F1/Subtype/TrueType/Type/Font/Widths 128 0 R >>"
|
||||
},
|
||||
{
|
||||
"obj": "9",
|
||||
"baseFont": "BCDFEE+ArialMT",
|
||||
"subtype": "TrueType",
|
||||
"encoding": "WinAnsiEncoding",
|
||||
"fontFileRef": null,
|
||||
"hasFontDescriptor": true,
|
||||
"dictPreview": "<</BaseFont/BCDFEE+ArialMT/Encoding/WinAnsiEncoding/FirstChar 32/FontDescriptor 10 0 R /LastChar 124/Name/F2/Subtype/TrueType/Type/Font/Widths 132 0 R >>"
|
||||
},
|
||||
{
|
||||
"obj": "13",
|
||||
"baseFont": "BCDGEE+Arial-BoldMT",
|
||||
"subtype": "Type0",
|
||||
"encoding": "Identity",
|
||||
"fontFileRef": null,
|
||||
"hasFontDescriptor": false,
|
||||
"dictPreview": "<</BaseFont/BCDGEE+Arial-BoldMT/DescendantFonts 14 0 R /Encoding/Identity-H/Subtype/Type0/ToUnicode 125 0 R /Type/Font>>"
|
||||
},
|
||||
{
|
||||
"obj": "15",
|
||||
"baseFont": "BCDGEE+Arial-BoldMT",
|
||||
"subtype": "CIDFontType2",
|
||||
"encoding": null,
|
||||
"fontFileRef": null,
|
||||
"hasFontDescriptor": true,
|
||||
"dictPreview": "<</BaseFont/BCDGEE+Arial-BoldMT/CIDSystemInfo 16 0 R /CIDToGIDMap/Identity/DW 1000/FontDescriptor 17 0 R /Subtype/CIDFontType2/Type/Font/W 127 0 R >>"
|
||||
},
|
||||
{
|
||||
"obj": "18",
|
||||
"baseFont": "BCDHEE+Arial-ItalicMT",
|
||||
"subtype": "TrueType",
|
||||
"encoding": "WinAnsiEncoding",
|
||||
"fontFileRef": null,
|
||||
"hasFontDescriptor": true,
|
||||
"dictPreview": "<</BaseFont/BCDHEE+Arial-ItalicMT/Encoding/WinAnsiEncoding/FirstChar 32/FontDescriptor 19 0 R /LastChar 121/Name/F4/Subtype/TrueType/Type/Font/Widths 136 0 R >>"
|
||||
},
|
||||
{
|
||||
"obj": "20",
|
||||
"baseFont": "BCDIEE+Arial-ItalicMT",
|
||||
"subtype": "Type0",
|
||||
"encoding": "Identity",
|
||||
"fontFileRef": null,
|
||||
"hasFontDescriptor": false,
|
||||
"dictPreview": "<</BaseFont/BCDIEE+Arial-ItalicMT/DescendantFonts 21 0 R /Encoding/Identity-H/Subtype/Type0/ToUnicode 133 0 R /Type/Font>>"
|
||||
},
|
||||
{
|
||||
"obj": "22",
|
||||
"baseFont": "BCDIEE+Arial-ItalicMT",
|
||||
"subtype": "CIDFontType2",
|
||||
"encoding": null,
|
||||
"fontFileRef": null,
|
||||
"hasFontDescriptor": true,
|
||||
"dictPreview": "<</BaseFont/BCDIEE+Arial-ItalicMT/CIDSystemInfo 23 0 R /CIDToGIDMap/Identity/DW 1000/FontDescriptor 24 0 R /Subtype/CIDFontType2/Type/Font/W 135 0 R >>"
|
||||
},
|
||||
{
|
||||
"obj": "25",
|
||||
"baseFont": "BCDJEE+ArialMT",
|
||||
"subtype": "Type0",
|
||||
"encoding": "Identity",
|
||||
"fontFileRef": null,
|
||||
"hasFontDescriptor": false,
|
||||
"dictPreview": "<</BaseFont/BCDJEE+ArialMT/DescendantFonts 26 0 R /Encoding/Identity-H/Subtype/Type0/ToUnicode 129 0 R /Type/Font>>"
|
||||
},
|
||||
{
|
||||
"obj": "27",
|
||||
"baseFont": "BCDJEE+ArialMT",
|
||||
"subtype": "CIDFontType2",
|
||||
"encoding": null,
|
||||
"fontFileRef": null,
|
||||
"hasFontDescriptor": true,
|
||||
"dictPreview": "<</BaseFont/BCDJEE+ArialMT/CIDSystemInfo 28 0 R /CIDToGIDMap/Identity/DW 1000/FontDescriptor 29 0 R /Subtype/CIDFontType2/Type/Font/W 131 0 R >>"
|
||||
},
|
||||
{
|
||||
"obj": "140",
|
||||
"baseFont": "Arial-BoldMT",
|
||||
"subtype": "Type0",
|
||||
"encoding": "Identity",
|
||||
"fontFileRef": null,
|
||||
"hasFontDescriptor": false,
|
||||
"dictPreview": "<</BaseFont/Arial-BoldMT/DescendantFonts[ 141 0 R ]/Encoding/Identity-H/Subtype/Type0/ToUnicode 146 0 R /Type/Font>>"
|
||||
},
|
||||
{
|
||||
"obj": "141",
|
||||
"baseFont": "Arial-BoldMT",
|
||||
"subtype": "CIDFontType2",
|
||||
"encoding": null,
|
||||
"fontFileRef": null,
|
||||
"hasFontDescriptor": true,
|
||||
"dictPreview": "<</BaseFont/Arial-BoldMT/CIDSystemInfo 142 0 R /FontDescriptor 143 0 R /Subtype/CIDFontType2/Type/Font/W 145 0 R >>"
|
||||
}
|
||||
]
|
||||
},
|
||||
"region_ORIG": {
|
||||
"w": 1224,
|
||||
"h": 72,
|
||||
"sha": "e4f450f37b413ba6df3c7f53973359273bd90736fe55fea082102c5bea553afc"
|
||||
},
|
||||
"region_PREV": {
|
||||
"w": 1224,
|
||||
"h": 72,
|
||||
"sha": "e4f450f37b413ba6df3c7f53973359273bd90736fe55fea082102c5bea553afc"
|
||||
},
|
||||
"first_diff": "glyph outline pixels differ for 'o'"
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
{
|
||||
"mismatched_seed": {
|
||||
"label": "mismatched_seed",
|
||||
"text": "Professional Experiencex",
|
||||
"x0": 50.525001525878906,
|
||||
"advLen": 24,
|
||||
"advPrefix": [
|
||||
6.3868125000000004,
|
||||
4.1191875,
|
||||
6.451125,
|
||||
3.5859375,
|
||||
6.0410625,
|
||||
4.7870625,
|
||||
4.7870625,
|
||||
2.9473125000000002
|
||||
],
|
||||
"seedPrefix": [
|
||||
8.003997802734375,
|
||||
4.740001678466797,
|
||||
7.247997283935547,
|
||||
3.996002197265625,
|
||||
6.7440032958984375,
|
||||
6.743995666503906,
|
||||
6.743995666503906,
|
||||
3.2519989013671875
|
||||
],
|
||||
"firstAdvDiff": {
|
||||
"i": 0,
|
||||
"seed": 8.003997802734375,
|
||||
"got": 6.3868125000000004,
|
||||
"ch": "P"
|
||||
},
|
||||
"prefixAdvancesMatch": false
|
||||
},
|
||||
"full_length": {
|
||||
"label": "full_length",
|
||||
"text": "Professional Experiencex",
|
||||
"x0": 50.525001525878906,
|
||||
"advLen": 24,
|
||||
"advPrefix": [
|
||||
8.003997802734375,
|
||||
4.740001678466797,
|
||||
7.247997283935547,
|
||||
3.996002197265625,
|
||||
6.7440032958984375,
|
||||
6.743995666503906,
|
||||
6.743995666503906,
|
||||
3.2519989013671875
|
||||
],
|
||||
"seedPrefix": [
|
||||
8.003997802734375,
|
||||
4.740001678466797,
|
||||
7.247997283935547,
|
||||
3.996002197265625,
|
||||
6.7440032958984375,
|
||||
6.743995666503906,
|
||||
6.743995666503906,
|
||||
3.2519989013671875
|
||||
],
|
||||
"firstAdvDiff": null,
|
||||
"prefixAdvancesMatch": true
|
||||
},
|
||||
"no_advances": {
|
||||
"label": "no_advances",
|
||||
"text": "Professional Experiencex",
|
||||
"x0": 50.525001525878906,
|
||||
"advLen": 24,
|
||||
"advPrefix": [
|
||||
6.3868125000000004,
|
||||
4.1191875,
|
||||
6.451125,
|
||||
3.5859375,
|
||||
6.0410625,
|
||||
4.7870625,
|
||||
4.7870625,
|
||||
2.9473125000000002
|
||||
],
|
||||
"seedPrefix": [
|
||||
8.003997802734375,
|
||||
4.740001678466797,
|
||||
7.247997283935547,
|
||||
3.996002197265625,
|
||||
6.7440032958984375,
|
||||
6.743995666503906,
|
||||
6.743995666503906,
|
||||
3.2519989013671875
|
||||
],
|
||||
"firstAdvDiff": {
|
||||
"i": 0,
|
||||
"seed": 8.003997802734375,
|
||||
"got": 6.3868125000000004,
|
||||
"ch": "P"
|
||||
},
|
||||
"prefixAdvancesMatch": false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"pdf": "C:\\Users\\Maskan\\Downloads\\Saqib_Ali_Mir_Resume.pdf", "payloads": {"mismatched_seed": {"version": "1.0", "operations": [{"id": "t", "type": "reflow_paragraph", "pageIndex": 0, "data": {"objectIndices": [30], "runs": [{"text": "Professional Experiencex", "internalFontId": "Arial-BoldMT_TrueType_32", "fontSize": 12.0, "color": "#1a5276", "advances": [8.003997802734375, 4.740001678466797, 7.247997283935547, 3.996002197265625, 6.7440032958984375, 6.743995666503906, 6.743995666503906, 3.2519989013671875, 7.2480010986328125, 7.2480010986328125, 6.7440032958984375, 3.2519989013671875, 3.2519989013671875, 8.003997802734375, 6.73199462890625, 7.2480010986328125, 6.7440032958984375, 4.739990234375, 3.2519989013671875, 6.7440032958984375, 7.24798583984375, 6.7440185546875, 5.843994140625], "advanceSeedText": "Professional Experience"}], "lineX": [50.525001525878906], "lineBaselineY": [597.4500122070312], "columnLeft": 51.4010009765625, "columnRight": 301.4010009765625, "pushColumnLeft": 51.4010009765625, "firstBaselineY": 597.4500122070312, "leading": 14.399999999999999, "oldLineCount": 1, "align": "left", "paraId": "t"}}]}, "full_length": {"version": "1.0", "operations": [{"id": "t", "type": "reflow_paragraph", "pageIndex": 0, "data": {"objectIndices": [30], "runs": [{"text": "Professional Experiencex", "internalFontId": "Arial-BoldMT_TrueType_32", "fontSize": 12.0, "color": "#1a5276", "advances": [8.003997802734375, 4.740001678466797, 7.247997283935547, 3.996002197265625, 6.7440032958984375, 6.743995666503906, 6.743995666503906, 3.2519989013671875, 7.2480010986328125, 7.2480010986328125, 6.7440032958984375, 3.2519989013671875, 3.2519989013671875, 8.003997802734375, 6.73199462890625, 7.2480010986328125, 6.7440032958984375, 4.739990234375, 3.2519989013671875, 6.7440032958984375, 7.24798583984375, 6.7440185546875, 5.843994140625, 5.843994140625]}], "lineX": [50.525001525878906], "lineBaselineY": [597.4500122070312], "columnLeft": 51.4010009765625, "columnRight": 301.4010009765625, "pushColumnLeft": 51.4010009765625, "firstBaselineY": 597.4500122070312, "leading": 14.399999999999999, "oldLineCount": 1, "align": "left", "paraId": "t"}}]}, "no_advances": {"version": "1.0", "operations": [{"id": "t", "type": "reflow_paragraph", "pageIndex": 0, "data": {"objectIndices": [30], "runs": [{"text": "Professional Experiencex", "internalFontId": "Arial-BoldMT_TrueType_32", "fontSize": 12.0, "color": "#1a5276"}], "lineX": [50.525001525878906], "lineBaselineY": [597.4500122070312], "columnLeft": 51.4010009765625, "columnRight": 301.4010009765625, "pushColumnLeft": 51.4010009765625, "firstBaselineY": 597.4500122070312, "leading": 14.399999999999999, "oldLineCount": 1, "align": "left", "paraId": "t"}}]}}, "seed_adv": [8.003997802734375, 4.740001678466797, 7.247997283935547, 3.996002197265625, 6.7440032958984375, 6.743995666503906, 6.743995666503906, 3.2519989013671875, 7.2480010986328125, 7.2480010986328125, 6.7440032958984375, 3.2519989013671875, 3.2519989013671875, 8.003997802734375, 6.73199462890625, 7.2480010986328125, 6.7440032958984375, 4.739990234375, 3.2519989013671875, 6.7440032958984375, 7.24798583984375, 6.7440185546875, 5.843994140625]}
|
||||
@@ -0,0 +1,272 @@
|
||||
"""Deep font-program + normalized glyph outline compare for real resume PDF."""
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
import struct
|
||||
import sys
|
||||
import zlib
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
# Prefer rebuilt engine
|
||||
sys.path.insert(0, str(ROOT / "gateway"))
|
||||
sys.path.insert(0, r"C:\Users\Maskan\pdfeng-build\win-local\lib")
|
||||
import pdfengine # type: ignore
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
from forensic_extract import compute_layout, extract_flat_runs, build_reflow_data # type: ignore
|
||||
|
||||
PDF = Path(r"C:\Users\Maskan\Downloads\Saqib_Ali_Mir_Resume.pdf")
|
||||
OUT = Path(__file__).resolve().parent / "forensic_real"
|
||||
FID = "Arial-BoldMT_TrueType_32"
|
||||
TARGET = "Professional Experience"
|
||||
GLYPHS = list("Prof")
|
||||
|
||||
|
||||
def sha(b: bytes) -> str:
|
||||
return hashlib.sha256(b).hexdigest()
|
||||
|
||||
|
||||
def chunk_png(path: Path, rgba: bytes, w: int, h: int):
|
||||
def chunk(tag: bytes, data: bytes) -> bytes:
|
||||
return struct.pack(">I", len(data)) + tag + data + struct.pack(">I", zlib.crc32(tag + data) & 0xFFFFFFFF)
|
||||
raw = b"".join(b"\x00" + rgba[y * w * 4:(y + 1) * w * 4] for y in range(h))
|
||||
ihdr = struct.pack(">IIBBBBB", w, h, 8, 6, 0, 0, 0)
|
||||
path.write_bytes(b"\x89PNG\r\n\x1a\n" + chunk(b"IHDR", ihdr) + chunk(b"IDAT", zlib.compress(raw, 9)) + chunk(b"IEND", b""))
|
||||
|
||||
|
||||
def find_para(doc):
|
||||
page = doc.get_page(0)
|
||||
model = page.extract_document_model()
|
||||
for idx, para in enumerate(model.paragraphs):
|
||||
text = " ".join("".join(r.text for r in ln.runs) for ln in para.lines)
|
||||
if TARGET in text:
|
||||
return idx, para, text
|
||||
raise RuntimeError("not found")
|
||||
|
||||
|
||||
def glyph_boxes(para):
|
||||
wanted = {c: None for c in GLYPHS}
|
||||
for ln in para.lines:
|
||||
for r in ln.runs:
|
||||
for g in r.glyphs:
|
||||
if g.text in wanted and wanted[g.text] is None:
|
||||
wanted[g.text] = dict(
|
||||
origin_x=g.origin_x, origin_y=g.origin_y,
|
||||
bbox_x=g.bbox_x, bbox_y=g.bbox_y,
|
||||
bbox_w=g.bbox_w, bbox_h=g.bbox_h,
|
||||
font_size=g.font_size, font_name=g.font_name,
|
||||
)
|
||||
return wanted
|
||||
|
||||
|
||||
def crop_with_box(doc, box, dpi=288, pad=2.0):
|
||||
page = doc.get_page(0)
|
||||
ph = page.height
|
||||
x0, y0 = box["bbox_x"] - pad, box["bbox_y"] - pad
|
||||
x1, y1 = box["bbox_x"] + box["bbox_w"] + pad, box["bbox_y"] + box["bbox_h"] + pad
|
||||
y_top = ph - y1
|
||||
height = y1 - y0
|
||||
w, h, raw = page.render_region_raw(dpi, y_top, height)
|
||||
scale = dpi / 72.0
|
||||
left = max(0, int(x0 * scale))
|
||||
right = min(w, int(x1 * scale) + 1)
|
||||
cw = max(1, right - left)
|
||||
crop = bytearray(cw * h * 4)
|
||||
for row in range(h):
|
||||
src = (row * w + left) * 4
|
||||
dst = row * cw * 4
|
||||
crop[dst:dst + cw * 4] = raw[src:src + cw * 4]
|
||||
return bytes(crop), cw, h
|
||||
|
||||
|
||||
def parse_objects(pdf_bytes: bytes):
|
||||
parts = re.split(rb"(\d+)\s+0\s+obj", pdf_bytes)
|
||||
objs = {}
|
||||
i = 1
|
||||
while i + 1 < len(parts):
|
||||
num = int(parts[i].decode())
|
||||
body = parts[i + 1].split(b"endobj", 1)[0]
|
||||
objs[num] = body
|
||||
i += 2
|
||||
return objs
|
||||
|
||||
|
||||
def stream_data(body: bytes) -> bytes | None:
|
||||
m = re.search(rb"stream\r?\n(.*?)\r?\nendstream", body, re.DOTALL)
|
||||
if not m:
|
||||
return None
|
||||
raw = m.group(1)
|
||||
hdr = body.split(b"stream")[0]
|
||||
if b"/FlateDecode" in hdr:
|
||||
try:
|
||||
return zlib.decompress(raw)
|
||||
except Exception:
|
||||
return raw
|
||||
return raw
|
||||
|
||||
|
||||
def resolve_fontfile(objs: dict, font_obj_num: int) -> dict | None:
|
||||
"""Follow Font -> FontDescriptor -> FontFile2/3 and return program info."""
|
||||
body = objs.get(font_obj_num)
|
||||
if not body:
|
||||
return None
|
||||
text = body.split(b"stream")[0].decode("latin-1", "replace")
|
||||
bf = re.search(r"/BaseFont\s*/([^\s/>\[]+)", text)
|
||||
# DescendantFonts [ N 0 R ]
|
||||
desc_m = re.search(r"/DescendantFonts\s*\[\s*(\d+)\s+0\s+R", text)
|
||||
target = font_obj_num
|
||||
if desc_m:
|
||||
target = int(desc_m.group(1))
|
||||
text = objs[target].split(b"stream")[0].decode("latin-1", "replace")
|
||||
fd_m = re.search(r"/FontDescriptor\s+(\d+)\s+0\s+R", text)
|
||||
if not fd_m:
|
||||
return {"baseFont": bf.group(1) if bf else None, "fontFile": None, "reason": "no FontDescriptor"}
|
||||
fd_num = int(fd_m.group(1))
|
||||
fd = objs[fd_num].split(b"stream")[0].decode("latin-1", "replace")
|
||||
ff_m = re.search(r"/FontFile([23]?)\s+(\d+)\s+0\s+R", fd)
|
||||
if not ff_m:
|
||||
return {"baseFont": bf.group(1) if bf else None, "fontDescriptor": fd_num,
|
||||
"fontFile": None, "descriptorPreview": " ".join(fd.split())[:300]}
|
||||
kind, ff_num = ff_m.group(1) or "1", int(ff_m.group(2))
|
||||
data = stream_data(objs[ff_num])
|
||||
return {
|
||||
"baseFont": bf.group(1) if bf else None,
|
||||
"fontDescriptor": fd_num,
|
||||
"fontFileKind": f"FontFile{kind}",
|
||||
"fontFileObj": ff_num,
|
||||
"programLen": len(data) if data else 0,
|
||||
"programSha": sha(data) if data else None,
|
||||
"programHead": data[:16].hex() if data else None,
|
||||
"isSfnt": bool(data and data[:4] in (b"\x00\x01\x00\x00", b"OTTO", b"true")),
|
||||
}
|
||||
|
||||
|
||||
def find_fonts_by_base(objs: dict, needle: str) -> list[tuple[int, str]]:
|
||||
hits = []
|
||||
for num, body in objs.items():
|
||||
hdr = body.split(b"stream")[0].decode("latin-1", "replace")
|
||||
if "/BaseFont" in hdr and needle in hdr and "/Type" in hdr and "/Font" in hdr:
|
||||
bf = re.search(r"/BaseFont\s*/([^\s/>\[]+)", hdr)
|
||||
hits.append((num, bf.group(1) if bf else "?"))
|
||||
return hits
|
||||
|
||||
|
||||
def main():
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
print("[engine]", pdfengine.__file__)
|
||||
orig_bytes = PDF.read_bytes()
|
||||
doc = pdfengine.PdfDocument.load_from_file(str(PDF), "")
|
||||
_, para, text = find_para(doc)
|
||||
print("[para]", repr(text))
|
||||
|
||||
api_font = doc.get_font_data(FID)
|
||||
print(f"[API get_font_data] len={len(api_font)} sha={sha(api_font) if api_font else None}")
|
||||
|
||||
layout = compute_layout(para)
|
||||
flat = extract_flat_runs(layout["seedRuns"], FID, layout["seedRuns"][0]["size"], layout["seedRuns"][0]["color"])
|
||||
data = build_reflow_data(layout, flat, layout["origLines"], "deep-font")
|
||||
op = {"version": "1.0", "operations": [{"id": "e", "type": "reflow_paragraph", "pageIndex": 0, "data": data}]}
|
||||
|
||||
print("\n=== REFLOW (listen for EMIT/LOAD logs) ===")
|
||||
doc2 = pdfengine.PdfDocument.load_from_file(str(PDF), "")
|
||||
doc2.apply_edits(json.dumps(op))
|
||||
prev_bytes = doc2.save_full()
|
||||
(OUT / "preview_entry.pdf").write_bytes(prev_bytes)
|
||||
|
||||
objs_o = parse_objects(orig_bytes)
|
||||
objs_p = parse_objects(prev_bytes)
|
||||
|
||||
print("\n=== ORIGINAL fonts matching Arial-Bold ===")
|
||||
for num, bf in find_fonts_by_base(objs_o, "Arial-Bold"):
|
||||
info = resolve_fontfile(objs_o, num)
|
||||
print(f" obj {num} BaseFont={bf}")
|
||||
print(f" {json.dumps(info)}")
|
||||
|
||||
print("\n=== PREVIEW fonts matching Arial-Bold / Helvetica ===")
|
||||
for needle in ("Arial-Bold", "Helvetica", "Arial"):
|
||||
for num, bf in find_fonts_by_base(objs_p, needle):
|
||||
info = resolve_fontfile(objs_p, num)
|
||||
print(f" obj {num} BaseFont={bf}")
|
||||
print(f" {json.dumps(info)}")
|
||||
|
||||
# Compare API font program to every FontFile2 in preview that is new
|
||||
print("\n=== PROGRAM IDENTITY ===")
|
||||
api_sha = sha(api_font) if api_font else None
|
||||
print("API embedded program sha:", api_sha)
|
||||
prev_programs = []
|
||||
for num, bf in find_fonts_by_base(objs_p, "Arial"):
|
||||
info = resolve_fontfile(objs_p, num)
|
||||
if info and info.get("programSha"):
|
||||
prev_programs.append((num, bf, info))
|
||||
same = info["programSha"] == api_sha
|
||||
print(f" preview font obj={num} BaseFont={bf} programSha={info['programSha'][:16]}... "
|
||||
f"SAME_AS_EMBEDDED_API={same} len={info['programLen']}")
|
||||
|
||||
# Identify NEW BaseFont Arial-BoldMT (non-subset) introduced by emit
|
||||
new_emit = [p for p in prev_programs if p[1] == "Arial-BoldMT"]
|
||||
orig_subset = []
|
||||
for num, bf in find_fonts_by_base(objs_o, "BCDEEE+Arial-BoldMT"):
|
||||
info = resolve_fontfile(objs_o, num)
|
||||
orig_subset.append((num, bf, info))
|
||||
print(f" original subset obj={num} BaseFont={bf} programSha={(info or {}).get('programSha')}")
|
||||
|
||||
if new_emit and api_sha:
|
||||
ne = new_emit[0][2]
|
||||
print(f"\nFIRST FONT-PROGRAM DIVERGENCE:")
|
||||
print(f" emitted BaseFont=Arial-BoldMT programSha={ne.get('programSha')}")
|
||||
print(f" original API Arial-BoldMT_TrueType_32 sha={api_sha}")
|
||||
print(f" identical programs? {ne.get('programSha') == api_sha}")
|
||||
if ne.get("programSha") != api_sha:
|
||||
print(" => EMITTED FONT USES A DIFFERENT FONT PROGRAM (system fallback), NOT THE EMBEDDED PDF FONT FILE")
|
||||
|
||||
# Normalized glyph crops: use ORIGINAL bbox for BOTH docs so dimensions match
|
||||
print("\n=== NORMALIZED GLYPH OUTLINES (same crop box) ===")
|
||||
boxes = glyph_boxes(para)
|
||||
docp = pdfengine.PdfDocument.load_from_memory(prev_bytes, "")
|
||||
first = None
|
||||
results = {}
|
||||
for ch in GLYPHS:
|
||||
box = boxes[ch]
|
||||
if not box:
|
||||
continue
|
||||
rgba_o, wo, ho = crop_with_box(doc, box)
|
||||
rgba_p, wp, hp = crop_with_box(docp, box)
|
||||
# Force same size: pad/truncate
|
||||
h = min(ho, hp)
|
||||
w = min(wo, wp)
|
||||
def trim(rgba, W, H, tw, th):
|
||||
out = bytearray(tw * th * 4)
|
||||
for y in range(th):
|
||||
out[y*tw*4:(y+1)*tw*4] = rgba[y*W*4:y*W*4 + tw*4]
|
||||
return bytes(out)
|
||||
to, tp = trim(rgba_o, wo, ho, w, h), trim(rgba_p, wp, hp, w, h)
|
||||
same = to == tp
|
||||
# pixel diff count
|
||||
diff = sum(1 for a, b in zip(to, tp) if a != b)
|
||||
chunk_png(OUT / f"norm_{ch}_orig.png", to, w, h)
|
||||
chunk_png(OUT / f"norm_{ch}_prev.png", tp, w, h)
|
||||
print(f" [{ch}] {w}x{h} identical={same} differing_bytes={diff} "
|
||||
f"orig_font={box['font_name']} bbox_w={box['bbox_w']:.3f}")
|
||||
results[ch] = {"identical": same, "diff_bytes": diff, "w": w, "h": h}
|
||||
if not same and first is None:
|
||||
first = ch
|
||||
|
||||
print("\n=== FIRST GLYPH OUTLINE DIFFERENCE ===")
|
||||
print(first)
|
||||
(OUT / "deep_report.json").write_text(json.dumps({
|
||||
"api_font_sha": api_sha,
|
||||
"emitted_programs": [
|
||||
{"obj": n, "baseFont": bf, "sha": info.get("programSha"), "len": info.get("programLen"),
|
||||
"same_as_api": info.get("programSha") == api_sha}
|
||||
for n, bf, info in prev_programs
|
||||
],
|
||||
"glyphs": results,
|
||||
"first_outline_diff_char": first,
|
||||
}, indent=2), encoding="utf-8")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,392 @@
|
||||
"""Forensic on REAL failing PDF: Professional Experience / Arial-BoldMT_TrueType_32.
|
||||
|
||||
Compares:
|
||||
- original embedded font resource vs emitted font after edit-entry reflow
|
||||
- font dictionaries / FontFile streams
|
||||
- individual glyph crops for P,r,o,f from original vs preview
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
import struct
|
||||
import sys
|
||||
import zlib
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, r"C:\Users\Maskan\pdfeng-build\win-local\lib")
|
||||
sys.path.insert(0, str(ROOT / "gateway"))
|
||||
import pdfengine # type: ignore
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
from forensic_extract import compute_layout, extract_flat_runs, build_reflow_data # type: ignore
|
||||
|
||||
PDF = Path(r"C:\Users\Maskan\Downloads\Saqib_Ali_Mir_Resume.pdf")
|
||||
OUT = Path(__file__).resolve().parent / "forensic_real"
|
||||
TARGET = "Professional Experience"
|
||||
FID = "Arial-BoldMT_TrueType_32"
|
||||
GLYPHS = list("Prof") # P, r, o, f
|
||||
|
||||
|
||||
def sha(b: bytes) -> str:
|
||||
return hashlib.sha256(b).hexdigest()
|
||||
|
||||
|
||||
def inflate_streams(pdf_bytes: bytes) -> list[tuple[dict, bytes]]:
|
||||
"""Return list of (dict_header_textish, raw_or_inflated_stream)."""
|
||||
out = []
|
||||
parts = re.split(rb"(\d+)\s+0\s+obj", pdf_bytes)
|
||||
i = 1
|
||||
while i + 1 < len(parts):
|
||||
body = parts[i + 1].split(b"endobj", 1)[0]
|
||||
i += 2
|
||||
if b"stream" not in body:
|
||||
continue
|
||||
hdr = body.split(b"stream")[0]
|
||||
m = re.search(rb"stream\r?\n(.*?)\r?\nendstream", body, re.DOTALL)
|
||||
if not m:
|
||||
continue
|
||||
raw = m.group(1)
|
||||
data = raw
|
||||
if b"/FlateDecode" in hdr:
|
||||
try:
|
||||
data = zlib.decompress(raw)
|
||||
except Exception:
|
||||
pass
|
||||
out.append((hdr.decode("latin-1", "replace"), data))
|
||||
return out
|
||||
|
||||
|
||||
def dump_font_dicts(pdf_bytes: bytes) -> list[dict]:
|
||||
fonts = []
|
||||
# Compact and pretty forms
|
||||
for m in re.finditer(
|
||||
rb"<<[^>]*?/Type\s*/Font[^>]*?>>",
|
||||
pdf_bytes,
|
||||
re.DOTALL,
|
||||
):
|
||||
chunk = m.group(0).decode("latin-1", "replace")
|
||||
fonts.append({"raw": chunk[:500]})
|
||||
# Also objects containing BaseFont
|
||||
parts = re.split(rb"(\d+)\s+0\s+obj", pdf_bytes)
|
||||
i = 1
|
||||
while i + 1 < len(parts):
|
||||
num = parts[i].decode()
|
||||
body = parts[i + 1].split(b"endobj", 1)[0]
|
||||
i += 2
|
||||
if b"/Font" not in body or b"/BaseFont" not in body:
|
||||
continue
|
||||
hdr = body.split(b"stream")[0] if b"stream" in body else body
|
||||
text = hdr.decode("latin-1", "replace")
|
||||
bf = re.search(r"/BaseFont\s*/([^\s/>]+)", text)
|
||||
st = re.search(r"/Subtype\s*/(\w+)", text)
|
||||
enc = re.search(r"/Encoding\s*/(\w+)", text)
|
||||
ff = re.search(r"/FontFile[23]?\s+(\d+)\s+0\s+R", text)
|
||||
fonts.append({
|
||||
"obj": num,
|
||||
"baseFont": bf.group(1) if bf else None,
|
||||
"subtype": st.group(1) if st else None,
|
||||
"encoding": enc.group(1) if enc else None,
|
||||
"fontFileRef": ff.group(1) if ff else None,
|
||||
"hasFontDescriptor": "/FontDescriptor" in text,
|
||||
"dictPreview": " ".join(text.split())[:400],
|
||||
})
|
||||
return fonts
|
||||
|
||||
|
||||
def extract_fontfile_streams(pdf_bytes: bytes) -> list[dict]:
|
||||
"""Find FontFile/FontFile2/FontFile3 streams and hash their bytes."""
|
||||
results = []
|
||||
parts = re.split(rb"(\d+)\s+0\s+obj", pdf_bytes)
|
||||
i = 1
|
||||
while i + 1 < len(parts):
|
||||
num = parts[i].decode()
|
||||
body = parts[i + 1].split(b"endobj", 1)[0]
|
||||
i += 2
|
||||
if b"stream" not in body:
|
||||
continue
|
||||
hdr = body.split(b"stream")[0].decode("latin-1", "replace")
|
||||
# Heuristic: Length + (often referenced as font program). Tag by nearby refs from font dicts later.
|
||||
m = re.search(rb"stream\r?\n(.*?)\r?\nendstream", body, re.DOTALL)
|
||||
if not m:
|
||||
continue
|
||||
raw = m.group(1)
|
||||
data = raw
|
||||
if "/FlateDecode" in hdr:
|
||||
try:
|
||||
data = zlib.decompress(raw)
|
||||
except Exception:
|
||||
pass
|
||||
# Detect SFNT
|
||||
kind = "unknown"
|
||||
if data[:4] in (b"\x00\x01\x00\x00", b"OTTO", b"true", b"typ1"):
|
||||
kind = "sfnt"
|
||||
elif data[:2] == b"\x80\x01" or b"eexec" in data[:200]:
|
||||
kind = "type1"
|
||||
elif data[:4] == b"wOF2" or data[:4] == b"wOFF":
|
||||
kind = "woff"
|
||||
if kind == "unknown" and len(data) < 100:
|
||||
continue
|
||||
# Keep larger binary streams that look like fonts
|
||||
if kind == "unknown" and not (len(data) > 1000 and data[:4].isascii() is False):
|
||||
# still keep if Length suggests font and not content stream operators
|
||||
if b"BT" in data[:50] or b"q\n" in data[:20]:
|
||||
continue
|
||||
if len(data) < 2000:
|
||||
continue
|
||||
kind = "binary_blob"
|
||||
results.append({
|
||||
"obj": num,
|
||||
"kind": kind,
|
||||
"rawLen": len(raw),
|
||||
"dataLen": len(data),
|
||||
"sha256": sha(data),
|
||||
"head": data[:16].hex(),
|
||||
})
|
||||
return results
|
||||
|
||||
|
||||
def find_para(doc):
|
||||
page = doc.get_page(0)
|
||||
model = page.extract_document_model()
|
||||
for idx, para in enumerate(model.paragraphs):
|
||||
text = " ".join("".join(r.text for r in ln.runs) for ln in para.lines)
|
||||
if TARGET in text:
|
||||
return idx, para, text
|
||||
raise RuntimeError(f"Paragraph containing {TARGET!r} not found")
|
||||
|
||||
|
||||
def glyph_boxes(para):
|
||||
"""Map character -> list of glyph bboxes (PDF space) for first matching chars."""
|
||||
wanted = {c: None for c in GLYPHS}
|
||||
for ln in para.lines:
|
||||
for r in ln.runs:
|
||||
for g in r.glyphs:
|
||||
ch = g.text
|
||||
if ch in wanted and wanted[ch] is None:
|
||||
wanted[ch] = {
|
||||
"origin_x": g.origin_x,
|
||||
"origin_y": g.origin_y,
|
||||
"bbox_x": g.bbox_x,
|
||||
"bbox_y": g.bbox_y,
|
||||
"bbox_w": g.bbox_w,
|
||||
"bbox_h": g.bbox_h,
|
||||
"font_size": g.font_size,
|
||||
"font_name": g.font_name,
|
||||
}
|
||||
return wanted
|
||||
|
||||
|
||||
def render_glyph_crop(doc, box, dpi=288, pad=1.5) -> tuple[bytes, int, int, str]:
|
||||
"""Render a tight crop around a glyph bbox; return PNG bytes via region raw → simple PPM-less save as raw hash + PNG via page render tile if available."""
|
||||
page = doc.get_page(0)
|
||||
# page coords: y grows up. render_region_raw uses y_top from top of page.
|
||||
ph = page.height
|
||||
x0 = box["bbox_x"] - pad
|
||||
y0 = box["bbox_y"] - pad
|
||||
x1 = box["bbox_x"] + box["bbox_w"] + pad
|
||||
y1 = box["bbox_y"] + box["bbox_h"] + pad
|
||||
# Convert to top-origin band
|
||||
y_top = ph - y1
|
||||
height = y1 - y0
|
||||
# Full-width region then crop in python
|
||||
w, h, raw = page.render_region_raw(dpi, y_top, height)
|
||||
scale = dpi / 72.0
|
||||
# region is full page width; crop x range
|
||||
left = max(0, int(x0 * scale))
|
||||
right = min(w, int(x1 * scale) + 1)
|
||||
top = 0
|
||||
bottom = h
|
||||
# Extract RGBA crop
|
||||
crop_w = max(1, right - left)
|
||||
crop_h = bottom - top
|
||||
crop = bytearray(crop_w * crop_h * 4)
|
||||
for row in range(crop_h):
|
||||
src = ((top + row) * w + left) * 4
|
||||
dst = row * crop_w * 4
|
||||
crop[dst:dst + crop_w * 4] = raw[src:src + crop_w * 4]
|
||||
return bytes(crop), crop_w, crop_h, sha(bytes(crop))
|
||||
|
||||
|
||||
def save_rgba_png(path: Path, rgba: bytes, w: int, h: int):
|
||||
"""Minimal PNG writer (RGBA)."""
|
||||
import zlib as Z
|
||||
|
||||
def chunk(tag: bytes, data: bytes) -> bytes:
|
||||
return struct.pack(">I", len(data)) + tag + data + struct.pack(">I", Z.crc32(tag + data) & 0xFFFFFFFF)
|
||||
|
||||
raw = b"".join(b"\x00" + rgba[y * w * 4:(y + 1) * w * 4] for y in range(h))
|
||||
ihdr = struct.pack(">IIBBBBB", w, h, 8, 6, 0, 0, 0)
|
||||
png = b"\x89PNG\r\n\x1a\n" + chunk(b"IHDR", ihdr) + chunk(b"IDAT", Z.compress(raw, 9)) + chunk(b"IEND", b"")
|
||||
path.write_bytes(png)
|
||||
|
||||
|
||||
def main():
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
assert PDF.exists(), PDF
|
||||
print(f"[engine] {pdfengine.__file__}")
|
||||
print(f"[pdf] {PDF}")
|
||||
|
||||
doc = pdfengine.PdfDocument.load_from_file(str(PDF), "")
|
||||
pi, para, text = find_para(doc)
|
||||
print(f"[para] page={pi} text={text!r}")
|
||||
|
||||
# Font info from engine
|
||||
fonts = doc.get_fonts(0, 0)
|
||||
target_fonts = [f for f in fonts if FID in (f.internal_font_id or "") or f.font_name == "Arial-BoldMT"]
|
||||
print("[FontInfo]")
|
||||
for f in target_fonts:
|
||||
print(f" name={f.font_name} id={f.internal_font_id} emb={f.is_embedded} type={f.type} "
|
||||
f"subset={getattr(f, 'is_subset', None)} ascent={f.ascent} descent={f.descent}")
|
||||
|
||||
# Extract font program bytes via API
|
||||
font_bytes = doc.get_font_data(FID)
|
||||
recon_bytes = doc.get_reconstructed_font_data(FID)
|
||||
print(f"[get_font_data] len={len(font_bytes)} sha={sha(font_bytes) if font_bytes else None}")
|
||||
print(f"[get_reconstructed_font_data] len={len(recon_bytes)} sha={sha(recon_bytes) if recon_bytes else None}")
|
||||
if font_bytes:
|
||||
(OUT / "orig_font_program.bin").write_bytes(font_bytes)
|
||||
if recon_bytes:
|
||||
(OUT / "recon_font_program.bin").write_bytes(recon_bytes)
|
||||
|
||||
# Build edit-entry reflow (unchanged text)
|
||||
layout = compute_layout(para)
|
||||
print("[layout]", {
|
||||
"columnLeft": layout["columnLeft"], "columnRight": layout["columnRight"],
|
||||
"firstBaselineY": layout["firstBaselineY"], "leading": layout["leading"],
|
||||
"seedRuns": [(r["text"], r["fid"], r["fontName"], r["size"], len(r.get("advances") or []))
|
||||
for r in layout["seedRuns"]],
|
||||
"objectIndices": layout["objectIndices"],
|
||||
})
|
||||
dominant_fid = next((r["fid"] for r in layout["seedRuns"] if r["text"].strip() and r["fid"]), FID)
|
||||
dom = next((r for r in layout["seedRuns"] if r["text"].strip()), layout["seedRuns"][0])
|
||||
flat = extract_flat_runs(layout["seedRuns"], dominant_fid, dom["size"], dom["color"])
|
||||
data = build_reflow_data(layout, flat, layout["origLines"], "forensic-real-profexp")
|
||||
op = {"version": "1.0", "operations": [{
|
||||
"id": "entry", "type": "reflow_paragraph", "pageIndex": 0, "data": data,
|
||||
}]}
|
||||
(OUT / "reflow_payload.json").write_text(json.dumps(op, indent=2), encoding="utf-8")
|
||||
|
||||
print("\n=== APPLY EDIT-ENTRY REFLOW (unchanged text) ===")
|
||||
doc2 = pdfengine.PdfDocument.load_from_file(str(PDF), "")
|
||||
doc2.apply_edits(json.dumps(op))
|
||||
preview_bytes = doc2.save_full()
|
||||
(OUT / "preview_entry.pdf").write_bytes(preview_bytes)
|
||||
orig_bytes = PDF.read_bytes()
|
||||
(OUT / "original.pdf").write_bytes(orig_bytes)
|
||||
|
||||
# Font dictionaries
|
||||
print("\n=== FONT DICTIONARIES ORIGINAL ===")
|
||||
orig_fonts = dump_font_dicts(orig_bytes)
|
||||
for f in orig_fonts:
|
||||
if f.get("baseFont") and ("Arial" in (f.get("baseFont") or "") or "Bold" in (f.get("baseFont") or "")):
|
||||
print(json.dumps(f, indent=2))
|
||||
print("\n=== FONT DICTIONARIES PREVIEW ===")
|
||||
prev_fonts = dump_font_dicts(preview_bytes)
|
||||
for f in prev_fonts:
|
||||
if f.get("baseFont") and ("Arial" in (f.get("baseFont") or "") or "Helv" in (f.get("baseFont") or "") or "Bold" in (f.get("baseFont") or "")):
|
||||
print(json.dumps(f, indent=2))
|
||||
|
||||
print("\n=== FONTFILE / EMBEDDED PROGRAM HASHES ===")
|
||||
orig_ff = extract_fontfile_streams(orig_bytes)
|
||||
prev_ff = extract_fontfile_streams(preview_bytes)
|
||||
print(f"original font-like streams: {len(orig_ff)}")
|
||||
for x in orig_ff:
|
||||
if x["kind"] in ("sfnt", "type1", "binary_blob") and x["dataLen"] > 5000:
|
||||
print(" ORIG", x)
|
||||
print(f"preview font-like streams: {len(prev_ff)}")
|
||||
for x in prev_ff:
|
||||
if x["kind"] in ("sfnt", "type1", "binary_blob") and x["dataLen"] > 1000:
|
||||
print(" PREV", x)
|
||||
|
||||
orig_shas = {x["sha256"] for x in orig_ff if x["dataLen"] > 5000}
|
||||
prev_shas = {x["sha256"] for x in prev_ff if x["dataLen"] > 1000}
|
||||
shared = orig_shas & prev_shas
|
||||
print(f"shared embedded program SHAs: {len(shared)}")
|
||||
print(f"orig-only programs: {len(orig_shas - prev_shas)}")
|
||||
print(f"prev-only programs: {len(prev_shas - orig_shas)}")
|
||||
if font_bytes:
|
||||
print(f"API font_bytes in orig streams? {sha(font_bytes) in orig_shas or any(sha(font_bytes)==x['sha256'] for x in orig_ff)}")
|
||||
print(f"API font_bytes in prev streams? {any(sha(font_bytes)==x['sha256'] for x in prev_ff)}")
|
||||
|
||||
# Re-extract preview paragraph fonts
|
||||
docp = pdfengine.PdfDocument.load_from_memory(preview_bytes, "")
|
||||
_, para_p, text_p = find_para(docp)
|
||||
print(f"\n[preview para] text={text_p!r}")
|
||||
for ln in para_p.lines:
|
||||
for r in ln.runs:
|
||||
if r.text.strip():
|
||||
print(f" run text={r.text!r} font={r.font_name} id={r.internal_font_id} "
|
||||
f"emb={r.is_embedded} type={r.type} size={r.font_size} w={r.w}")
|
||||
|
||||
# Glyph crops P,r,o,f
|
||||
print("\n=== GLYPH CROPS P/r/o/f ===")
|
||||
boxes_o = glyph_boxes(para)
|
||||
boxes_p = glyph_boxes(para_p)
|
||||
report = {"glyphs": {}, "font": {
|
||||
"api_font_sha": sha(font_bytes) if font_bytes else None,
|
||||
"api_recon_sha": sha(recon_bytes) if recon_bytes else None,
|
||||
"shared_programs": list(shared),
|
||||
"orig_programs": [x for x in orig_ff if x["dataLen"] > 5000],
|
||||
"prev_programs": [x for x in prev_ff if x["dataLen"] > 1000],
|
||||
"orig_font_dicts": [f for f in orig_fonts if f.get("baseFont")],
|
||||
"prev_font_dicts": [f for f in prev_fonts if f.get("baseFont")],
|
||||
}}
|
||||
first_diff = None
|
||||
for ch in GLYPHS:
|
||||
bo, bp = boxes_o.get(ch), boxes_p.get(ch)
|
||||
print(f"\n[{ch}] orig_box={bo}")
|
||||
print(f"[{ch}] prev_box={bp}")
|
||||
if not bo or not bp:
|
||||
report["glyphs"][ch] = {"error": "missing box"}
|
||||
if first_diff is None:
|
||||
first_diff = f"glyph {ch!r}: missing box orig={bo is not None} prev={bp is not None}"
|
||||
continue
|
||||
try:
|
||||
rgba_o, wo, ho, sho = render_glyph_crop(doc, bo)
|
||||
rgba_p, wp, hp, shp = render_glyph_crop(docp, bp)
|
||||
save_rgba_png(OUT / f"glyph_{ch}_orig.png", rgba_o, wo, ho)
|
||||
save_rgba_png(OUT / f"glyph_{ch}_prev.png", rgba_p, wp, hp)
|
||||
same = sho == shp
|
||||
print(f"[{ch}] crop orig={wo}x{ho} sha={sho[:16]} prev={wp}x{hp} sha={shp[:16]} SAME={same}")
|
||||
# Also compare dimensions / bbox metrics
|
||||
metric_diff = {
|
||||
"bbox_w": (bo["bbox_w"], bp["bbox_w"]),
|
||||
"bbox_h": (bo["bbox_h"], bp["bbox_h"]),
|
||||
"font_size": (bo["font_size"], bp["font_size"]),
|
||||
"font_name": (bo["font_name"], bp["font_name"]),
|
||||
}
|
||||
report["glyphs"][ch] = {
|
||||
"orig_box": bo, "prev_box": bp,
|
||||
"orig_sha": sho, "prev_sha": shp,
|
||||
"pixel_identical": same,
|
||||
"metrics": metric_diff,
|
||||
}
|
||||
if not same and first_diff is None:
|
||||
first_diff = f"glyph outline pixels differ for {ch!r}"
|
||||
if bo["font_name"] != bp["font_name"] and first_diff is None:
|
||||
first_diff = f"font_name differs at glyph {ch!r}: {bo['font_name']} -> {bp['font_name']}"
|
||||
except Exception as e:
|
||||
print(f"[{ch}] render failed: {e}")
|
||||
report["glyphs"][ch] = {"error": str(e)}
|
||||
|
||||
# Full-page / paragraph region hash
|
||||
print("\n=== REGION HASH (paragraph band) ===")
|
||||
y_top = doc.get_page(0).height - (layout["firstBaselineY"] + layout["leading"])
|
||||
hgt = layout["leading"] * 2.5
|
||||
for label, d in [("ORIG", doc), ("PREV", docp)]:
|
||||
w, h, raw = d.get_page(0).render_region_raw(144, max(0, y_top), hgt)
|
||||
print(f" {label} region {w}x{h} sha={sha(raw)[:24]}")
|
||||
report[f"region_{label}"] = {"w": w, "h": h, "sha": sha(raw)}
|
||||
|
||||
report["first_diff"] = first_diff
|
||||
(OUT / "report.json").write_text(json.dumps(report, indent=2), encoding="utf-8")
|
||||
print("\n=== FIRST DIFFERENCE ===")
|
||||
print(first_diff)
|
||||
print(f"Wrote artifacts to {OUT}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,57 @@
|
||||
/** Compare first-keystroke layouts in the SHIPPED browser WASM. */
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
import { resolve, dirname } from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
const repo = resolve(here, "../../");
|
||||
const payloads = JSON.parse(readFileSync(resolve(here, "forensic_real/wasm_keystroke_payloads.json"), "utf8"));
|
||||
const pdfBytes = readFileSync(payloads.pdf);
|
||||
const mjsPath = resolve(repo, "frontend/public/pdfium-engine.mjs");
|
||||
const wasmPath = resolve(repo, "frontend/public/pdfium-engine.wasm");
|
||||
|
||||
const { default: createModule } = await import(pathToFileURL(mjsPath).href);
|
||||
const Module = await createModule({
|
||||
locateFile: (p) => (p.endsWith(".wasm") ? wasmPath : p),
|
||||
});
|
||||
|
||||
const ptr = Module._malloc(pdfBytes.length);
|
||||
Module.HEAPU8.set(pdfBytes, ptr);
|
||||
const handle = Module.ccall("loadDocument", "number", ["number", "number"], [ptr, pdfBytes.length]);
|
||||
Module._free(ptr);
|
||||
|
||||
function run(label, op) {
|
||||
Module.ccall("previewRender", "number", ["number", "number", "number", "string"], [
|
||||
handle, 0, 144, JSON.stringify(op),
|
||||
]);
|
||||
const layoutJson = Module.ccall("lastLayoutJson", "string", [], []) || "{}";
|
||||
const layout = JSON.parse(layoutJson);
|
||||
const line = layout.lines?.[0];
|
||||
const adv = line?.adv || [];
|
||||
const seed = payloads.seed_adv;
|
||||
let firstAdvDiff = null;
|
||||
for (let i = 0; i < Math.min(seed.length, adv.length); i++) {
|
||||
if (Math.abs(seed[i] - adv[i]) > 0.05) {
|
||||
firstAdvDiff = { i, seed: seed[i], got: adv[i], ch: "Professional Experience"[i] };
|
||||
break;
|
||||
}
|
||||
}
|
||||
return {
|
||||
label,
|
||||
text: line?.text,
|
||||
x0: line?.x0,
|
||||
advLen: adv.length,
|
||||
advPrefix: adv.slice(0, 8),
|
||||
seedPrefix: seed.slice(0, 8),
|
||||
firstAdvDiff,
|
||||
prefixAdvancesMatch: firstAdvDiff === null && adv.length >= seed.length,
|
||||
};
|
||||
}
|
||||
|
||||
const results = {};
|
||||
for (const [k, op] of Object.entries(payloads.payloads)) {
|
||||
results[k] = run(k, op);
|
||||
}
|
||||
writeFileSync(resolve(here, "forensic_real/wasm_keystroke_compare.json"), JSON.stringify(results, null, 2));
|
||||
console.log(JSON.stringify(results, null, 2));
|
||||
Module.ccall("freeDocument", null, ["number"], [handle]);
|
||||