1#![cfg(feature = "write")]
2use lopdf::{dictionary, Document, Object, ObjectId, Stream};
14use std::collections::HashSet;
15
16use crate::builder::{AnnotRect, AnnotSubtype, AnnotationBuilder};
17
18fn unique_xobject_name(xobjects: &lopdf::Dictionary, next: &mut usize) -> String {
24 loop {
25 let candidate = format!("FlatAnnot{}", *next);
26 *next += 1;
27 if !xobjects.has(candidate.as_bytes()) {
28 return candidate;
29 }
30 }
31}
32
33fn as_number(obj: &Object) -> Option<f64> {
35 match obj {
36 Object::Integer(i) => Some(*i as f64),
37 Object::Real(f) => Some(*f as f64),
38 _ => None,
39 }
40}
41
42fn resolve_inherited_resources_clone(
44 doc: &lopdf::Document,
45 page_id: ObjectId,
46) -> Option<lopdf::Dictionary> {
47 let mut current = page_id;
48 let mut visited = HashSet::new();
49 loop {
50 if !visited.insert(current) {
51 return None;
52 }
53 let dict = doc.get_dictionary(current).ok()?;
54 if let Ok(res_obj) = dict.get(b"Resources") {
55 return match res_obj {
56 Object::Dictionary(d) => Some(d.clone()),
57 Object::Reference(id) => doc.get_dictionary(*id).ok().cloned(),
58 _ => None,
59 };
60 }
61 match dict.get(b"Parent") {
62 Ok(Object::Reference(id)) => current = *id,
63 _ => return None,
64 }
65 }
66}
67
68fn resolve_content_streams(doc: &lopdf::Document, page_id: ObjectId) -> Vec<ObjectId> {
70 let page_obj = match doc.get_object(page_id) {
71 Ok(obj) => obj,
72 Err(_) => return Vec::new(),
73 };
74 let page_dict = match page_obj {
75 Object::Dictionary(ref d) => d,
76 _ => return Vec::new(),
77 };
78 match page_dict.get(b"Contents").ok() {
79 Some(c) => flatten_content_refs(doc, c),
80 None => Vec::new(),
81 }
82}
83
84fn flatten_content_refs(doc: &lopdf::Document, obj: &Object) -> Vec<ObjectId> {
85 match obj {
86 Object::Reference(id) => {
87 if let Ok(Object::Array(arr)) = doc.get_object(*id) {
88 return arr
89 .iter()
90 .flat_map(|o| flatten_content_refs(doc, o))
91 .collect();
92 }
93 vec![*id]
94 }
95 Object::Array(arr) => arr
96 .iter()
97 .flat_map(|o| flatten_content_refs(doc, o))
98 .collect(),
99 _ => Vec::new(),
100 }
101}
102
103fn wrap_existing_content_in_save_restore(doc: &mut lopdf::Document, page_id: ObjectId) {
105 let existing = resolve_content_streams(doc, page_id);
106 if existing.is_empty() {
107 return;
108 }
109
110 let q_stream = Stream::new(dictionary! {}, b"q\n".to_vec());
111 let q_id = doc.add_object(Object::Stream(q_stream));
112
113 let big_q_stream = Stream::new(dictionary! {}, b"\nQ\n".to_vec());
114 let big_q_id = doc.add_object(Object::Stream(big_q_stream));
115
116 let mut new_arr = Vec::with_capacity(existing.len() + 2);
118 new_arr.push(Object::Reference(q_id));
119 for id in existing {
120 new_arr.push(Object::Reference(id));
121 }
122 new_arr.push(Object::Reference(big_q_id));
123 let wrapped = Object::Array(new_arr);
124
125 if let Ok(Object::Dictionary(ref mut d)) = doc.get_object_mut(page_id) {
126 d.set("Contents", wrapped);
127 }
128}
129
130fn append_content_to_page(doc: &mut lopdf::Document, page_id: ObjectId, content_id: ObjectId) {
132 let existing = resolve_content_streams(doc, page_id);
133 let new_contents = if existing.is_empty() {
134 Object::Reference(content_id)
135 } else {
136 let mut arr: Vec<Object> = existing.into_iter().map(Object::Reference).collect();
137 arr.push(Object::Reference(content_id));
138 Object::Array(arr)
139 };
140
141 if let Ok(Object::Dictionary(ref mut d)) = doc.get_object_mut(page_id) {
142 d.set("Contents", new_contents);
143 }
144}
145
146fn get_matrix_and_bbox(n_stream_dict: &lopdf::Dictionary, rect: &[f64; 4]) -> Option<[f64; 6]> {
148 let bbox = n_stream_dict.get(b"BBox").ok()?;
149 let bbox_arr = match bbox {
150 Object::Array(ref arr) if arr.len() >= 4 => arr,
151 _ => return None,
152 };
153 let bx0 = as_number(&bbox_arr[0])?;
154 let by0 = as_number(&bbox_arr[1])?;
155 let bx1 = as_number(&bbox_arr[2])?;
156 let by1 = as_number(&bbox_arr[3])?;
157
158 let bw = bx1 - bx0;
159 let bh = by1 - by0;
160 if bw == 0.0 || bh == 0.0 {
161 return None;
162 }
163
164 let rx0 = rect[0];
165 let ry0 = rect[1];
166 let rx1 = rect[2];
167 let ry1 = rect[3];
168 let rw = rx1 - rx0;
169 let rh = ry1 - ry0;
170
171 let matrix_arr = n_stream_dict.get(b"Matrix").ok().and_then(|m| match m {
173 Object::Array(ref arr) if arr.len() >= 6 => Some(arr),
174 _ => None,
175 });
176
177 if let Some(m) = matrix_arr {
178 let ma = as_number(&m[0]).unwrap_or(1.0);
179 let mb = as_number(&m[1]).unwrap_or(0.0);
180 let mc = as_number(&m[2]).unwrap_or(0.0);
181 let md = as_number(&m[3]).unwrap_or(1.0);
182 let me = as_number(&m[4]).unwrap_or(0.0);
183 let mf = as_number(&m[5]).unwrap_or(0.0);
184
185 let corners = [(bx0, by0), (bx1, by0), (bx0, by1), (bx1, by1)];
186 let mut t_corners = [(0.0, 0.0); 4];
187 for (i, &(x, y)) in corners.iter().enumerate() {
188 t_corners[i] = (ma * x + mc * y + me, mb * x + md * y + mf);
189 }
190 let t_bx0 = t_corners.iter().map(|p| p.0).fold(f64::INFINITY, f64::min);
191 let t_by0 = t_corners.iter().map(|p| p.1).fold(f64::INFINITY, f64::min);
192 let t_bx1 = t_corners
193 .iter()
194 .map(|p| p.0)
195 .fold(f64::NEG_INFINITY, f64::max);
196 let t_by1 = t_corners
197 .iter()
198 .map(|p| p.1)
199 .fold(f64::NEG_INFINITY, f64::max);
200
201 let t_bw = t_bx1 - t_bx0;
202 let t_bh = t_by1 - t_by0;
203 if t_bw == 0.0 || t_bh == 0.0 {
204 return None;
205 }
206
207 let sx = rw / t_bw;
208 let sy = rh / t_bh;
209 let tx = rx0 - t_bx0 * sx;
210 let ty = ry0 - t_by0 * sy;
211
212 Some([
213 ma * sx,
214 mb * sy,
215 mc * sx,
216 md * sy,
217 me * sx + tx,
218 mf * sy + ty,
219 ])
220 } else {
221 let sx = rw / bw;
222 let sy = rh / bh;
223 let tx = rx0 - bx0 * sx;
224 let ty = ry0 - by0 * sy;
225 Some([sx, 0.0, 0.0, sy, tx, ty])
226 }
227}
228
229fn has_usable_appearance(doc: &Document, annot_dict: &lopdf::Dictionary) -> bool {
232 let ap = match annot_dict.get(b"AP") {
233 Ok(Object::Dictionary(d)) => d.clone(),
234 Ok(Object::Reference(id)) => match doc.get_object(*id) {
235 Ok(Object::Dictionary(d)) => d.clone(),
236 _ => return false,
237 },
238 _ => return false,
239 };
240 match ap.get(b"N") {
241 Ok(Object::Stream(_)) => true,
242 Ok(Object::Reference(id)) => {
243 matches!(doc.get_object(*id), Ok(Object::Stream(_)))
244 }
245 _ => false,
246 }
247}
248
249fn annot_rgb(value: &Object) -> Option<(f64, f64, f64)> {
251 let arr = value.as_array().ok()?;
252 let nums: Vec<f64> = arr.iter().filter_map(as_number).collect();
253 match nums.as_slice() {
254 [g] => Some((*g, *g, *g)),
255 [r, g, b] => Some((*r, *g, *b)),
256 [c, m, y, k] => Some((
257 (1.0 - (c + k).min(1.0)).max(0.0),
258 (1.0 - (m + k).min(1.0)).max(0.0),
259 (1.0 - (y + k).min(1.0)).max(0.0),
260 )),
261 _ => None,
262 }
263}
264
265fn annot_border_width(doc: &Document, annot_dict: &lopdf::Dictionary) -> Option<f64> {
267 if let Ok(bs) = annot_dict.get(b"BS") {
268 let dict = match bs {
269 Object::Dictionary(d) => Some(d.clone()),
270 Object::Reference(id) => doc.get_dictionary(*id).ok().cloned(),
271 _ => None,
272 };
273 if let Some(d) = dict {
274 if let Ok(w) = d.get(b"W") {
275 if let Some(w) = as_number(w) {
276 return Some(w);
277 }
278 }
279 }
280 }
281 if let Ok(Object::Array(border)) = annot_dict.get(b"Border") {
282 if let Some(w) = border.get(2).and_then(as_number) {
283 return Some(w);
284 }
285 }
286 None
287}
288
289fn annot_dash(doc: &Document, annot_dict: &lopdf::Dictionary) -> Option<Vec<f64>> {
291 let bs = annot_dict.get(b"BS").ok()?;
292 let dict = match bs {
293 Object::Dictionary(d) => d.clone(),
294 Object::Reference(id) => doc.get_dictionary(*id).ok().cloned()?,
295 _ => return None,
296 };
297 let dash = dict.get(b"D").ok()?.as_array().ok()?.clone();
298 let nums: Vec<f64> = dash.iter().filter_map(as_number).collect();
299 if nums.is_empty() {
300 None
301 } else {
302 Some(nums)
303 }
304}
305
306fn annot_text(value: &Object) -> Option<String> {
308 match value {
309 Object::String(bytes, _) => Some(String::from_utf8_lossy(bytes).into_owned()),
310 _ => None,
311 }
312}
313
314pub fn ensure_appearances(doc: &mut Document) -> Result<usize, crate::error::AnnotBuildError> {
322 let page_ids: Vec<ObjectId> = doc.get_pages().values().copied().collect();
323 let mut added = 0usize;
324 for page_id in page_ids {
325 let page_dict = match doc.get_object(page_id) {
326 Ok(Object::Dictionary(d)) => d.clone(),
327 _ => continue,
328 };
329 let annots_arr = match page_dict.get(b"Annots") {
330 Ok(Object::Array(arr)) => arr.clone(),
331 Ok(Object::Reference(id)) => match doc.get_object(*id) {
332 Ok(Object::Array(arr)) => arr.clone(),
333 _ => continue,
334 },
335 _ => continue,
336 };
337 for annot_obj in &annots_arr {
338 let annot_id = match annot_obj {
339 Object::Reference(id) => *id,
340 _ => continue,
341 };
342 let annot_dict = match doc.get_object(annot_id) {
343 Ok(Object::Dictionary(d)) => d.clone(),
344 _ => continue,
345 };
346 let subtype = match annot_dict.get(b"Subtype") {
347 Ok(Object::Name(name)) => String::from_utf8_lossy(name).into_owned(),
348 _ => continue,
349 };
350 if subtype == "Widget" || subtype == "Link" {
351 continue;
352 }
353 let parsed = match AnnotSubtype::from_name(&subtype) {
354 Some(found) => found,
355 None => continue,
356 };
357 if has_usable_appearance(doc, &annot_dict) {
358 continue;
359 }
360 let rect = match annot_dict.get(b"Rect") {
361 Ok(Object::Array(arr)) if arr.len() >= 4 => [
362 as_number(&arr[0]),
363 as_number(&arr[1]),
364 as_number(&arr[2]),
365 as_number(&arr[3]),
366 ],
367 _ => continue,
368 };
369 let [Some(x0), Some(y0), Some(x1), Some(y1)] = rect else {
370 continue;
371 };
372 if (x1 - x0).abs() < f64::EPSILON || (y1 - y0).abs() < f64::EPSILON {
373 continue;
374 }
375 let rect = AnnotRect::new(x0, y0, x1, y1);
376 let mut builder = match parsed {
377 AnnotSubtype::Ink => {
378 let strokes = match annot_dict.get(b"InkList") {
379 Ok(Object::Array(paths)) => paths
380 .iter()
381 .filter_map(|p| p.as_array().ok())
382 .map(|a| a.iter().filter_map(as_number).collect::<Vec<_>>())
383 .filter(|p| p.len() >= 2)
384 .collect(),
385 _ => Vec::new(),
386 };
387 AnnotationBuilder::ink(rect, strokes)
388 }
389 AnnotSubtype::Polygon => {
390 let verts = match annot_dict.get(b"Vertices") {
391 Ok(Object::Array(arr)) => {
392 arr.iter().filter_map(as_number).collect::<Vec<_>>()
393 }
394 _ => Vec::new(),
395 };
396 AnnotationBuilder::polygon(rect, verts)
397 }
398 AnnotSubtype::PolyLine => {
399 let verts = match annot_dict.get(b"Vertices") {
400 Ok(Object::Array(arr)) => {
401 arr.iter().filter_map(as_number).collect::<Vec<_>>()
402 }
403 _ => Vec::new(),
404 };
405 AnnotationBuilder::polyline(rect, verts)
406 }
407 other => AnnotationBuilder::new(other, rect),
408 };
409 if parsed == AnnotSubtype::Line {
410 if let Ok(Object::Array(line)) = annot_dict.get(b"L") {
411 if line.len() >= 4 {
412 let ends = [
413 as_number(&line[0]),
414 as_number(&line[1]),
415 as_number(&line[2]),
416 as_number(&line[3]),
417 ];
418 if let [Some(x1), Some(y1), Some(x2), Some(y2)] = ends {
419 builder.set_line_endpoints([x1, y1, x2, y2]);
420 }
421 }
422 }
423 }
424 if let Ok(color) = annot_dict.get(b"C") {
425 if let Some((r, g, b)) = annot_rgb(color) {
426 builder = builder.color(r, g, b);
427 }
428 }
429 if let Ok(ic) = annot_dict.get(b"IC") {
430 if let Some((r, g, b)) = annot_rgb(ic) {
431 builder = builder.interior_color(r, g, b);
432 }
433 }
434 if let Ok(ca) = annot_dict.get(b"CA") {
435 if let Some(alpha) = as_number(ca) {
436 builder = builder.opacity(alpha);
437 }
438 }
439 if let Some(w) = annot_border_width(doc, &annot_dict) {
440 builder = builder.border_width(w);
441 }
442 if let Some(dash) = annot_dash(doc, &annot_dict) {
443 builder = builder.dash(dash);
444 }
445 if let Ok(contents) = annot_dict.get(b"Contents") {
446 if let Some(text) = annot_text(contents) {
447 builder = builder.contents(text);
448 }
449 }
450 if let Ok(Object::Integer(q)) = annot_dict.get(b"Q") {
451 builder = builder.alignment(*q);
452 }
453 if matches!(parsed, AnnotSubtype::Stamp | AnnotSubtype::Text) {
454 if let Ok(Object::Name(name)) = annot_dict.get(b"Name") {
455 builder.set_icon_name(String::from_utf8_lossy(name).into_owned());
456 }
457 }
458 if let Ok(Object::Array(qp)) = annot_dict.get(b"QuadPoints") {
459 let nums: Vec<f64> = qp.iter().filter_map(as_number).collect();
460 if !nums.is_empty() {
461 builder = builder.quad_points(nums);
462 }
463 }
464 let ap_id = match builder.appearance_stream(doc) {
465 Ok(id) => id,
466 Err(crate::error::AnnotBuildError::InvalidRect) => continue,
467 Err(other) => return Err(other),
468 };
469 let ap_ref = Object::Reference(ap_id);
471 match annot_dict.get(b"AP").cloned() {
472 Ok(Object::Dictionary(mut ap)) => {
473 ap.set("N", ap_ref);
474 if let Ok(Object::Dictionary(d)) = doc.get_object_mut(annot_id) {
475 d.set("AP", Object::Dictionary(ap));
476 added += 1;
477 }
478 }
479 Ok(Object::Reference(id)) => {
480 if let Ok(Object::Dictionary(ap)) = doc.get_object_mut(id) {
481 ap.set("N", ap_ref);
482 added += 1;
483 } else if let Ok(Object::Dictionary(d)) = doc.get_object_mut(annot_id) {
484 let ap = dictionary! { "N" => ap_ref };
485 d.set("AP", Object::Dictionary(ap));
486 added += 1;
487 }
488 }
489 _ => {
490 if let Ok(Object::Dictionary(d)) = doc.get_object_mut(annot_id) {
491 let ap = dictionary! { "N" => ap_ref };
492 d.set("AP", Object::Dictionary(ap));
493 added += 1;
494 }
495 }
496 }
497 }
498 }
499 Ok(added)
500}
501
502pub fn flatten_annotations(doc: &mut lopdf::Document) -> Result<(), crate::error::AnnotBuildError> {
512 let page_ids: Vec<ObjectId> = doc.get_pages().values().copied().collect();
513 let mut deleted_objects = HashSet::new();
514 let mut popups_to_remove: HashSet<ObjectId> = HashSet::new();
517
518 for page_id in page_ids {
519 let page_dict = match doc.get_object(page_id) {
521 Ok(Object::Dictionary(ref d)) => d.clone(),
522 _ => continue,
523 };
524
525 let annots_arr = match page_dict.get(b"Annots") {
526 Ok(Object::Array(ref arr)) => arr.clone(),
527 Ok(Object::Reference(id)) => match doc.get_object(*id) {
528 Ok(Object::Array(ref arr)) => arr.clone(),
529 _ => continue,
530 },
531 _ => continue,
532 };
533
534 if annots_arr.is_empty() {
535 continue;
536 }
537
538 let mut remaining_annots = Vec::new();
539 let mut draw_ops = Vec::new();
540
541 let mut resources = if let Ok(res_obj) = page_dict.get(b"Resources") {
542 match res_obj {
543 Object::Dictionary(ref d) => d.clone(),
544 Object::Reference(id) => match doc.get_object(*id) {
545 Ok(Object::Dictionary(ref d)) => d.clone(),
546 _ => lopdf::Dictionary::new(),
547 },
548 _ => lopdf::Dictionary::new(),
549 }
550 } else {
551 resolve_inherited_resources_clone(doc, page_id).unwrap_or_default()
552 };
553
554 let mut xobjects = match resources.get(b"XObject") {
555 Ok(Object::Dictionary(ref d)) => d.clone(),
556 Ok(Object::Reference(id)) => match doc.get_object(*id) {
557 Ok(Object::Dictionary(ref d)) => d.clone(),
558 _ => lopdf::Dictionary::new(),
559 },
560 _ => lopdf::Dictionary::new(),
561 };
562
563 let mut flat_annot_count = 0;
564
565 for annot_obj in &annots_arr {
566 let annot_id = match annot_obj {
567 Object::Reference(id) => *id,
568 _ => {
569 remaining_annots.push(annot_obj.clone());
570 continue;
571 }
572 };
573
574 let annot_dict = match doc.get_object(annot_id) {
575 Ok(Object::Dictionary(ref d)) => d.clone(),
576 _ => {
577 remaining_annots.push(annot_obj.clone());
578 continue;
579 }
580 };
581
582 let subtype = match annot_dict.get(b"Subtype") {
583 Ok(Object::Name(ref name)) => name.as_slice(),
584 _ => {
585 remaining_annots.push(annot_obj.clone());
586 continue;
587 }
588 };
589
590 if subtype == b"Widget" || subtype == b"Link" {
592 remaining_annots.push(annot_obj.clone());
593 continue;
594 }
595
596 let ap = match annot_dict.get(b"AP") {
598 Ok(Object::Dictionary(ref d)) => d.clone(),
599 Ok(Object::Reference(id)) => match doc.get_object(*id) {
600 Ok(Object::Dictionary(ref d)) => d.clone(),
601 _ => {
602 remaining_annots.push(annot_obj.clone());
603 continue;
604 }
605 },
606 _ => {
607 remaining_annots.push(annot_obj.clone());
608 continue;
609 }
610 };
611
612 let n_obj = match ap.get(b"N") {
613 Ok(obj) => obj.clone(),
614 _ => {
615 remaining_annots.push(annot_obj.clone());
616 continue;
617 }
618 };
619
620 let (n_stream_id, n_stream) = match n_obj {
621 Object::Reference(id) => match doc.get_object(id) {
622 Ok(Object::Stream(ref s)) => (Some(id), s.clone()),
623 _ => {
624 remaining_annots.push(annot_obj.clone());
625 continue;
626 }
627 },
628 Object::Stream(ref s) => (None, s.clone()),
629 _ => {
630 remaining_annots.push(annot_obj.clone());
631 continue;
632 }
633 };
634
635 let rect_obj = match annot_dict.get(b"Rect") {
637 Ok(Object::Array(ref arr)) if arr.len() >= 4 => arr,
638 _ => {
639 remaining_annots.push(annot_obj.clone());
640 continue;
641 }
642 };
643 let rx0 = match as_number(&rect_obj[0]) {
644 Some(v) => v,
645 None => {
646 remaining_annots.push(annot_obj.clone());
647 continue;
648 }
649 };
650 let ry0 = match as_number(&rect_obj[1]) {
651 Some(v) => v,
652 None => {
653 remaining_annots.push(annot_obj.clone());
654 continue;
655 }
656 };
657 let rx1 = match as_number(&rect_obj[2]) {
658 Some(v) => v,
659 None => {
660 remaining_annots.push(annot_obj.clone());
661 continue;
662 }
663 };
664 let ry1 = match as_number(&rect_obj[3]) {
665 Some(v) => v,
666 None => {
667 remaining_annots.push(annot_obj.clone());
668 continue;
669 }
670 };
671 let rect = [rx0, ry0, rx1, ry1];
672
673 let matrix = match get_matrix_and_bbox(&n_stream.dict, &rect) {
675 Some(m) => m,
676 None => {
677 remaining_annots.push(annot_obj.clone());
678 continue;
679 }
680 };
681
682 let stream_id = match n_stream_id {
684 Some(id) => {
685 if let Ok(Object::Stream(ref mut s)) = doc.get_object_mut(id) {
686 s.dict.set("Type", Object::Name(b"XObject".to_vec()));
687 s.dict.set("Subtype", Object::Name(b"Form".to_vec()));
688 }
689 id
690 }
691 None => {
692 let mut s = n_stream.clone();
693 s.dict.set("Type", Object::Name(b"XObject".to_vec()));
694 s.dict.set("Subtype", Object::Name(b"Form".to_vec()));
695 doc.add_object(Object::Stream(s))
696 }
697 };
698
699 let ap_name = unique_xobject_name(&xobjects, &mut flat_annot_count);
703 xobjects.set(ap_name.as_bytes().to_vec(), Object::Reference(stream_id));
704
705 let draw_op = format!(
707 "q\n{} {} {} {} {} {} cm\n/{} Do\nQ\n",
708 matrix[0], matrix[1], matrix[2], matrix[3], matrix[4], matrix[5], ap_name
709 );
710 draw_ops.extend_from_slice(draw_op.as_bytes());
711
712 deleted_objects.insert(annot_id);
713 if let Ok(Object::Reference(popup_id)) = annot_dict.get(b"Popup") {
716 popups_to_remove.insert(*popup_id);
717 }
718 }
719
720 if !draw_ops.is_empty() {
721 resources.set("XObject", Object::Dictionary(xobjects));
723
724 if let Ok(Object::Dictionary(ref mut pd)) = doc.get_object_mut(page_id) {
725 pd.set("Resources", Object::Dictionary(resources));
726 }
727
728 wrap_existing_content_in_save_restore(doc, page_id);
730
731 let new_content_stream = Stream::new(dictionary! {}, draw_ops);
733 let new_content_id = doc.add_object(Object::Stream(new_content_stream));
734 append_content_to_page(doc, page_id, new_content_id);
735
736 remaining_annots.retain(|o| match o {
739 Object::Reference(id) => !popups_to_remove.contains(id),
740 _ => true,
741 });
742
743 if let Ok(Object::Dictionary(ref mut pd)) = doc.get_object_mut(page_id) {
745 if remaining_annots.is_empty() {
746 pd.remove(b"Annots");
747 } else {
748 pd.set("Annots", Object::Array(remaining_annots));
749 }
750 }
751 }
752 }
753
754 for id in deleted_objects.into_iter().chain(popups_to_remove) {
761 doc.delete_object(id);
762 }
763
764 Ok(())
765}
766
767#[cfg(test)]
768mod tests {
769 use super::{ensure_appearances, flatten_annotations, unique_xobject_name};
770 use lopdf::Dictionary;
771 use lopdf::{dictionary, Document, Object, ObjectId, Stream};
772
773 #[test]
774 fn unique_xobject_name_skips_existing_entries() {
775 let mut xobjects = Dictionary::new();
776 xobjects.set("FlatAnnot0", Object::Null);
777 xobjects.set("FlatAnnot1", Object::Null);
778
779 let mut next = 0usize;
781 assert_eq!(unique_xobject_name(&xobjects, &mut next), "FlatAnnot2");
782
783 xobjects.set("FlatAnnot2", Object::Null);
785 assert_eq!(unique_xobject_name(&xobjects, &mut next), "FlatAnnot3");
786 }
787
788 #[test]
789 fn unique_xobject_name_empty_dict_starts_at_zero() {
790 let xobjects = Dictionary::new();
791 let mut next = 0usize;
792 assert_eq!(unique_xobject_name(&xobjects, &mut next), "FlatAnnot0");
793 assert_eq!(unique_xobject_name(&xobjects, &mut next), "FlatAnnot1");
794 }
795
796 fn one_page_doc() -> (Document, ObjectId) {
797 let mut doc = Document::with_version("1.7");
798 let pages_id = doc.new_object_id();
799 let content_id = doc.add_object(Stream::new(dictionary! {}, b"".to_vec()));
800 let page_id = doc.add_object(dictionary! {
801 "Type" => "Page",
802 "Parent" => pages_id,
803 "MediaBox" => vec![0.into(), 0.into(), 612.into(), 792.into()],
804 "Contents" => content_id,
805 "Resources" => dictionary! {},
806 });
807 doc.objects.insert(
808 pages_id,
809 Object::Dictionary(dictionary! {
810 "Type" => "Pages",
811 "Kids" => vec![Object::Reference(page_id)],
812 "Count" => 1_i64,
813 }),
814 );
815 let catalog_id = doc.add_object(dictionary! {
816 "Type" => "Catalog",
817 "Pages" => pages_id,
818 });
819 doc.trailer.set("Root", catalog_id);
820 (doc, page_id)
821 }
822
823 fn add_annot(doc: &mut Document, page_id: ObjectId, subtype: &str, with_ap: bool) -> ObjectId {
824 let mut dict = dictionary! {
825 "Type" => "Annot",
826 "Subtype" => Object::Name(subtype.as_bytes().to_vec()),
827 "Rect" => vec![10.into(), 10.into(), 110.into(), 60.into()],
828 };
829 if with_ap {
830 let ap = Stream::new(
831 dictionary! {
832 "Type" => "XObject",
833 "Subtype" => "Form",
834 "BBox" => vec![0.into(), 0.into(), 100.into(), 50.into()],
835 },
836 b"0 0 1 RG 0 0 100 50 re S".to_vec(),
837 );
838 let ap_id = doc.add_object(Object::Stream(ap));
839 dict.set(
840 "AP",
841 Object::Dictionary(dictionary! { "N" => Object::Reference(ap_id) }),
842 );
843 }
844 let id = doc.add_object(Object::Dictionary(dict));
845 let page = doc.get_dictionary_mut(page_id).unwrap();
846 match page.get(b"Annots").cloned() {
847 Ok(Object::Array(mut arr)) => {
848 arr.push(Object::Reference(id));
849 page.set("Annots", Object::Array(arr));
850 }
851 _ => {
852 page.set("Annots", Object::Array(vec![Object::Reference(id)]));
853 }
854 }
855 id
856 }
857
858 fn ap_normal(doc: &Document, annot_id: ObjectId) -> Option<ObjectId> {
859 let dict = doc.get_dictionary(annot_id).ok()?;
860 let ap = dict.get(b"AP").ok()?.clone();
861 let ap_dict = match ap {
862 Object::Dictionary(d) => d,
863 Object::Reference(id) => doc.get_dictionary(id).ok()?.clone(),
864 _ => return None,
865 };
866 match ap_dict.get(b"N").ok()?.clone() {
867 Object::Reference(id) => Some(id),
868 _ => None,
869 }
870 }
871
872 #[test]
873 fn ensure_appearances_backfills_only_what_flatten_paints() {
874 let (mut doc, page_id) = one_page_doc();
875 let square = add_annot(&mut doc, page_id, "Square", false);
876 let highlight = add_annot(&mut doc, page_id, "Highlight", false);
877 let kept = add_annot(&mut doc, page_id, "Square", true);
878 let kept_ap = ap_normal(&doc, kept).expect("fixture has an appearance");
879 let link = add_annot(&mut doc, page_id, "Link", false);
880 let widget = add_annot(&mut doc, page_id, "Widget", false);
881 let sound = add_annot(&mut doc, page_id, "Sound", false);
882
883 assert_eq!(ensure_appearances(&mut doc).unwrap(), 2);
884 assert!(ap_normal(&doc, square).is_some());
885 assert!(ap_normal(&doc, highlight).is_some());
886 assert_eq!(ap_normal(&doc, kept), Some(kept_ap));
888 assert_eq!(ap_normal(&doc, link), None);
889 assert_eq!(ap_normal(&doc, widget), None);
890 assert_eq!(ap_normal(&doc, sound), None);
891 }
892
893 #[test]
894 fn backfilled_appearances_flatten_away() {
895 let (mut doc, page_id) = one_page_doc();
896 add_annot(&mut doc, page_id, "Circle", false);
897 add_annot(&mut doc, page_id, "Link", false);
898
899 assert_eq!(ensure_appearances(&mut doc).unwrap(), 1);
900 flatten_annotations(&mut doc).unwrap();
901
902 let page = doc.get_dictionary(page_id).unwrap();
903 let annots = page.get(b"Annots").unwrap().as_array().unwrap().clone();
904 assert_eq!(annots.len(), 1);
905 let survivor = doc
906 .get_dictionary(annots[0].as_reference().unwrap())
907 .unwrap();
908 assert_eq!(
909 survivor.get(b"Subtype").unwrap().as_name().unwrap(),
910 b"Link"
911 );
912 }
913}