1. Stale test: test_extract_objects_renders_same_page_once_for_multiple_crops
now asserts render_pdf_page_cached is NOT called (new PageRenderContext path)
and pages/page_001.jpg is NOT created.
2. Extract _resolve_object_crop_pdf_path() helper with 4-case test.
Phase 4 now uses it instead of inline fallback logic.
Test locks in: Phase 1 resolved path beats stale meta['source_pdf'].
3. PageRenderContext: use fitz.csRGB in get_pixmap() for safe CMYK/n>3 handling.
Removed fragile pix.n mode detection.
4. Added test_extract_and_write_objects_with_use_disk_page_cache_false_and_valid_pdf
6 fixes bundled:
1. Cleanup: remove accidental empty file '6s}'
2. Rebuild determinism: add 'use_disk_page_cache' gate to _crop_asset_from_pdf.
Rebuild (use_disk_page_cache=False) never reads or writes pages/page_XXX.jpg.
All rendering goes through PageRenderContext in-memory.
Legacy callers keep backward-compat behavior (default True).
3. PageRenderContext safety:
- Only use when page_width>0 and page_height>0 and not rotation_deg
- Fix Pixmap.n mode detection (L/RGB/RGBA -> convert to RGB)
- Fix Image.Resampling name bug (Image -> PILImage)
- Rotated crops always fall back to direct PDF clip path
4. Phase 1+2a merged traversal: always extract PDF lines on every page,
regardless of whether span_metadata already exists. Fixes figure inventory
data gaps when pages have pre-existing span coverage.
5. Phase 4: use resolved source_pdf_path from Phase 1, not ocr_meta['source_pdf']
fallback (which may be stale or missing).
6. Added 5 determinism tests
Add PageRenderContext class that renders each page once into memory
(PIL Image via get_pixmap + frombytes), shared across all crop tasks
in a single rebuild session. No full-page JPG is written to disk.
- _crop_asset_from_pdf: new fast path (page_render_context kwarg)
- Three task functions pass context through
- extract_and_write_objects opens PDF once, creates context, closes after
- Cold Phase 4a: 4.0s → 1.24s (60p, 30 unique pages, 42 crops)
- Full cold rebuild: 10.9s → 7.17s
figure_inner_text role means the text IS part of the figure content
(forest plot y-axis labels, nomogram variable names, panel markers).
The earlier fix listed the text as a separate below the image — that
was wrong: the text already lives inside the cropped image when the
crop bbox covers it.
Now extract_and_write_objects expands the crop bbox to include all
figure_inner_text blocks owned by the figure (via _object_owner_id,
same page). The cropped jpg contains the labels; render only emits
image + Legend.
Verified: M84CTEM9 figure_a001 (forest plot) now crops to 787x363
(previously 444x358 — missed the y-axis labels); figure_a002
(nomogram) still includes its variable labels. 105 focused tests pass.
figure_inner_text role has been recognized (side-adjacent + contained
passes) but its blocks were dropped from body AND silently dropped from
figure render output — they had nowhere to go. The owner contract
(_object_owner_id) was already stamped on the block; the renderer
just never consumed it.
Three changes:
- extract_and_write_objects now accepts structured_blocks and matches
figure_inner_text blocks to their owner figure via _object_owner_id
- render_figure_object_markdown merges the inner text as figure body
(no separate Note section — figure_inner_text IS the figure content)
- tag_figure_contained_text now stamps _object_owner_id on contained
blocks claimed via the matched-figure path, so contained text also
flows back to its owning figure (previously only side-adjacent was
bound)
Rebuild ordering fixed too: apply_object_writebacks now runs BEFORE
write_figure_inventory so claims land in the persisted inventory.
Tested: M84CTEM9 figure_a001.md now includes the forest plot y-axis
variable labels (Age/SPADI/Sex/...) as figure content; body no longer
leaks them; 105 focused tests pass.
37LK5T97 Tables 1-3 span dir=[0,-1] (rotated 90° content on portrait page).
The table body AND caption are both rotated — the rendered JPEG showed
vertical text.
Fix:
- Add _table_has_rotated_content() in ocr_tables.py — checks asset
span_metadata dir to detect rotated tables, returns 270° correction
- In build_table_inventory, compute union render_bbox (caption+asset)
and store render_rotation_deg in the table entry
- In ocr_objects.py table render loop, use render_bbox + rotation_deg
when available, passing rotation_deg to _crop_asset_from_pdf
Result:
Tables 1-5 (rotated): 1908x2858 -> 2858x1908 (270° corrected, readable)
Table 6 (normal): 987x191 -> unchanged (no rotation_deg)
428 regression tests pass.
Two bugs fixed in _crop_asset_from_pdf for rotated vector figures:
1. Coordinate mismatch: bbox values are in OCR coordinates (2x PDF), but
PyMuPDF get_pixmap(clip=...) expects PDF user-space coordinates.
Added per-page scale conversion using page_width/page_height vs PDF
page rect ratio. Without this, the clip started ~52pt right of the
actual chart, cutting off the left edge.
2. Triple quality loss: removed stale _apply_rotation_if_needed inner
function (PIL save+reopen). Replaced with 4x zoom render + single-pass
PIL rotate from pix.tobytes('png'). No JPEG intermediary, one rotation
interpolation instead of two. 4.7x pixel improvement.
Resolution quality:
Before (cached page path): ~862x1309 px (1.1M px)
After (4x pixmap + PIL): 2618x1914 px (5.0M px, 4.5x)
428 regression tests pass.
- preserve PyMuPDF dir/wmode in span_metadata and bump span backfill version
- promote rotated vision_footnote figure descriptions into normal legend prematch
- normalize rotated caption/asset regions before score_figure_match
- carry rotation_correction_deg into matched figures and cropped renders
- use LANCZOS resampling on rotated figure crops to avoid aliasing
- keep synthetic fallback as last resort, reusing the same rotation metadata
- Fix references zone detection without heading (backward reference fallback)
- Fix reference_item silently dropped in _reorder_tail_run_fifo when no heading
- Fix figure proximity matching: use nearest legend regardless of column order
- Fix figure numbering in extract_and_write_objects: use actual figure_id
- Fix body→reference_item rescue: require text starts with numeric ref pattern
- Fix tail reading order rebuilt after block normalization
- A8E7SRVS: corrected figure-legend pairing on page 5
- CAQNW9Q2: references now render without heading
- K7R8PEKW: adjusted body retention threshold