1use std::collections::{HashMap, HashSet, VecDeque};
7use std::sync::Arc;
8
9use otf_pdf_syntax::{Dict, Error, Name, ObjId, Object, OpenOptions, PdfString, Result, Stream};
10
11use crate::document::{Document, Info, OutlineItem, name_tree};
12use crate::pages::{Rect, standalone_page_dict};
13use crate::store::Store;
14use crate::text::{decode_text_string, encode_text_string};
15use crate::xmp;
16
17fn empty_pdf() -> Vec<u8> {
20 let mut out = b"%PDF-1.7\n".to_vec();
21 let mut offsets = Vec::new();
22 for body in [
23 "<< /Type /Catalog /Pages 2 0 R >>",
24 "<< /Type /Pages /Kids [] /Count 0 >>",
25 ] {
26 offsets.push(out.len());
27 out.extend_from_slice(format!("{} 0 obj\n{body}\nendobj\n", offsets.len()).as_bytes());
28 }
29 let xref = out.len();
30 out.extend_from_slice(b"xref\n0 3\n0000000000 65535 f \n");
31 for o in offsets {
32 out.extend_from_slice(format!("{o:010} 00000 n \n").as_bytes());
33 }
34 out.extend_from_slice(
35 format!("trailer\n<< /Size 3 /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n").as_bytes(),
36 );
37 out
38}
39
40#[derive(Debug, Clone, Default, PartialEq, Eq)]
42pub struct AttachmentOptions {
43 pub mime_type: Option<String>,
45 pub description: Option<String>,
47 pub creation_date: Option<String>,
49 pub mod_date: Option<String>,
51}
52
53fn text(s: &str) -> Object {
54 Object::String(PdfString::literal(encode_text_string(s)))
55}
56
57impl Document {
58 pub fn new() -> Result<Self> {
60 Self::open(empty_pdf(), OpenOptions::default())
61 }
62
63 fn set_catalog(&mut self, catalog: Dict) -> Result<()> {
65 match self.catalog_id() {
66 Some(id) => self.store_mut().set(id, Object::Dict(catalog)),
67 None => {
68 let id = self.store_mut().add(Object::Dict(catalog))?;
69 self.store_mut().set_trailer_entry("Root", Object::Ref(id));
70 }
71 }
72 Ok(())
73 }
74
75 fn write_page_tree(&mut self, pages: Vec<(Option<ObjId>, Dict)>) -> Result<()> {
80 let catalog = self.catalog();
81 let tree = match catalog.get(b"Pages") {
82 Some(Object::Ref(id)) => *id,
83 _ => self.store_mut().allocate()?,
84 };
85 let mut kids = Vec::with_capacity(pages.len());
86 let mut used = HashSet::new();
87 for (id, mut dict) in pages {
88 dict.insert("Parent", tree);
89 let id = match id {
91 Some(id) if used.insert(id) => id,
92 _ => self.store_mut().allocate()?,
93 };
94 self.store_mut().set(id, Object::Dict(dict));
95 kids.push(Object::Ref(id));
96 }
97 let mut node = Dict::new();
98 node.insert("Type", Name::from("Pages"));
99 node.insert("Count", kids.len() as i64);
100 node.insert("Kids", kids);
101 self.store_mut().set(tree, Object::Dict(node));
102 if catalog.get_ref(b"Pages") != Some(tree) {
103 let mut catalog = catalog;
104 catalog.insert("Pages", tree);
105 self.set_catalog(catalog)?;
106 }
107 self.invalidate();
108 Ok(())
109 }
110
111 pub fn select_pages(&mut self, indices: &[usize]) -> Result<()> {
115 let pages = self.pages()?;
116 let mut chosen = Vec::with_capacity(indices.len());
117 for &i in indices {
118 let page = pages
119 .get(i)
120 .ok_or(Error::syntax(0, "page index out of range"))?;
121 chosen.push((page.id, standalone_page_dict(page)));
122 }
123 let kept: HashSet<ObjId> = chosen.iter().filter_map(|(id, _)| *id).collect();
124 let removed: HashSet<ObjId> = pages
125 .iter()
126 .filter_map(|p| p.id)
127 .filter(|id| !kept.contains(id))
128 .collect();
129 self.write_page_tree(chosen)?;
130 if !removed.is_empty() {
131 for id in &removed {
132 self.store_mut().delete(*id);
133 }
134 self.remove_destinations_to(&removed)?;
135 }
136 Ok(())
137 }
138
139 pub fn reorder_pages(&mut self, order: &[usize]) -> Result<()> {
141 let count = self.page_count()?;
142 let mut sorted = order.to_vec();
143 sorted.sort_unstable();
144 if sorted != (0..count).collect::<Vec<_>>() {
145 return Err(Error::syntax(
146 0,
147 "page order must be a permutation of every page",
148 ));
149 }
150 self.select_pages(order)
151 }
152
153 pub fn delete_pages(&mut self, indices: &[usize]) -> Result<()> {
156 let count = self.page_count()?;
157 let drop: HashSet<usize> = indices.iter().copied().collect();
158 if drop.iter().any(|&i| i >= count) {
159 return Err(Error::syntax(0, "page index out of range"));
160 }
161 let keep: Vec<usize> = (0..count).filter(|i| !drop.contains(i)).collect();
162 self.select_pages(&keep)
163 }
164
165 fn edit_page(&mut self, index: usize, f: impl FnOnce(&mut Dict)) -> Result<()> {
167 let pages = self.pages()?;
168 let page = pages
169 .get(index)
170 .ok_or(Error::syntax(0, "page index out of range"))?;
171 let mut dict = page.dict.clone();
172 f(&mut dict);
173 match page.id {
174 Some(id) => self.store_mut().set(id, Object::Dict(dict)),
175 None => {
177 let mut all: Vec<_> = pages
178 .iter()
179 .map(|p| (p.id, standalone_page_dict(p)))
180 .collect();
181 if let Some(slot) = all.get_mut(index) {
182 slot.1 = dict;
183 }
184 self.write_page_tree(all)?;
185 }
186 }
187 self.invalidate();
188 Ok(())
189 }
190
191 pub fn rotate_page(&mut self, index: usize, degrees: i32) -> Result<()> {
194 if degrees % 90 != 0 {
195 return Err(Error::syntax(0, "rotation must be a multiple of 90"));
196 }
197 let current = i32::from(
198 self.pages()?
199 .get(index)
200 .ok_or(Error::syntax(0, "page index out of range"))?
201 .rotate,
202 );
203 let rotate = (current + degrees).rem_euclid(360);
204 self.edit_page(index, |d| {
205 d.insert("Rotate", i64::from(rotate));
206 })
207 }
208
209 pub fn set_crop_box(&mut self, index: usize, rect: Rect) -> Result<()> {
211 self.edit_page(index, |d| {
212 d.insert("CropBox", rect.to_object());
213 })
214 }
215
216 pub fn set_media_box(&mut self, index: usize, rect: Rect) -> Result<()> {
218 self.edit_page(index, |d| {
219 d.insert("MediaBox", rect.to_object());
220 })
221 }
222
223 pub fn append_pages_from(&mut self, other: &Document, indices: Option<&[usize]>) -> Result<()> {
227 let theirs = other.pages()?;
228 let wanted: Vec<usize> = match indices {
229 Some(i) => i.to_vec(),
230 None => (0..theirs.len()).collect(),
231 };
232 let mut map: HashMap<ObjId, ObjId> = HashMap::new();
233 let mut new_pages = Vec::with_capacity(wanted.len());
235 for &i in &wanted {
236 let page = theirs
237 .get(i)
238 .ok_or(Error::syntax(0, "page index out of range"))?;
239 let id = self.store_mut().allocate()?;
240 if let Some(src) = page.id {
241 map.entry(src).or_insert(id);
242 }
243 new_pages.push((id, page));
244 }
245 let mut pages: Vec<(Option<ObjId>, Dict)> = self
246 .pages()?
247 .iter()
248 .map(|p| (p.id, standalone_page_dict(p)))
249 .collect();
250 for (id, page) in new_pages {
251 let mut dict = standalone_page_dict(page);
252 for key in [&b"StructParents"[..], b"B"] {
254 dict.remove(key);
255 }
256 let copied = import(
257 &mut self.store,
258 other.store(),
259 &Object::Dict(dict),
260 &mut map,
261 )?;
262 let Object::Dict(dict) = copied else {
263 return Err(Error::syntax(0, "page did not copy as a dictionary"));
264 };
265 pages.push((Some(id), dict));
266 }
267 self.write_page_tree(pages)
268 }
269
270 pub fn extract_pages(&self, indices: &[usize]) -> Result<Document> {
272 let mut out = Document::new()?;
273 out.append_pages_from(self, Some(indices))?;
274 Ok(out)
275 }
276
277 fn remove_destinations_to(&mut self, pages: &HashSet<ObjId>) -> Result<()> {
281 let named = self.named_destinations()?;
282 let dead_names: HashSet<Vec<u8>> = named
283 .iter()
284 .filter(|(_, dest)| targets(dest, pages, &HashMap::new(), self.store()))
285 .map(|(k, _)| k.clone())
286 .collect();
287 let named_map: HashMap<Vec<u8>, Object> = named.into_iter().collect();
288
289 let outlines = self.outlines()?;
290 let (kept, changed) = prune_outlines(outlines, &|item: &OutlineItem| {
291 let dest = item.dest.clone().or_else(|| {
292 item.action
293 .as_ref()
294 .filter(|a| a.get_name(b"S") == Some(b"GoTo"))
295 .and_then(|a| a.get(b"D").cloned())
296 });
297 dest.is_some_and(|d| targets(&d, pages, &named_map, self.store()))
298 });
299 if changed {
300 self.set_outlines(&kept)?;
301 }
302 if !dead_names.is_empty() {
303 self.remove_named_destinations(&dead_names)?;
304 }
305 Ok(())
306 }
307
308 pub fn named_destinations(&self) -> Result<Vec<(Vec<u8>, Object)>> {
311 let catalog = self.catalog();
312 let store = self.store();
313 let mut out = Vec::new();
314 if let Some(dests) = catalog.get(b"Dests").and_then(|d| store.resolve_dict(d)) {
315 for (k, v) in dests.iter() {
316 out.push((k.as_bytes().to_vec(), store.resolve(v)));
317 }
318 }
319 if let Some(tree) = catalog
320 .get(b"Names")
321 .and_then(|n| store.resolve_dict(n))
322 .and_then(|n| n.get(b"Dests").cloned())
323 {
324 for (k, v) in name_tree(store, &tree)? {
325 out.push((k, store.resolve(&v)));
326 }
327 }
328 Ok(out)
329 }
330
331 fn remove_named_destinations(&mut self, dead: &HashSet<Vec<u8>>) -> Result<()> {
332 let mut catalog = self.catalog();
333 if let Some(dests_obj) = catalog.get(b"Dests").cloned()
334 && let Some(mut dests) = self.store().resolve_dict(&dests_obj)
335 {
336 for name in dead {
337 dests.remove(name);
338 }
339 match dests_obj {
340 Object::Ref(id) => self.store_mut().set(id, Object::Dict(dests)),
341 _ => {
342 catalog.insert("Dests", dests);
343 }
344 }
345 }
346 let names_tree = catalog
347 .get(b"Names")
348 .and_then(|n| self.store().resolve_dict(n))
349 .and_then(|n| n.get(b"Dests").cloned());
350 if let Some(tree) = names_tree {
351 let entries: Vec<_> = name_tree(self.store(), &tree)?
352 .into_iter()
353 .filter(|(k, _)| !dead.contains(k))
354 .collect();
355 catalog = self.with_name_tree(catalog, b"Dests", entries)?;
356 }
357 self.set_catalog(catalog)
358 }
359
360 fn with_name_tree(
363 &mut self,
364 mut catalog: Dict,
365 tree: &[u8],
366 mut entries: Vec<(Vec<u8>, Object)>,
367 ) -> Result<Dict> {
368 entries.sort_by(|a, b| a.0.cmp(&b.0));
369 entries.dedup_by(|a, b| a.0 == b.0);
370 let names_obj = catalog.get(b"Names").cloned();
371 let mut names = names_obj
372 .as_ref()
373 .and_then(|n| self.store().resolve_dict(n))
374 .unwrap_or_default();
375 if entries.is_empty() {
376 names.remove(tree);
377 } else {
378 let mut flat = Vec::with_capacity(entries.len() * 2);
379 for (k, v) in entries {
380 flat.push(Object::String(PdfString::literal(k)));
381 flat.push(v);
382 }
383 let mut node = Dict::new();
384 node.insert("Names", flat);
385 let id = self.store_mut().add(Object::Dict(node))?;
386 names.insert(Name::new(tree.to_vec()), Object::Ref(id));
387 }
388 match names_obj {
389 Some(Object::Ref(id)) => self.store_mut().set(id, Object::Dict(names)),
390 _ if names.is_empty() => {
391 catalog.remove(b"Names");
392 }
393 _ => {
394 catalog.insert("Names", names);
395 }
396 }
397 Ok(catalog)
398 }
399
400 pub fn set_outlines(&mut self, items: &[OutlineItem]) -> Result<()> {
405 let mut catalog = self.catalog();
406 if items.is_empty() {
407 catalog.remove(b"Outlines");
408 return self.set_catalog(catalog);
409 }
410 let root = self.store_mut().allocate()?;
411 let (first, last, visible) = self.write_outline_level(root, items)?;
412 let mut dict = Dict::new();
413 dict.insert("Type", Name::from("Outlines"));
414 dict.insert("First", first);
415 dict.insert("Last", last);
416 dict.insert("Count", visible as i64);
417 self.store_mut().set(root, Object::Dict(dict));
418 catalog.insert("Outlines", root);
419 self.set_catalog(catalog)
420 }
421
422 fn write_outline_level(
425 &mut self,
426 parent: ObjId,
427 items: &[OutlineItem],
428 ) -> Result<(ObjId, ObjId, usize)> {
429 let ids: Vec<ObjId> = items
430 .iter()
431 .map(|_| self.store_mut().allocate())
432 .collect::<Result<_>>()?;
433 let mut visible = 0;
434 for (i, (item, &id)) in items.iter().zip(&ids).enumerate() {
435 let mut dict = item.extra.clone();
436 dict.insert("Title", text(&item.title));
437 dict.insert("Parent", parent);
438 if i > 0
439 && let Some(prev) = ids.get(i - 1)
440 {
441 dict.insert("Prev", *prev);
442 }
443 if let Some(next) = ids.get(i + 1) {
444 dict.insert("Next", *next);
445 }
446 if let Some(dest) = &item.dest {
447 dict.insert("Dest", dest.clone());
448 }
449 if let Some(action) = &item.action {
450 dict.insert("A", action.clone());
451 }
452 visible += 1;
453 if !item.children.is_empty() {
454 let (first, last, below) = self.write_outline_level(id, &item.children)?;
455 dict.insert("First", first);
456 dict.insert("Last", last);
457 if item.open {
460 dict.insert("Count", below as i64);
461 visible += below;
462 } else {
463 dict.insert("Count", -(below as i64));
464 }
465 }
466 self.store_mut().set(id, Object::Dict(dict));
467 }
468 let first = *ids.first().ok_or(Error::syntax(0, "empty outline level"))?;
469 let last = *ids.last().ok_or(Error::syntax(0, "empty outline level"))?;
470 Ok((first, last, visible))
471 }
472
473 pub fn set_info(&mut self, info: &Info) -> Result<()> {
479 let info_obj = self.store().trailer().get(b"Info").cloned();
480 let mut dict = info_obj
481 .as_ref()
482 .and_then(|i| self.store().resolve_dict(i))
483 .unwrap_or_default();
484 for (key, value) in [
485 ("Title", &info.title),
486 ("Author", &info.author),
487 ("Subject", &info.subject),
488 ("Keywords", &info.keywords),
489 ("Creator", &info.creator),
490 ("Producer", &info.producer),
491 ("CreationDate", &info.creation_date),
492 ("ModDate", &info.mod_date),
493 ] {
494 match value {
495 Some(v) => {
497 dict.insert(key, text(v));
498 }
499 None => {
500 dict.remove(key.as_bytes());
501 }
502 }
503 }
504 match &info.trapped {
505 Some(t) => {
506 dict.insert("Trapped", Name::from(t.as_str()));
507 }
508 None => {
509 dict.remove(b"Trapped");
510 }
511 }
512 match info_obj {
513 Some(Object::Ref(id)) => self.store_mut().set(id, Object::Dict(dict)),
514 _ => {
515 let id = self.store_mut().add(Object::Dict(dict))?;
516 self.store_mut().set_trailer_entry("Info", Object::Ref(id));
517 }
518 }
519 let previous = self.xmp_metadata()?;
520 let packet = xmp::packet(info, previous.as_deref());
521 let mut meta = Dict::new();
522 meta.insert("Type", Name::from("Metadata"));
523 meta.insert("Subtype", Name::from("XML"));
524 let stream = Object::Stream(Stream {
525 dict: meta,
526 raw: packet.into_bytes().into(),
527 });
528 let mut catalog = self.catalog();
529 match catalog.get(b"Metadata") {
530 Some(Object::Ref(id)) => {
531 let id = *id;
532 self.store_mut().set(id, stream);
533 }
534 _ => {
535 let id = self.store_mut().add(stream)?;
536 catalog.insert("Metadata", id);
537 self.set_catalog(catalog)?;
538 }
539 }
540 Ok(())
541 }
542
543 pub fn add_attachment(
548 &mut self,
549 name: &str,
550 data: &[u8],
551 options: &AttachmentOptions,
552 ) -> Result<()> {
553 let mut params = Dict::new();
554 params.insert("Size", data.len() as i64);
555 if let Some(d) = &options.creation_date {
556 params.insert("CreationDate", text(d));
557 }
558 if let Some(d) = &options.mod_date {
559 params.insert("ModDate", text(d));
560 }
561 let mut ef_dict = Dict::new();
562 ef_dict.insert("Type", Name::from("EmbeddedFile"));
563 if let Some(mime) = &options.mime_type {
564 ef_dict.insert("Subtype", Name::from(mime.as_str()));
565 }
566 ef_dict.insert("Params", params);
567 let stream = self.store_mut().add(Object::Stream(Stream {
568 dict: ef_dict,
569 raw: Arc::from(data),
570 }))?;
571 let mut ef = Dict::new();
572 ef.insert("F", stream);
573 ef.insert("UF", stream);
574 let mut spec = Dict::new();
575 spec.insert("Type", Name::from("Filespec"));
576 let ascii: String = name
578 .chars()
579 .map(|c| if c.is_ascii() { c } else { '_' })
580 .collect();
581 spec.insert("F", PdfString::literal(ascii.into_bytes()));
582 spec.insert("UF", text(name));
583 spec.insert("EF", ef);
584 if let Some(desc) = &options.description {
585 spec.insert("Desc", text(desc));
586 }
587 let spec = self.store_mut().add(Object::Dict(spec))?;
588 let key = encode_text_string(name);
589 let mut entries = self.embedded_files()?;
590 entries.retain(|(k, _)| *k != key);
591 entries.push((key, Object::Ref(spec)));
592 let catalog = self.catalog();
593 let catalog = self.with_name_tree(catalog, b"EmbeddedFiles", entries)?;
594 self.set_catalog(catalog)
595 }
596
597 pub fn remove_attachment(&mut self, name: &str) -> Result<bool> {
599 let mut entries = self.embedded_files()?;
600 let before = entries.len();
601 entries.retain(|(k, _)| decode_text_string(k) != name);
602 if entries.len() == before {
603 return Ok(false);
604 }
605 let catalog = self.catalog();
606 let catalog = self.with_name_tree(catalog, b"EmbeddedFiles", entries)?;
607 self.set_catalog(catalog)?;
608 Ok(true)
609 }
610
611 fn embedded_files(&self) -> Result<Vec<(Vec<u8>, Object)>> {
612 let tree = self
613 .catalog()
614 .get(b"Names")
615 .and_then(|n| self.store().resolve_dict(n))
616 .and_then(|n| n.get(b"EmbeddedFiles").cloned());
617 match tree {
618 Some(t) => name_tree(self.store(), &t),
619 None => Ok(Vec::new()),
620 }
621 }
622}
623
624fn targets(
627 dest: &Object,
628 pages: &HashSet<ObjId>,
629 named: &HashMap<Vec<u8>, Object>,
630 store: &Store,
631) -> bool {
632 let explicit = match dest {
633 Object::Name(n) => named.get(n.as_bytes()).map(|d| store.resolve(d)),
634 Object::String(s) => named.get(&s.bytes).map(|d| store.resolve(d)),
635 other => Some(store.resolve(other)),
636 };
637 let array = match explicit {
638 Some(Object::Dict(d)) => d.get(b"D").map(|x| store.resolve(x)),
640 other => other,
641 };
642 array
643 .as_ref()
644 .and_then(Object::as_array)
645 .and_then(|a| a.first())
646 .and_then(Object::as_ref)
647 .is_some_and(|page| pages.contains(&page))
648}
649
650fn prune_outlines(
653 items: Vec<OutlineItem>,
654 dead: &dyn Fn(&OutlineItem) -> bool,
655) -> (Vec<OutlineItem>, bool) {
656 let mut out = Vec::with_capacity(items.len());
657 let mut changed = false;
658 for mut item in items {
659 let (children, child_changed) = prune_outlines(std::mem::take(&mut item.children), dead);
660 changed |= child_changed;
661 if dead(&item) {
662 changed = true;
663 out.extend(children);
664 } else {
665 item.children = children;
666 out.push(item);
667 }
668 }
669 (out, changed)
670}
671
672pub fn import(
676 dst: &mut Store,
677 src: &Store,
678 object: &Object,
679 map: &mut HashMap<ObjId, ObjId>,
680) -> Result<Object> {
681 let mut queue: VecDeque<ObjId> = VecDeque::new();
682 let top = remap_into(dst, object, map, &mut queue)?;
683 while let Some(src_id) = queue.pop_front() {
684 dst.limits().charge(1)?;
685 let Some(&dst_id) = map.get(&src_id) else {
686 continue;
687 };
688 let obj = match src.get(src_id) {
689 Ok(o) => (*o).clone(),
690 Err(_) => Object::Null,
691 };
692 let copied = remap_into(dst, &obj, map, &mut queue)?;
693 dst.set(dst_id, copied);
694 }
695 Ok(top)
696}
697
698fn remap_into(
701 dst: &mut Store,
702 object: &Object,
703 map: &mut HashMap<ObjId, ObjId>,
704 queue: &mut VecDeque<ObjId>,
705) -> Result<Object> {
706 Ok(match object {
707 Object::Ref(id) => {
708 let new = match map.get(id) {
709 Some(n) => *n,
710 None => {
711 let n = dst.allocate()?;
712 map.insert(*id, n);
713 queue.push_back(*id);
714 n
715 }
716 };
717 Object::Ref(new)
718 }
719 Object::Array(items) => Object::Array(
720 items
721 .iter()
722 .map(|i| remap_into(dst, i, map, queue))
723 .collect::<Result<_>>()?,
724 ),
725 Object::Dict(d) => Object::Dict(remap_dict_into(dst, d, map, queue)?),
726 Object::Stream(s) => Object::Stream(Stream {
727 dict: remap_dict_into(dst, &s.dict, map, queue)?,
728 raw: Arc::clone(&s.raw),
729 }),
730 other => other.clone(),
731 })
732}
733
734fn remap_dict_into(
735 dst: &mut Store,
736 dict: &Dict,
737 map: &mut HashMap<ObjId, ObjId>,
738 queue: &mut VecDeque<ObjId>,
739) -> Result<Dict> {
740 dict.iter()
741 .map(|(k, v)| Ok((k.clone(), remap_into(dst, v, map, queue)?)))
742 .collect()
743}
744
745#[cfg(test)]
746mod tests {
747 use super::*;
748
749 fn item(title: &str, children: Vec<OutlineItem>) -> OutlineItem {
750 OutlineItem {
751 id: None,
752 title: title.into(),
753 dest: None,
754 action: None,
755 open: true,
756 children,
757 extra: Dict::new(),
758 }
759 }
760
761 #[test]
762 fn empty_document() {
763 let doc = Document::new().unwrap();
764 assert_eq!(doc.page_count().unwrap(), 0);
765 assert!(doc.diagnostics().is_empty(), "{:?}", doc.diagnostics());
766 }
767
768 #[test]
769 fn pruning_promotes_children() {
770 let tree = vec![
771 item("a", vec![item("a1", vec![]), item("x", vec![])]),
772 item("x", vec![item("x1", vec![])]),
773 ];
774 let (kept, changed) = prune_outlines(tree, &|i| i.title == "x");
775 assert!(changed);
776 let titles: Vec<_> = kept.iter().map(|i| i.title.as_str()).collect();
777 assert_eq!(titles, ["a", "x1"]);
778 assert_eq!(kept[0].children.len(), 1);
779 let (_, unchanged) = prune_outlines(vec![item("a", vec![])], &|_| false);
780 assert!(!unchanged);
781 }
782}