mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-24 10:08:00 -06:00
Fix named properties in forms without resources
This commit is contained in:
@@ -2,6 +2,8 @@
|
||||
|
||||
**Stage: alpha** · next: beta, ~25% (ready for real work 50% → 75%) and ~400–700 h away
|
||||
|
||||
> **Last reviewed:** 2026-10-20 | **Last updated:** 2026-10-20 | **Change:** minor (M2, #3574: named form properties retain page origin and own dictionary boundaries; estimates unchanged)
|
||||
|
||||
> **Last reviewed:** 2026-10-18 · **Last updated:** 2026-10-18 · **Change:** minor (M5, PR #1971: bounded nested FDF export excludes source page/page-tree dictionaries; refreshed against upstream 4db6ac4a; estimates unchanged)
|
||||
> **Last reviewed:** 2026-10-15 · **Last updated:** 2026-10-15 · **Change:** minor (M7: the Edit text caret is as tall as the row it is on; estimates unchanged) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS)
|
||||
> **Last reviewed:** 2026-10-15 · **Last updated:** 2026-10-15 · **Change:** minor (M6: CSV tab-containing headers; estimates unchanged)
|
||||
@@ -172,6 +174,8 @@ Ranked; detail and the full milestone table in [docs/roadmap.md](docs/roadmap.md
|
||||
|
||||
## Progress log
|
||||
|
||||
- **2026-10-20 (M2, #3574):** Forms without Resources use page Properties for ActualText and layer visibility. Synthetic tests cover boundaries, nested/shared forms, and save/reopen. Estimates unchanged.
|
||||
|
||||
- **2026-10-19 (M14, #3373):** The main menu and its long submenus scroll in short windows, keeping their commands reachable. Estimates unchanged.
|
||||
|
||||
- **2026-10-19 (M3, #3387):** Save-prompt shortcuts yield to later consent and utility dialogs; a synthetic UI regression checks that an edited document stays open. Estimates unchanged.
|
||||
|
||||
@@ -0,0 +1,294 @@
|
||||
//! Contributor-original synthetic forms for named marked-content properties.
|
||||
use pdfcraft_render::{PageRenderer, RenderConfig, RenderRequest, RequestKind};
|
||||
use std::sync::Arc;
|
||||
|
||||
fn fixture(own: bool) -> Vec<u8> {
|
||||
let res1 = "<< /Font << /F1 3 0 R >> /XObject << /Fm0 8 0 R >> /Properties << /P1 << /ActualText (REPLACED) >> >> >>";
|
||||
let res2 = "<< /Font << /F1 3 0 R >> /XObject << /Fm0 9 0 R >> /Properties << /oc1 7 0 R >> >>";
|
||||
let own1 = if own { format!("/Resources {res1}") } else { String::new() };
|
||||
let own2 = if own { format!("/Resources {res2}") } else { String::new() };
|
||||
let body1 = "/Span /P1 BDC BT /F1 24 Tf 72 700 Td (glyphs) Tj ET EMC";
|
||||
let body2 = "/OC /oc1 BDC BT /F1 24 Tf 72 700 Td (HIDDENTEXT) Tj ET EMC";
|
||||
let page1 = "q /Fm0 Do Q";
|
||||
let page2 = "q /Fm0 Do Q BT /F1 24 Tf 72 600 Td (visible) Tj ET";
|
||||
pdf(&[
|
||||
"<< /Type /Catalog /Pages 2 0 R /OCProperties << /OCGs [7 0 R] /D << /OFF [7 0 R] >> >> >>".into(),
|
||||
"<< /Type /Pages /Kids [4 0 R 5 0 R] /Count 2 >>".into(),
|
||||
"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>".into(),
|
||||
format!("<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents 6 0 R /Resources {res1} >>"),
|
||||
format!("<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents 10 0 R /Resources {res2} >>"),
|
||||
stream("", page1),
|
||||
"<< /Type /OCG /Name (Hidden) >>".into(),
|
||||
stream(&format!("/Type /XObject /Subtype /Form /BBox [0 0 612 792] {own1}"), body1),
|
||||
stream(&format!("/Type /XObject /Subtype /Form /BBox [0 0 612 792] {own2}"), body2),
|
||||
stream("", page2),
|
||||
])
|
||||
}
|
||||
|
||||
fn text(renderer: &mut PageRenderer, page: usize) -> Arc<pdfcraft_render::PageText> {
|
||||
let result = renderer.render(RenderRequest { page, kind: RequestKind::Text, ..Default::default() });
|
||||
assert!(result.error.is_none(), "{:?}", result.error);
|
||||
result.text.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn named_actual_text_in_resource_less_form_replaces_glyphs() {
|
||||
for own in [true, false] {
|
||||
let mut renderer = PageRenderer::new(Arc::new(fixture(own)), RenderConfig::default());
|
||||
let result = text(&mut renderer, 0);
|
||||
assert_eq!(result.plain_text(), "REPLACED", "own Resources: {own}");
|
||||
assert_eq!(result.find("REPLACED").len(), 1);
|
||||
assert!(result.find("glyphs").is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn named_off_layer_in_resource_less_form_stays_hidden() {
|
||||
for own in [true, false] {
|
||||
let mut renderer = PageRenderer::new(Arc::new(fixture(own)), RenderConfig::default());
|
||||
let image = renderer.render(RenderRequest { page: 1, scale: 1.0, ..Default::default() });
|
||||
assert!(image.error.is_none(), "{:?}", image.error);
|
||||
assert_eq!((image.width, image.height), (612, 792));
|
||||
let upper = image
|
||||
.rgba
|
||||
.as_chunks::<4>()
|
||||
.0
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(index, _)| (70..105).contains(&(index / 612)))
|
||||
.filter(|(_, pixel)| **pixel != [255, 255, 255, 255])
|
||||
.count();
|
||||
let lower = image
|
||||
.rgba
|
||||
.as_chunks::<4>()
|
||||
.0
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(index, _)| (170..200).contains(&(index / 612)))
|
||||
.filter(|(_, pixel)| **pixel != [255, 255, 255, 255])
|
||||
.count();
|
||||
let result = text(&mut renderer, 1);
|
||||
println!(
|
||||
"own={own}: upper ink={upper}; lower ink={lower}; text={:?}; hidden Find={:?}; visible Find={:?}",
|
||||
result.plain_text(),
|
||||
result.find("HIDDENTEXT"),
|
||||
result.find("visible")
|
||||
);
|
||||
assert_eq!(upper, 0, "OFF layer has no upper ink, own Resources: {own}");
|
||||
assert!(lower > 0, "ordinary page text stays visible");
|
||||
assert_eq!(result.plain_text(), "visible", "own Resources: {own}");
|
||||
assert_eq!(result.find("visible").len(), 1);
|
||||
assert!(result.find("HIDDENTEXT").is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
// Contributor-original synthetic PDFs. No font or document assets are embedded.
|
||||
fn pdf(objects: &[String]) -> Vec<u8> {
|
||||
let mut bytes = b"%PDF-1.7\n".to_vec();
|
||||
let mut offsets = Vec::new();
|
||||
for (i, object) in objects.iter().enumerate() {
|
||||
offsets.push(bytes.len());
|
||||
bytes.extend_from_slice(format!("{} 0 obj\n{object}\nendobj\n", i + 1).as_bytes());
|
||||
}
|
||||
let xref = bytes.len();
|
||||
bytes.extend_from_slice(format!("xref\n0 {}\n0000000000 65535 f \n", objects.len() + 1).as_bytes());
|
||||
for offset in offsets {
|
||||
bytes.extend_from_slice(format!("{offset:010} 00000 n \n").as_bytes());
|
||||
}
|
||||
bytes.extend_from_slice(format!("trailer << /Size {} /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n", objects.len() + 1).as_bytes());
|
||||
bytes
|
||||
}
|
||||
|
||||
fn stream(dict: &str, body: &str) -> String {
|
||||
format!("<< {dict} /Length {} >>\nstream\n{body}\nendstream", body.len())
|
||||
}
|
||||
|
||||
fn resources(properties: &str) -> String {
|
||||
format!("<< /Font << /F1 3 0 R >> /XObject << /Fm 6 0 R /Inner 7 0 R >> {properties} >>")
|
||||
}
|
||||
|
||||
fn form(resources: &str, body: &str) -> String {
|
||||
stream(&format!("/Type /XObject /Subtype /Form /BBox [0 0 200 200] {resources}"), body)
|
||||
}
|
||||
|
||||
const GLYPHS: &str = "/Span /P BDC BT /F1 12 Tf 10 150 Td (glyphs) Tj ET EMC";
|
||||
const OFF_PAINT: &str = "/OC /O BDC 1 0 0 rg 10 100 20 20 re f EMC 0 0 1 rg 50 100 20 20 re f";
|
||||
|
||||
fn objects(page_resources: &str, form_resources: &str, body: &str) -> Vec<String> {
|
||||
vec![
|
||||
"<< /Type /Catalog /Pages 2 0 R /OCProperties << /OCGs [8 0 R 9 0 R] /D << /OFF [8 0 R] >> >> >>".into(),
|
||||
"<< /Type /Pages /Kids [4 0 R] /Count 1 >>".into(),
|
||||
"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>".into(),
|
||||
format!("<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] /Contents 5 0 R {page_resources} >>"),
|
||||
stream("", "/Fm Do"),
|
||||
form(form_resources, body),
|
||||
form("", GLYPHS),
|
||||
"<< /Type /OCG /Name (PageOff) >>".into(),
|
||||
"<< /Type /OCG /Name (FormOn) >>".into(),
|
||||
]
|
||||
}
|
||||
|
||||
fn assert_text(bytes: Vec<u8>, page: usize, expected: &str) {
|
||||
let mut renderer = PageRenderer::new(Arc::new(bytes), RenderConfig::default());
|
||||
let result = text(&mut renderer, page);
|
||||
assert_eq!(result.plain_text(), expected);
|
||||
assert_eq!(result.find(expected).len(), 1);
|
||||
if expected != "glyphs" {
|
||||
assert!(result.find("glyphs").is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn own_form_dictionary_stops_property_fallback() {
|
||||
let page = format!("/Resources {}", resources("/Properties << /P << /ActualText (PAGE) >> >>"));
|
||||
for (own, expected) in [
|
||||
("", "PAGE"),
|
||||
("/Resources << /Properties << /P << /ActualText (OWN) >> >> >>", "OWN"),
|
||||
("/Resources << >>", "glyphs"),
|
||||
("/Resources << /Properties << >> >>", "glyphs"),
|
||||
("/Resources << /Properties << /Other << /ActualText (OTHER) >> >> >>", "glyphs"),
|
||||
("/Resources << /Properties << /P 42 >> >>", "glyphs"),
|
||||
("/Resources << /Properties << /P 99 0 R >> >>", "glyphs"),
|
||||
("/Resources << /Properties 42 >>", "glyphs"),
|
||||
("/Resources 42", "glyphs"),
|
||||
("/Resources 99 0 R", "glyphs"),
|
||||
// Preserve the pre-fix behavior for an explicitly present null entry.
|
||||
("/Resources null", "glyphs"),
|
||||
] {
|
||||
assert_text(pdf(&objects(&page, own, GLYPHS)), 0, expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested_resource_less_form_uses_page_origin_and_own_form_shadows_it() {
|
||||
let page = format!("/Resources {}", resources("/Properties << /P << /ActualText (PAGE) >> >>"));
|
||||
let outer = format!("/Resources {}", resources("/Properties << /P << /ActualText (CALLER) >> >>"));
|
||||
for (inner, expected) in [("", "PAGE"), ("/Resources << /Properties << /P << /ActualText (INNER) >> >> >>", "INNER")] {
|
||||
let mut fixture = objects(&page, &outer, "/Inner Do");
|
||||
fixture[6] = form(inner, GLYPHS);
|
||||
assert_text(pdf(&fixture), 0, expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shared_form_uses_each_pages_properties() {
|
||||
let first = format!("/Resources {}", resources("/Properties << /P << /ActualText (FIRST) >> >>"));
|
||||
let second = format!("/Resources {}", resources("/Properties << /P << /ActualText (SECOND) >> >>"));
|
||||
let mut fixture = objects(&first, "", GLYPHS);
|
||||
fixture[1] = "<< /Type /Pages /Kids [4 0 R 10 0 R] /Count 2 >>".into();
|
||||
fixture.push(format!("<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] /Contents 5 0 R {second} >>"));
|
||||
let bytes = pdf(&fixture);
|
||||
assert_text(bytes.clone(), 0, "FIRST");
|
||||
assert_text(bytes, 1, "SECOND");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_tree_properties_inherit_only_until_an_own_resources_dictionary() {
|
||||
let root = resources("/Properties << /P << /ActualText (ROOT) >> >>");
|
||||
let near = resources("/Properties << /P << /ActualText (NEAR) >> >>");
|
||||
for (node, page, expected) in [
|
||||
("".to_string(), "".to_string(), "ROOT"),
|
||||
(format!("/Resources {near}"), "".to_string(), "NEAR"),
|
||||
("/Resources << >>".to_string(), "".to_string(), "glyphs"),
|
||||
("".to_string(), "/Resources << >>".to_string(), "glyphs"),
|
||||
] {
|
||||
let mut fixture = objects(&page, "", GLYPHS);
|
||||
fixture[1] = format!("<< /Type /Pages /Resources {root} /Kids [10 0 R] /Count 1 >>");
|
||||
fixture[3] = fixture[3].replace("/Parent 2 0 R", "/Parent 10 0 R");
|
||||
fixture.push(format!("<< /Type /Pages /Parent 2 0 R /Kids [4 0 R] /Count 1 {node} >>"));
|
||||
assert_text(pdf(&fixture), 0, expected);
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_paint(bytes: Vec<u8>, page: usize, layers: Vec<(i32, i32, bool)>, visible: bool) {
|
||||
let mut renderer = PageRenderer::new(Arc::new(bytes), RenderConfig { layers: Arc::new(layers), ..Default::default() });
|
||||
let image = renderer.render(RenderRequest { page, scale: 1.0, ..Default::default() });
|
||||
assert!(image.error.is_none(), "{:?}", image.error);
|
||||
assert_eq!((image.width, image.height), (200, 200));
|
||||
let pixel = |x: usize, y: usize| &image.rgba[(y * 200 + x) * 4..(y * 200 + x) * 4 + 4];
|
||||
assert_eq!(pixel(60, 90), [0, 0, 255, 255], "unmarked control remains blue");
|
||||
assert_eq!(pixel(20, 90), if visible { [255, 0, 0, 255] } else { [255; 4] });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn indirect_page_properties_preserve_ocg_identity_and_visibility_overrides() {
|
||||
let page = format!("/Resources {}", resources("/Properties 10 0 R"));
|
||||
let mut fixture = objects(&page, "", OFF_PAINT);
|
||||
fixture.push("<< /O 8 0 R >>".into());
|
||||
let bytes = pdf(&fixture);
|
||||
assert_paint(bytes.clone(), 0, vec![], false);
|
||||
assert_paint(bytes.clone(), 0, vec![(9, 0, true)], false);
|
||||
assert_paint(bytes.clone(), 0, vec![(8, 0, true)], true);
|
||||
assert_paint(bytes, 0, vec![(8, 0, false)], false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested_ocg_uses_page_origin_and_own_dictionary_boundaries() {
|
||||
let page = format!("/Resources {}", resources("/Properties << /O 8 0 R >>"));
|
||||
let caller = format!("/Resources {}", resources("/Properties << /O 9 0 R >>"));
|
||||
for (inner, visible) in [
|
||||
("", false),
|
||||
("/Resources << /Properties << /O 9 0 R >> >>", true),
|
||||
("/Resources << >>", true),
|
||||
("/Resources << /Properties << /Other 8 0 R >> >>", true),
|
||||
("/Resources << /Properties << /O 42 >> >>", true),
|
||||
("/Resources << /Properties << /O 99 0 R >> >>", true),
|
||||
] {
|
||||
let mut fixture = objects(&page, &caller, "/Inner Do");
|
||||
fixture[6] = form(inner, OFF_PAINT);
|
||||
assert_paint(pdf(&fixture), 0, vec![], visible);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn direct_and_inline_properties_and_unmarked_content_keep_existing_behavior() {
|
||||
let page = format!("/Resources {}", resources("/Properties << /P << /ActualText (DIRECT) >> >>"));
|
||||
for (body, expected) in [
|
||||
(GLYPHS, "DIRECT"),
|
||||
("/Span << /ActualText (INLINE) >> BDC BT /F1 12 Tf 10 150 Td (glyphs) Tj ET EMC", "INLINE"),
|
||||
("/Span BMC BT /F1 12 Tf 10 150 Td (glyphs) Tj ET EMC", "glyphs"),
|
||||
("/Span << /ActualText (OUTER) >> BDC /Span /P BDC BT /F1 12 Tf 10 150 Td (glyphs) Tj ET EMC EMC", "OUTER"),
|
||||
] {
|
||||
assert_text(pdf(&objects(&page, "", body)), 0, expected);
|
||||
}
|
||||
let inline_ref = OFF_PAINT.replace("/O BDC", "<< /OC 8 0 R >> BDC");
|
||||
assert_paint(pdf(&objects(&page, "", &inline_ref)), 0, vec![], false);
|
||||
assert_paint(pdf(&objects(&page, "", &inline_ref)), 0, vec![(8, 0, true)], true);
|
||||
let direct_ocg = format!("/Resources {}", resources("/Properties << /O << /Type /OCG /Name (Direct) >> >>"));
|
||||
assert_paint(pdf(&objects(&direct_ocg, "", OFF_PAINT)), 0, vec![], true);
|
||||
assert_paint(pdf(&objects(&page, "", &OFF_PAINT.replace("/O BDC", "<< /Type /OCG /Name (Inline) >> BDC"))), 0, vec![], true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn save_reopen_preserves_missing_resources_and_effective_named_properties() {
|
||||
let page = format!("/Resources {}", resources("/Properties << /P << /ActualText (PAGE) >> /O 8 0 R >>"));
|
||||
for own in ["", "/Resources << /Properties << /P << /ActualText (PAGE) >> /O 8 0 R >> >>"] {
|
||||
let bytes = pdf(&objects(&page, own, GLYPHS));
|
||||
let mut document = lopdf::Document::load_mem(&bytes).unwrap();
|
||||
let before = document.get_object((6, 0)).unwrap().as_stream().unwrap().dict.clone();
|
||||
let mut saved = Vec::new();
|
||||
document.save_to(&mut saved).unwrap();
|
||||
let reopened = lopdf::Document::load_mem(&saved).unwrap();
|
||||
assert_eq!(reopened.get_object((6, 0)).unwrap().as_stream().unwrap().dict, before);
|
||||
assert_text(saved, 0, "PAGE");
|
||||
let painted = objects(&page, own, OFF_PAINT);
|
||||
let mut document = lopdf::Document::load_mem(&pdf(&painted)).unwrap();
|
||||
let mut saved = Vec::new();
|
||||
document.save_to(&mut saved).unwrap();
|
||||
assert_paint(saved, 0, vec![], false);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shared_form_uses_each_pages_ocg_reference() {
|
||||
let first = format!("/Resources {}", resources("/Properties << /O 8 0 R >>"));
|
||||
let second = format!("/Resources {}", resources("/Properties << /O 9 0 R >>"));
|
||||
let mut fixture = objects(&first, "", OFF_PAINT);
|
||||
fixture[1] = "<< /Type /Pages /Kids [4 0 R 10 0 R] /Count 2 >>".into();
|
||||
fixture.push(format!("<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] /Contents 5 0 R {second} >>"));
|
||||
let bytes = pdf(&fixture);
|
||||
assert_paint(bytes.clone(), 0, vec![], false);
|
||||
assert_paint(bytes.clone(), 1, vec![], true);
|
||||
assert_paint(bytes.clone(), 0, vec![(8, 0, true)], true);
|
||||
assert_paint(bytes, 1, vec![(9, 0, false)], false);
|
||||
}
|
||||
+3
-1
@@ -1,5 +1,7 @@
|
||||
# PdfCraft roadmap: milestones and what's next
|
||||
|
||||
> **Last reviewed:** 2026-10-20 | **Last updated:** 2026-10-20 | **Change:** minor (M2, #3574: named form properties retain page origin and own dictionary boundaries; estimates unchanged)
|
||||
|
||||
> **Last reviewed:** 2026-10-18 · **Last updated:** 2026-10-18 · **Change:** minor (M5, PR #1971: bounded nested FDF export excludes source page/page-tree dictionaries; refreshed against upstream 4db6ac4a; estimates unchanged)
|
||||
> **Last reviewed:** 2026-10-15 · **Last updated:** 2026-10-15 · **Change:** minor (M14/#2383 GPU-limit candidate; estimates unchanged) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS)
|
||||
> **Last reviewed:** 2026-10-15 · **Last updated:** 2026-10-15 · **Change:** minor (M7: the Edit text caret is as tall as the row it is on; estimates unchanged) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS)
|
||||
@@ -111,7 +113,7 @@ total matches the parity estimate.
|
||||
|---|---|---|---|---|---|
|
||||
| M0 | Skeleton: workspace, xtask gates, CI | 15–30 | 98% | 1–2 | GitHub workflow, `deny.toml` (licence audit of every dependency), parity checklist (826 features, `xtask parity`) done. Missing: remaining crate stubs, testkit/oracle crates |
|
||||
| M1 | COS: filters, crypt, parser, xref, writer | 120–200 | 80% | 20–40 | Done:<br>- filters and crypt: every standard-security revision R2–R6 (RC4, AES-128/256), SASLprep, permissions, creating encryption;<br>- cos: parse and repair (including invalid xref object-number ranges), decoded-stream limits including unfiltered data, decrypt on load, re-encrypt on save, incremental and full writing.<br>Corpus: open/edit/save passes on 958 files, and all 7 password-protected files open.<br>Full saves now pack objects into compressed object streams. An unchanged incremental save of a classic table writes a zero-length cross-reference subsection instead of a section with no subsections (#1339). A full rewrite keeps unknown trailer entries, including objects reachable only from them (#1338). Fuzzing runs nightly (`xtask fuzz`). Raw-deflate fallback preserves decoding-limit errors, including through tolerant COS stream decoding. Missing: ≥ 250 tests, own image codecs |
|
||||
| M2 | Model, render, text | 200–350 | 15% | 130–230 | hayro bootstrap renderer (vendored patches); inspector predictor scratch is bounded by available input, and JPX sample rescaling handles 1–32 bits safely. Text extraction reaches word-F1 0.98 against pdftotext. Short same-line redraws split before placement rather than interleaving. Missing: model crate, fonts, DisplayList, renderer independent of hayro. Inspector page-label traversal and output are resource-bounded, with physical-number fallback warnings. Packed TIFF-predictor RGB rows retain later rows in decoding and rendering. Page marks, added content and print output retain small page-unit transform coefficients when serialized. |
|
||||
| M2 | Model, render, text | 200–350 | 15% | 130–230 | hayro bootstrap renderer (vendored patches); inspector predictor scratch is bounded by available input, and JPX sample rescaling handles 1–32 bits safely. Text extraction reaches word-F1 0.98 against pdftotext. Short same-line redraws split before placement rather than interleaving. Missing: model crate, fonts, DisplayList, renderer independent of hayro. Inspector page-label traversal and output are resource-bounded, with physical-number fallback warnings. Packed TIFF-predictor RGB rows retain later rows in decoding and rendering. Page marks, added content and print output retain small page-unit transform coefficients when serialized. Forms without Resources use page Properties for ActualText and layer visibility; own dictionaries stop fallback. |
|
||||
| M2 | Model, render, text | 200–350 | 15% | 130–230 | hayro bootstrap renderer (vendored patches); inspector predictor scratch is bounded by available input, and JPX sample rescaling handles 1–32 bits safely. Text extraction reaches word-F1 0.98 against pdftotext. Short same-line redraws split before placement rather than interleaving. Missing: model crate, fonts, DisplayList, renderer independent of hayro. Inspector page-label traversal and output are resource-bounded, with physical-number fallback warnings. Packed TIFF-predictor RGB rows retain later rows in decoding and rendering. The renderer keeps its one-point crop-size floor in physical points after UserUnit scaling, with regression coverage for crop origins, rotations and f32 reciprocal rounding. Existing unit policy and nearly-zero fallback are retained. |
|
||||
| M2 | Model, render, text | 200–350 | 15% | 130–230 | hayro bootstrap renderer (vendored patches); inspector predictor scratch is bounded by available input, and JPX sample rescaling handles 1–32 bits safely. Text extraction reaches word-F1 0.98 against pdftotext. Short same-line redraws split before placement rather than interleaving. Missing: model crate, fonts, DisplayList, renderer independent of hayro. Inspector page-label traversal and output are resource-bounded, with physical-number fallback warnings |
|
||||
| M3 | Viewer app (native + web) | 80–150 | 90% | 10–20 | Acrobat-style shell, find, select, panels, tiles, web build, UI control channel for agents (opt-in). Missing: 60 fps test on a 500-page document, snapshot tests of every panel<br>Done since: fit visible, document title in the window; Linux middle-button scrolling; shared three-state theme preferences in the toolbar, View menu and control channel; numeric Linux portal events and idle wakeups completing #881, with native appearance inheritance retained (#446); late browser startup downloads preserve the current document and Save target (#171). Selected-text Copy in Edit and the canvas context menu is covered by focused regressions and native clipboard checks (#1225); document copy restrictions apply to the floating action too. #1364 candidate: absolute local GoToR links ask before target access; headless callers explicitly opt in. Native-opening candidate: chosen tools survive preparation and password retries; startup intake waits for pending preparation. Page-label navigation displays logical labels, retains the current page on unchanged submission and refreshes focused same-document replacements; clean file-watcher reload preserves the chosen scrolling mode, preventing a Single Page reader from jumping to page 1 during replacement. Synthetic control regressions cover these behaviors. #3149: page-list changes discard render demand based on old geometry; actual raster dimensions are checked against the GPU limit before upload. Synthetic crash and continued-rendering regressions cover the replacement path. Save-prompt shortcuts yield to dialogs above them; stacked link-consent UI regression covers Escape and Return. |
|
||||
|
||||
Vendored
+6
File diff suppressed because one or more lines are too long
+4
-3
@@ -64,7 +64,7 @@ pub(crate) struct FormXObject<'a> {
|
||||
is_isolated: bool,
|
||||
is_knockout: bool,
|
||||
pub(crate) dict: Dict<'a>,
|
||||
resources: Dict<'a>,
|
||||
resources: Option<Dict<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> FormXObject<'a> {
|
||||
@@ -73,7 +73,8 @@ impl<'a> FormXObject<'a> {
|
||||
|
||||
// PdfCraft patch: see `context::MAX_PAGE_CONTENT`.
|
||||
let decoded = crate::context::decode_content(stream)?;
|
||||
let resources = dict.get::<Dict<'_>>(RESOURCES).unwrap_or_default();
|
||||
// PdfCraft patch: retain absence; present malformed dictionaries stay local-empty.
|
||||
let resources = dict.contains_key(RESOURCES).then(|| dict.get::<Dict<'_>>(RESOURCES).unwrap_or_default());
|
||||
|
||||
let matrix = Affine::new(
|
||||
dict.get::<[f64; 6]>(MATRIX)
|
||||
@@ -189,7 +190,7 @@ pub(crate) fn draw_form_xobject<'a, 'b>(
|
||||
|
||||
interpret(
|
||||
iter,
|
||||
&Resources::from_parent(x_object.resources.clone(), resources.clone()),
|
||||
&Resources::for_form(x_object.resources.clone(), resources.clone()),
|
||||
context,
|
||||
device,
|
||||
);
|
||||
|
||||
Vendored
+30
-5
@@ -148,8 +148,8 @@ fn resolve_pages<'a>(
|
||||
ctx.rotate = Some(rotate);
|
||||
}
|
||||
|
||||
let resources = Resources::from_parent(
|
||||
pages_dict.get::<Dict<'_>>(RESOURCES).unwrap_or_default(),
|
||||
let resources = Resources::for_page(
|
||||
pages_dict.contains_key(RESOURCES).then(|| pages_dict.get::<Dict<'_>>(RESOURCES).unwrap_or_default()),
|
||||
resources.clone(),
|
||||
);
|
||||
|
||||
@@ -431,8 +431,8 @@ impl<'a> Page<'a> {
|
||||
.map_or(1.0, |u| u.min(MAX_USER_UNIT) as f32);
|
||||
|
||||
let ctx = resources.ctx.clone();
|
||||
let resources = Resources::from_parent(
|
||||
dict.get::<Dict<'_>>(RESOURCES).unwrap_or_default(),
|
||||
let resources = Resources::for_page(
|
||||
dict.contains_key(RESOURCES).then(|| dict.get::<Dict<'_>>(RESOURCES).unwrap_or_default()),
|
||||
resources,
|
||||
);
|
||||
|
||||
@@ -674,11 +674,13 @@ impl<'a> Page<'a> {
|
||||
pub struct Resources<'a> {
|
||||
parent: Option<Box<Self>>,
|
||||
ctx: ReaderContext<'a>,
|
||||
// PdfCraft patch: legacy forms without Resources use the page, not their caller.
|
||||
page_properties: Dict<'a>,
|
||||
/// The raw dictionary of external graphics states.
|
||||
pub ext_g_states: Dict<'a>,
|
||||
/// The raw dictionary of fonts.
|
||||
pub fonts: Dict<'a>,
|
||||
/// The raw dictionary of properties.
|
||||
/// The effective property dictionary selected at page or form entry.
|
||||
pub properties: Dict<'a>,
|
||||
/// The raw dictionary of color spaces.
|
||||
pub color_spaces: Dict<'a>,
|
||||
@@ -698,6 +700,26 @@ impl<'a> Resources<'a> {
|
||||
Self::new(resources, Some(parent), &ctx)
|
||||
}
|
||||
|
||||
// PdfCraft patch: page-tree inheritance stops at any present Resources dictionary.
|
||||
fn for_page(resources: Option<Dict<'a>>, parent: Self) -> Self {
|
||||
let properties = resources.as_ref().map_or_else(|| parent.properties.clone(), |dict| dict.get::<Dict<'_>>(PROPERTIES).unwrap_or_default());
|
||||
let mut result = Self::from_parent(resources.unwrap_or_default(), parent);
|
||||
result.properties = properties.clone();
|
||||
result.page_properties = properties;
|
||||
result
|
||||
}
|
||||
|
||||
/// Create form resources, using page Properties only when Resources is absent.
|
||||
/// Other resource categories retain their existing parent-chain lookup.
|
||||
// PdfCraft patch: ISO 32000-2, 7.8.3 and Table 93 require the page origin.
|
||||
pub fn for_form(resources: Option<Dict<'a>>, parent: Self) -> Self {
|
||||
let properties =
|
||||
resources.as_ref().map_or_else(|| parent.page_properties.clone(), |dict| dict.get::<Dict<'_>>(PROPERTIES).unwrap_or_default());
|
||||
let mut result = Self::from_parent(resources.unwrap_or_default(), parent);
|
||||
result.properties = properties;
|
||||
result
|
||||
}
|
||||
|
||||
/// Create a new `Resources` object.
|
||||
pub(crate) fn new(resources: Dict<'a>, parent: Option<Self>, ctx: &ReaderContext<'a>) -> Self {
|
||||
let ext_g_states = resources.get::<Dict<'_>>(EXT_G_STATE).unwrap_or_default();
|
||||
@@ -708,6 +730,8 @@ impl<'a> Resources<'a> {
|
||||
let shadings = resources.get::<Dict<'_>>(SHADING).unwrap_or_default();
|
||||
let properties = resources.get::<Dict<'_>>(PROPERTIES).unwrap_or_default();
|
||||
|
||||
// PdfCraft patch: generic child resources retain the page origin.
|
||||
let page_properties = parent.as_ref().map_or_else(|| properties.clone(), |parent| parent.page_properties.clone());
|
||||
let parent = parent.map(Box::new);
|
||||
|
||||
Self {
|
||||
@@ -716,6 +740,7 @@ impl<'a> Resources<'a> {
|
||||
fonts,
|
||||
color_spaces,
|
||||
properties,
|
||||
page_properties,
|
||||
x_objects,
|
||||
patterns,
|
||||
shadings,
|
||||
|
||||
Reference in New Issue
Block a user