diff --git a/crates/pdf/src/export.rs b/crates/pdf/src/export.rs index 529396fce..71c9ab25d 100644 --- a/crates/pdf/src/export.rs +++ b/crates/pdf/src/export.rs @@ -94,6 +94,9 @@ pub(crate) struct Exporter<'a> { layer_index: HashMap, /// The address of the layer whose form is being drawn. in_layer: usize, + /// The paper of the page being drawn, in its drawing space: the box of each layer's form + /// covers it ([`crate::forms::layer_form`]). + paper: Rect, /// Overprinting fills and strokes are drawn as forms marked to overprint. pub overprint: bool, /// An overprinting fill or stroke was drawn so. diff --git a/crates/pdf/src/export/exporter.rs b/crates/pdf/src/export/exporter.rs index ac53090f5..a40e272b0 100644 --- a/crates/pdf/src/export/exporter.rs +++ b/crates/pdf/src/export/exporter.rs @@ -45,6 +45,7 @@ impl<'a> Exporter<'a> { layers: false, layer_index: crate::forms::pdf_layers(doc).iter().enumerate().map(|(i, l)| (address(l.node), i)).collect(), in_layer: 0, + paper: Rect::ZERO, symbol_nodes: 0, overprint: false, overprinted: false, diff --git a/crates/pdf/src/export/nodes.rs b/crates/pdf/src/export/nodes.rs index 8367bef11..7d063046c 100644 --- a/crates/pdf/src/export/nodes.rs +++ b/crates/pdf/src/export/nodes.rs @@ -102,7 +102,8 @@ impl Exporter<'_> { { // A PDF layer: hidden layers and sublayers are written too (their group is off). let enclosing = std::mem::replace(&mut self.in_layer, address(n)); - crate::forms::form(s, Mark::Layer(i), |s| self.node(s, n, page, true)); + let paper = self.paper; + crate::forms::layer_form(s, i, paper, |s| self.node(s, n, page, true)); self.in_layer = enclosing; return; } diff --git a/crates/pdf/src/export/writer.rs b/crates/pdf/src/export/writer.rs index b2ca788eb..5e7c7e77a 100644 --- a/crates/pdf/src/export/writer.rs +++ b/crates/pdf/src/export/writer.rs @@ -156,6 +156,8 @@ impl Writer { s.push_mask(all); } if fits { + let shown = (sheet.view * sheet.place).inverse().transform_rect_bbox(paper); + ex.paper = if [shown.x0, shown.y0, shown.x1, shown.y1].iter().all(|v| v.is_finite()) { shown.union(r) } else { r }; ex.children(&mut s, &doc.layers, r); } if page_group || cmyk_page_group { diff --git a/crates/pdf/src/forms.rs b/crates/pdf/src/forms.rs index e382ccb0f..4a1d06147 100644 --- a/crates/pdf/src/forms.rs +++ b/crates/pdf/src/forms.rs @@ -4,9 +4,10 @@ //! form is. Marked forms also stay apart (the writer shares identical forms). [`finish`] completes //! them in the written file: a layer's form is drawn as the layer's optional content (`/OC … //! BDC`), an overprinting form sets a graphics state that overprints first, and the text of an -//! invisible form is drawn in rendering mode 3 (neither filled nor stroked). +//! invisible form is drawn in rendering mode 3 (neither filled nor stroked). It also removes the +//! marks, so no form keeps an empty clip far off the page (#2181). -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::io::Read; use craft_pdf::write::backend as krilla; @@ -77,13 +78,30 @@ impl Mark { /// Draw `draw` as a form of its own, marked `mark`. pub(crate) fn form(s: &mut Surface, mark: Mark, draw: impl FnOnce(&mut Surface)) { + let (x, y) = mark.at(); + marked(s, mark, [(x + 1.0, y), (x + 1.0, y + 1.0)], draw); +} + +/// Draw `draw` (the art of layer `i` of [`pdf_layers`]) as a form of its own, marked as the +/// layer's. The mark's other two points are the corners of `paper` (the page's paper in the +/// form's drawing space), rounded outwards to whole units: [`finish`] gives the form a box that +/// covers the paper and the art. +pub(crate) fn layer_form(s: &mut Surface, i: usize, paper: vectorcraft_geom::Rect, draw: impl FnOnce(&mut Surface)) { + let corner = |x: f64, y: f64| (x as f32, y as f32); + marked(s, Mark::Layer(i), [corner(paper.x0.floor(), paper.y0.floor()), corner(paper.x1.ceil(), paper.y1.ceil())], draw); +} + +/// Draw `draw` as a form of its own, marked `mark`: an empty clip from the mark's first point +/// through `rest`. +fn marked(s: &mut Surface, mark: Mark, rest: [(f32, f32); 2], draw: impl FnOnce(&mut Surface)) { let mut sb = s.stream_builder(); let mut fs = sb.surface(); let (x, y) = mark.at(); let mut pb = PathBuilder::new(); pb.move_to(x, y); - pb.line_to(x + 1.0, y); - pb.line_to(x + 1.0, y + 1.0); + for (x, y) in rest { + pb.line_to(x, y); + } pb.close(); // A form's drawing space is its own: the mark's points are written as they are. if let Some(p) = pb.finish() { @@ -102,8 +120,9 @@ pub(crate) fn form(s: &mut Surface, mark: Mark, draw: impl FnOnce(&mut Surface)) const OVERPRINT_GS: &str = "VCop"; /// The prefix of the resource names of the layers' optional content groups (then the index). const LAYER_OC: &str = "VCoc"; -/// The most bytes of a form's content read to find its mark. -const HEAD: u64 = 64; +/// The most bytes of a form's content read to find its mark (a layer's mark holds four more +/// numbers of up to 11 characters each). +const HEAD: u64 = 128; fn bad(why: &str) -> PdfError { PdfError::Write(format!("can't write PDF layers, overprint or invisible text: {why}")) @@ -159,6 +178,31 @@ impl Object<'_> { _ => None, } } + + /// The paper a layer's form's mark gives ([`layer_form`]): its left, top, right and bottom. + fn paper(&self, pdf: &[u8]) -> Option<[i64; 4]> { + let head = self.data(pdf, HEAD)?; + let mut lx = Lexer::at(&head, 0); + let mut word = || match lx.next() { + Some((Tok::Word(w), ..)) => Some(w), + _ => None, + }; + let words = [(); 10].map(|_| word()); + let [Some(b"q"), _, _, Some(b"m"), x0, y0, Some(b"l"), x1, y1, Some(b"l")] = words else { return None }; + Some([x0.and_then(int)?, y0.and_then(int)?, x1.and_then(int)?, y1.and_then(int)?]) + } +} + +/// `content` (a marked form's) without its mark: what follows the mark's `Q`. +fn unmarked(content: &[u8]) -> Option<&[u8]> { + let mut lx = Lexer::at(content, 0); + while let Some((tok, _, end)) = lx.next() { + if let Tok::Word(b"Q") = tok { + let rest = content.get(end..)?; + return rest.get(rest.iter().take_while(|c| c.is_ascii_whitespace()).count()..); + } + } + None } /// The objects of the written file that have a dictionary, by number. @@ -293,7 +337,7 @@ pub(crate) fn finish(pdf: Vec, doc: &Document, set: &PdfSettings, layers: bo return Err(bad("an overprinting form can't be read")); }; let mut new = format!("/{OVERPRINT_GS} gs\n").into_bytes(); - new.extend_from_slice(&content); + new.extend_from_slice(unmarked(&content).ok_or_else(|| bad("an overprinting form can't be read"))?); o.rewrite(&mut patch, &new)?; add_resources(&mut patch, res, "ExtGState", &format!("/{OVERPRINT_GS} {gs} 0 R"))?; } @@ -302,15 +346,19 @@ pub(crate) fn finish(pdf: Vec, doc: &Document, set: &PdfSettings, layers: bo for (n, _) in marks.iter().filter(|(_, m)| **m == Mark::Invisible) { let Some(o) = objects.get(n) else { continue }; let content = o.data(&pdf, u64::MAX).ok_or_else(|| bad("an invisible text form can't be read"))?; - o.rewrite(&mut patch, &invisible_text(&content).ok_or_else(|| bad("an invisible text form can't be read"))?)?; + let text = unmarked(&content).and_then(invisible_text).ok_or_else(|| bad("an invisible text form can't be read"))?; + o.rewrite(&mut patch, &text)?; } } if layers { - write_layers(&pdf, &xref, &objects, &marks, &mut patch, doc, set)?; + let rewritten = write_layers(&pdf, &xref, &objects, &marks, &mut patch, doc, set)?; for (n, _) in marks.iter().filter(|(_, m)| matches!(m, Mark::Layer(_))) { - if let Some(o) = objects.get(n) { - unbox(&pdf, o, &mut patch)?; + let Some(o) = objects.get(n) else { continue }; + if !rewritten.contains(n) { + let content = o.data(&pdf, u64::MAX).ok_or_else(|| bad("a layer's form can't be read"))?; + o.rewrite(&mut patch, unmarked(&content).ok_or_else(|| bad("a layer's form can't be read"))?)?; } + rebox(&pdf, o, &mut patch)?; } } let pdf = patch.apply(&pdf, &xref)?; @@ -321,7 +369,7 @@ pub(crate) fn finish(pdf: Vec, doc: &Document, set: &PdfSettings, layers: bo /// The optional content groups of `doc`'s layers and sublayers ([`pdf_layers`]), the uses of their /// forms marked as their content, and the catalog's `/OCProperties`, listing sublayers under their -/// layers. +/// layers. Returns the content streams it rewrote (a layer's form among them without its mark). fn write_layers( pdf: &[u8], xref: &Xref, @@ -330,7 +378,7 @@ fn write_layers( patch: &mut Patch, doc: &Document, set: &PdfSettings, -) -> Result<(), PdfError> { +) -> Result, PdfError> { // One group per layer, in paint order (bottom first). let layers = pdf_layers(doc); let mut groups: HashMap = HashMap::new(); @@ -357,13 +405,14 @@ fn write_layers( } } // Every content stream drawing layers' forms: the pages' and the forms'. - for o in objects.values() { + let mut rewritten = HashSet::new(); + for (n, o) in objects { let content = match o.dict.get(b"Contents") { - Some(Obj::Ref(c, _)) if o.dict.name(b"Type") == Some(b"Page") => objects.get(c), - _ if o.dict.name(b"Subtype") == Some(b"Form") => Some(o), + Some(Obj::Ref(c, _)) if o.dict.name(b"Type") == Some(b"Page") => objects.get(c).map(|o| (*c, o)), + _ if o.dict.name(b"Subtype") == Some(b"Form") => Some((*n, o)), _ => None, }; - let (Some(content), Some(res)) = (content, resources(objects, o)) else { continue }; + let (Some((number, content)), Some(res)) = (content, resources(objects, o)) else { continue }; let Some(Obj::Dict { entries, .. }) = res.get(b"XObject") else { continue }; let forms: HashMap<&[u8], usize> = entries .iter() @@ -379,8 +428,11 @@ fn write_layers( continue; } let data = content.data(pdf, u64::MAX).ok_or_else(|| bad("a content stream can't be read"))?; - let Some((new, marked)) = mark_layers(&data, &forms) else { continue }; + // A layer's form drawing its sublayers' forms loses its own mark here too. + let data = if marks.contains_key(&number) { unmarked(&data).ok_or_else(|| bad("a layer's form can't be read"))? } else { &data[..] }; + let Some((new, marked)) = mark_layers(data, &forms) else { continue }; content.rewrite(patch, &new)?; + rewritten.insert(number); let entries: String = marked.iter().filter_map(|i| Some(format!("/{LAYER_OC}{i} {} 0 R", groups.get(i)?))).collect(); add_resources(patch, res, "Properties", &entries)?; } @@ -399,24 +451,41 @@ fn write_layers( let root = xref.trailer_ref(pdf, b"/Root").and_then(|n| objects.get(&n)); let Some(Object { dict: Obj::Dict { start, .. }, .. }) = root else { return Err(bad("the written PDF has no catalog")) }; patch.replace(start + 2, start + 2, format!("/OCProperties<>>>", refs(&all)).into_bytes()); - Ok(()) + Ok(rewritten) } -/// A form's box as large as a page can be: the box of a layer's form is only the bounds of its -/// art, and a reader that makes a clipping group of a form's box (as importers do) would gather a -/// layer's art and its sublayers' in one group, which no layer can hold. A box round the whole -/// page clips nothing. +/// A form's box as large as a page can be, for a layer's form whose mark gives no paper. const UNBOXED: &[u8] = b"[-32767 -32767 32767 32767]"; -/// Give the layer's form `o` the [`UNBOXED`] box. -fn unbox(pdf: &[u8], o: &Object<'_>, patch: &mut Patch) -> Result<(), PdfError> { +/// Give the layer's form `o` a box round its art and the paper its mark gives ([`layer_form`]): +/// the box of a layer's form is only the bounds of its art, and a reader that makes a clipping +/// group of a form's box (as importers do) would gather a layer's art and its sublayers' in one +/// group, which no layer can hold. A box round the whole paper clips nothing, and its bounds stay +/// those of the page and the art (#2181). +fn rebox(pdf: &[u8], o: &Object<'_>, patch: &mut Patch) -> Result<(), PdfError> { let Obj::Dict { start, end, .. } = o.dict else { return Err(bad("a layer's form has no dictionary")) }; let dict = pdf.get(start..end).ok_or_else(|| bad("a layer's form can't be read"))?; // The writer writes the box as an array of numbers. let at = dict.windows(5).position(|w| w == b"/BBox").ok_or_else(|| bad("a layer's form has no box"))? + 5; let open = at + dict.get(at..).and_then(|d| d.iter().position(|&c| c == b'[')).ok_or_else(|| bad("a layer's form box isn't an array"))?; let close = open + dict.get(open..).and_then(|d| d.iter().position(|&c| c == b']')).ok_or_else(|| bad("a layer's form box isn't an array"))? + 1; - patch.replace(start + open, start + close, UNBOXED.to_vec()); + let art: Vec = std::str::from_utf8(dict.get(open + 1..close - 1).unwrap_or_default()) + .unwrap_or_default() + .split_ascii_whitespace() + .filter_map(|v| v.parse().ok()) + .collect(); + let new = match (o.paper(pdf), art.as_slice()) { + (Some([x0, y0, x1, y1]), &[a0, b0, a1, b1]) if [a0, b0, a1, b1].iter().all(|v| v.is_finite()) => { + let (a0, a1, b0, b1) = (a0.min(a1), a0.max(a1), b0.min(b1), b0.max(b1)); + // Whole units, outwards (the art's box is written to a few decimals). + let lo = |v: i64, a: f64| (v as f64).min(a.floor()); + let hi = |v: i64, a: f64| (v as f64).max(a.ceil()); + format!("[{} {} {} {}]", lo(x0, a0), lo(y0, b0), hi(x1, a1), hi(y1, b1)).into_bytes() + } + (Some([x0, y0, x1, y1]), _) => format!("[{x0} {y0} {x1} {y1}]").into_bytes(), + _ => UNBOXED.to_vec(), + }; + patch.replace(start + open, start + close, new); Ok(()) } diff --git a/crates/pdf/src/tests/layers.rs b/crates/pdf/src/tests/layers.rs index 05ad63106..cb5bf9a12 100644 --- a/crates/pdf/src/tests/layers.rs +++ b/crates/pdf/src/tests/layers.rs @@ -339,6 +339,45 @@ fn overprinting_art_imports_as_it_was_drawn() { } } +/// The `/BBox` of each form of the uncompressed `bytes`, in the order written. +fn form_boxes(bytes: &[u8]) -> Vec { + let text = String::from_utf8_lossy(bytes); + text.split("/Subtype/Form").skip(1).filter_map(|d| Some(d.split("/BBox[").nth(1)?.split(']').next()?.to_string())).collect() +} + +/// #2181: a layer's form has a box round the paper and its art, not one 23 m across, and no +/// form (a layer's, an overprinting one or invisible text's) keeps the empty clip far off the +/// page that marked it while the file was written. +#[test] +fn layer_forms_have_real_boxes_and_no_marks() { + let mut d = overprinting(RED); + // Invisible text (neither fill nor stroke) and art across the paper's left edge. + let l = d.layers[0].id; + let style = CharStyle { size: 24.0, fill: Paint::None, ..Default::default() }; + let t = Node::new(d.alloc_id(), NodeKind::Text(Box::new(TextObject::point(Point::new(200.0, 40.0), "Hid", style)))); + let past = Node::path(d.alloc_id(), shapes::rectangle(Rect::new(-50.0, 10.0, 5.0, 40.0)), Appearance::basic(Paint::solid(RED), Paint::None, 0.0)); + d.insert(Some(l), 0, t).unwrap(); + d.insert(Some(l), 0, past).unwrap(); + let mut sub = Node::layer(d.alloc_id(), "Sub", LayerColor::Preset(1)); + if let NodeKind::Layer { children, .. } = &mut sub.kind { + children.push(rect(&mut d, 250.0, BLUE).into()); + } + d.insert(Some(l), 0, sub).unwrap(); + let r = export(&d, json!({"createLayers": true, "compression": {"compressText": false}})); + let text = String::from_utf8_lossy(&r.bytes); + for mark in ["-32000 ", "-32001 ", "-32002 ", "32767"] { + assert!(!text.contains(mark), "{mark} is in the file: {text}"); + } + assert!(text.contains("3 Tr") && text.contains("/VCop gs"), "invisible text and overprint are still written: {text}"); + // The layer's form reaches the art across the left edge; its sublayer's covers the paper. + let boxes = form_boxes(&r.bytes); + assert!(boxes.contains(&"-50 0 300 100".to_string()) && boxes.contains(&"0 0 300 100".to_string()), "{boxes:?}"); + // It still imports as it was drawn: a layer holding its sublayer, no clipping group. + let mut got = vec![]; + tree(&import(&r.bytes).unwrap().layers, 0, &mut got); + assert!(got.contains(&"1 Sub true".to_string()) && got.contains(&format!("2 {:?}", Some(BLUE))), "{got:?}"); +} + #[test] fn pdf_x1a_keeps_overprint() { let r = export(&overprinting(RED), json!({"standard": "pdfX1a", "compatibility": "1.4", "compression": {"compressText": false}}));