Integration: PDF crop box is part of an asset's identity; clippings keep anchored graphics (#391)

Asset::same_file compares pdf_crop (#362) with type, page and bytes: one
PDF page shown with two crop boxes keeps two assets, so add_asset,
adopt_asset, insert_asset (IDML/INDD import) and the load/save merge
never point a placement at another crop. Test in
identical_files_share_one_asset.

The copy clipping (edit.rs clip_doc) keeps the assets used_assets finds
in it, so graphics anchored in copied text, graphic table cells (#379),
notes and media posters keep their files; it collected only graphics on
the copied items before. Test:
copied_text_frames_keep_the_graphics_anchored_in_their_text.
This commit is contained in:
aiongg
2026-10-10 23:44:15 +00:00
parent cfd57cb3ca
commit a386c73d82
2 changed files with 38 additions and 11 deletions
+11 -3
View File
@@ -15,10 +15,11 @@ use crate::{Asset, AssetId, Content, Document, Item};
const MAX_DEPTH: usize = 64;
impl Asset {
/// Whether `other` holds the same file: the same type, page and bytes. Assets without bytes
/// (a link that couldn't be read) are the same only when they link to the same path.
/// Whether `other` holds the same file: the same type, page, PDF crop box and bytes. Assets
/// without bytes (a link that couldn't be read) are the same only when they link to the same
/// path.
pub fn same_file(&self, other: &Asset) -> bool {
if self.mime != other.mime || self.page != other.page {
if self.mime != other.mime || self.page != other.page || self.pdf_crop != other.pdf_crop {
return false;
}
match (self.data.is_empty(), other.data.is_empty()) {
@@ -306,6 +307,13 @@ mod tests {
let b = d.add_asset(Asset { link: Some("/x/p.png".into()), ..asset(2, b"same") });
let c = d.add_asset(asset(3, b"diff"));
let p = d.add_asset(Asset { page: 1, ..asset(4, b"same") });
// One PDF page shown with two crop boxes: two assets (the box is the asset's).
let pdf = |id, pdf_crop| Asset { mime: "application/pdf".into(), pdf_crop, ..asset(id, b"%PDF") };
let crop = d.add_asset(pdf(9, crate::PdfCrop::Crop));
let art = d.add_asset(pdf(10, crate::PdfCrop::Art));
assert_ne!(crop, art);
assert_eq!(d.add_asset(pdf(11, crate::PdfCrop::Art)), art);
d.assets.retain(|id, _| ![crop, art].contains(id));
assert_eq!(a, b);
assert_ne!(a, c);
assert_ne!(a, p);
+27 -8
View File
@@ -300,18 +300,13 @@ pub(crate) fn clip_doc(s: &Session) -> Result<designcraft_doc::Document> {
for sp in d.spreads.iter_mut() {
Arc::make_mut(sp).items.clear();
}
// Kept frames and graphics (including inside groups).
// Kept frames (including inside groups).
let mut frames = std::collections::HashSet::new();
let mut assets = std::collections::HashSet::new();
for it in &items {
it.walk(&mut |i| match &i.content {
designcraft_doc::Content::Text(_) => {
it.walk(&mut |i| {
if let designcraft_doc::Content::Text(_) = &i.content {
frames.insert(i.id);
}
designcraft_doc::Content::Graphic(g) => {
assets.insert(g.asset);
}
_ => {}
});
}
if let Some(sp) = d.spreads.first_mut() {
@@ -325,6 +320,8 @@ pub(crate) fn clip_doc(s: &Session) -> Result<designcraft_doc::Document> {
for st in d.stories.values_mut() {
Arc::make_mut(st).frames.retain(|f| frames.contains(f));
}
// Graphics on the items and in the kept stories (anchored objects, graphic cells, notes).
let assets = d.used_assets();
d.assets.retain(|k, _| assets.contains(k));
d.hyperlinks.clear();
d.bookmarks.clear();
@@ -545,6 +542,28 @@ mod paste_tests {
assert_eq!(pasted(&mut s, "edit.pasteInPlace", json!({"spread": 1})), (SpreadRef::Doc(1), Rect::new(w + 36.0, 36.0, w + 136.0, 136.0)));
}
/// A copied text frame's clipping holds the files its anchored graphics show.
#[test]
fn copied_text_frames_keep_the_graphics_anchored_in_their_text() {
let mut s = Session::new();
s.execute("file.new", &json!({})).unwrap();
let png = designcraft_render::Rendered { width: 4, height: 4, pixels: vec![200u8; 64] }.to_png();
let placed = s.execute("file.place", &json!({"base64": super::super::file::base64_encode(&png), "name": "a.png", "x": 72, "y": 72})).unwrap();
let (g, aid) = (ItemId(placed["id"].as_u64().unwrap()), designcraft_doc::AssetId(placed["asset"].as_u64().unwrap()));
let f = ItemId(s.execute("frame.create", &json!({"rect": [36, 200, 300, 400], "content": "text"})).unwrap()["id"].as_u64().unwrap());
s.edit(|d, _| {
let it = d.item(g).cloned().ok_or(designcraft_doc::DocError::NoItem(g))?;
let sid = d.item(f).and_then(|i| i.text_frame()).map(|t| t.story).ok_or(designcraft_doc::DocError::NoItem(f))?;
super::super::anchored::anchor_items(d, sid, 0, vec![it], &Default::default())?;
Ok(())
})
.unwrap();
s.execute("selection.set", &json!({"ids": [f.0]})).unwrap();
s.execute("edit.copy", &json!({})).unwrap();
let clip = s.clipboard.clone().unwrap();
assert_eq!(clip.assets.keys().copied().collect::<Vec<_>>(), [aid]);
}
/// Undo leaves the id of the copy it removed in the selection. Copy measures from the spread
/// of the first selected object that is still there.
#[test]