diff --git a/crates/redact/src/interp.rs b/crates/redact/src/interp.rs index 02799ffa3f..9b45a377f6 100644 --- a/crates/redact/src/interp.rs +++ b/crates/redact/src/interp.rs @@ -215,7 +215,8 @@ const MAX_CMAP_PROGRAM: usize = 4 << 20; /// renderer reads the program, not only the dictionary. Anything that can't be read fully counts /// as vertical, so redaction refuses rather than place the text horizontally. fn cmap_vertical(doc: &Document, s: &Stream, depth: usize) -> bool { - if s.dict.int(b"WMode").is_some_and(|w| w != 0) { + // (an indirect /WMode is followed: read as absent it would place vertical text horizontally) + if s.dict.get(b"WMode").is_some_and(|w| doc.resolve(w).as_int().is_some_and(|w| w != 0)) { return true; } match s.dict.get(b"UseCMap").map(|u| doc.resolve(u)).as_deref() { diff --git a/crates/redact/src/lib.rs b/crates/redact/src/lib.rs index 41ac47357e..fec83081d2 100644 --- a/crates/redact/src/lib.rs +++ b/crates/redact/src/lib.rs @@ -52,7 +52,7 @@ pub enum RedactError { #[error("{0}")] Undrawable(String), #[error( - "page {0} has vertical-writing text while a redaction mark is pending, and PdfCraft cannot tell whether that text lies under the mark yet, so nothing was changed" + "page {0} has vertical-writing text, and PdfCraft cannot tell whether that text lies under a redaction mark or off the page yet, so the operation was refused" )] VerticalText(usize), #[error( diff --git a/crates/redact/src/tests.rs b/crates/redact/src/tests.rs index 6237e8517a..4675bc0474 100644 --- a/crates/redact/src/tests.rs +++ b/crates/redact/src/tests.rs @@ -258,17 +258,32 @@ fn vertical_writing_text_under_a_mark_fails_closed() { /// A page showing CIDs 1 to 4 at x 100, y 50 in a Type0 font whose embedded CMap sets its /// writing mode with `/WMode {wmode} def` in its program only, not in its dictionary. fn cmap_program_page(wmode: u8) -> Document { + cmap_page_with(wmode, "", Vec::new()) +} + +/// [`cmap_program_page`] with `dict` added to the CMap stream's dictionary, and `extra` objects (from 7 0). +fn cmap_page_with(wmode: u8, dict: &str, extra: Vec>) -> Document { let program = format!( "/CIDInit /ProcSet findresource begin 12 dict begin begincmap /WMode {wmode} def 1 begincodespacerange <0000> endcodespacerange 1 begincidrange <0000> 0 endcidrange endcmap CMapName currentdict /CMap defineresource pop end end" ); - pdf(vec![ + let mut objects = vec![ b"<< /Type /Catalog /Pages 2 0 R >>".to_vec(), b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>".to_vec(), b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 300 300] /Contents 4 0 R /Resources << /Font << /F2 5 0 R >> >> >>".to_vec(), stream("", b"BT /F2 10 Tf 100 50 Td <0001000200030004> Tj ET"), b"<< /Type /Font /Subtype /Type0 /BaseFont /X /Encoding 6 0 R /DescendantFonts [<< /Type /Font /Subtype /CIDFontType2 /BaseFont /X /DW 1000 >>] >>".to_vec(), - stream("/Type /CMap /CMapName /Custom", program.as_bytes()), - ]) + stream(&format!("/Type /CMap /CMapName /Custom {dict}"), program.as_bytes()), + ]; + objects.extend(extra); + pdf(objects) +} + +/// An indirect `/WMode 1` in the CMap's dictionary is vertical, whatever its program says. +#[test] +fn an_indirect_wmode_in_the_cmap_dictionary_fails_closed() { + let mut vdoc = cmap_page_with(0, "/WMode 7 0 R", vec![b"1".to_vec()]); + mark(&mut vdoc, 0, &[[95.0, 40.0, 140.0, 70.0]], ""); + assert_eq!(apply(&mut vdoc, None), Err(RedactError::VerticalText(1))); } /// The renderer takes the writing mode from the CMap program; its dictionary need not repeat it.