mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-25 10:17:02 -06:00
Merge pull request #675 from fastmikey99/fix/pdf-import-past-page-clip
PDF import: clip art reaching past the page to the crop box
This commit is contained in:
+2
-2
File diff suppressed because one or more lines are too long
@@ -135,6 +135,9 @@ fn group_layer(
|
||||
pub const OFF_ARTBOARD_NOTE: &str =
|
||||
"only the PDF-compatible part of this file was read: art outside its artboards is kept in the editor's private data alone, so it doesn't open";
|
||||
|
||||
/// Warning: art reaching past the page box was clipped to it.
|
||||
pub(crate) const PAST_PAGE_NOTE: &str = "art reaching past the page was clipped to it, as PDF viewers show it";
|
||||
|
||||
/// Import a PDF, returning the document plus warnings about content that was approximated or skipped.
|
||||
pub fn import_with_report(bytes: &[u8], opts: &ImportOptions) -> Result<ImportReport, PdfError> {
|
||||
let original = crate::pages::open(bytes, opts.password.as_deref())?;
|
||||
@@ -227,6 +230,29 @@ pub fn import_with_report(bytes: &[u8], opts: &ImportOptions) -> Result<ImportRe
|
||||
}
|
||||
parts.retain(|(_, art)| !art.is_empty());
|
||||
}
|
||||
// Viewers show a page through its crop box, so art reaching past it (a mock-up backdrop
|
||||
// around the page, say) never showed. A clip to the page box that the file states is
|
||||
// dropped as redundant (by hayro and by `push_clip_path`), so restore the box here:
|
||||
// each object reaching past it keeps its own clip, which Fit to Artwork Bounds and
|
||||
// `CropTo::Bounding` then respect.
|
||||
// The crop box is what viewers show, whichever box the artboard takes. Not in a
|
||||
// PDF-compatible `.ai`: its PDF part stands in for the editable document, whose art
|
||||
// crossing the artboard edge isn't clipped, so it opens whole (#646).
|
||||
let shown = xf.transform_rect_bbox(crate::pages::page_box(page, CropTo::Crop));
|
||||
let page_box = shown.inflate(0.01, 0.01);
|
||||
let mut clipped = false;
|
||||
for (_, art) in parts.iter_mut().filter(|_| !ai) {
|
||||
for n in art.iter_mut() {
|
||||
if n.visual_bounds().is_some_and(|r| !contains(page_box, r)) {
|
||||
let clip = Arc::new(b.clip_node(&shown.to_path(0.1), FillRule::NonZero));
|
||||
*n = Arc::new(Node::new(b.id(), NodeKind::Group { children: vec![clip, n.clone()], clip: true }));
|
||||
clipped = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if clipped {
|
||||
b.warn(PAST_PAGE_NOTE);
|
||||
}
|
||||
let children: Vec<Arc<Node>> = parts.iter().flat_map(|(_, v)| v.iter().cloned()).collect();
|
||||
placeholder &= ai && only_text(&children);
|
||||
let mut right = ab.x1;
|
||||
|
||||
@@ -446,3 +446,21 @@ fn a_stroke_under_a_stretched_transform_stays_a_stroke() {
|
||||
let b = objs[0].geometric_bounds().unwrap();
|
||||
assert!((b.x0 - 20.0).abs() < 1e-6 && (b.x1 - 80.0).abs() < 1e-6, "{b:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn art_past_the_page_clip_keeps_the_clip() {
|
||||
// A page-sized clip, then a backdrop far larger than the page and a square on it: the square
|
||||
// needs no clip, the backdrop keeps it, so the art's bounds stay on the page.
|
||||
let r = open_with(&one_page("0 0 100 100 re W n 0 g -500 -500 1100 1100 re f 1 0 0 rg 10 10 20 20 re f", "", &[]), |_| {});
|
||||
let d = &r.document;
|
||||
let art = d.art_bounds().expect("art");
|
||||
assert!(art.x0 >= -0.01 && art.y0 >= -0.01 && art.x1 <= 100.01 && art.y1 <= 100.01, "the art stays on the page: {art:?}");
|
||||
let clips: Vec<Node> = all(d).into_iter().filter(|n| matches!(n.kind, NodeKind::Group { clip: true, .. })).collect();
|
||||
let [c] = clips.as_slice() else { panic!("one clip group, the backdrop's: {clips:?}") };
|
||||
assert_eq!(c.children().map(Vec::len), Some(2), "the clip path and the backdrop");
|
||||
assert!(r.warnings.iter().any(|w| w == crate::import::PAST_PAGE_NOTE), "{:?}", r.warnings);
|
||||
// Art inside the page gets no clip and no warning.
|
||||
let r = open_with(&one_page("0 0 100 100 re W n 1 0 0 rg 10 10 20 20 re f", "", &[]), |_| {});
|
||||
assert!(!all(&r.document).iter().any(|n| matches!(n.kind, NodeKind::Group { clip: true, .. })));
|
||||
assert!(!r.warnings.iter().any(|w| w == crate::import::PAST_PAGE_NOTE), "{:?}", r.warnings);
|
||||
}
|
||||
|
||||
@@ -367,6 +367,25 @@ fn art_past_the_scan_limit_of_shown_layers_stays_shown() {
|
||||
assert!(r.warnings.iter().any(|w| w.contains("went to its page's layer")), "{:?}", r.warnings);
|
||||
}
|
||||
|
||||
/// Art reaching past the page keeps a clip to it in a plain PDF (as viewers show it), but not in
|
||||
/// a PDF-compatible `.ai`, whose art crossing the artboard edge opens whole and editable (#646).
|
||||
#[test]
|
||||
fn art_past_the_page_is_clipped_in_a_pdf_but_not_in_an_ai_file() {
|
||||
for ai in [false, true] {
|
||||
let page = PdfPage {
|
||||
entries: if ai { "/PieceInfo << /Illustrator << /Private << /AIPrivateData1 7 /NumBlock 1 >> >> >>".into() } else { String::new() },
|
||||
..PdfPage::new(100.0, 100.0, "0 0 100 100 re W n 1 0 0 rg 50 50 200 200 re f")
|
||||
};
|
||||
let r = import_with_report(&pdf_with_catalog(&[page], &[], "", None), &ImportOptions::default()).unwrap();
|
||||
let mut clips = 0;
|
||||
r.document.walk(|n| clips += usize::from(matches!(n.kind, NodeKind::Group { clip: true, .. })));
|
||||
let past = r.warnings.iter().any(|w| w == crate::import::PAST_PAGE_NOTE);
|
||||
assert_eq!((clips, past), if ai { (0, false) } else { (1, true) }, "ai: {ai}: {:?}", r.warnings);
|
||||
let art = r.document.art_bounds().unwrap();
|
||||
assert_eq!(art.x1 > 100.5, ai, "ai: {ai}: {art:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// The editor's private data of a `.ai` is its `AIPrivateData` streams, joined in order.
|
||||
#[test]
|
||||
fn an_ai_files_private_data_is_its_streams_joined() {
|
||||
|
||||
Reference in New Issue
Block a user