141 lines
5.2 KiB
Python
141 lines
5.2 KiB
Python
"""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()
|