1use crate::{EditDoc, Error};
5use kurbo::{Affine, Rect};
6use pdfrum_common::{Diagnostics, Limits, PageIndex};
7use pdfrum_object::{Array, ByteSpan, Dict, Name, ObjRef, Object, Resolve, Stream};
8use std::collections::HashSet;
9
10type Result<T> = core::result::Result<T, Error>;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum FlattenMode {
16 Display,
18 Print,
20}
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum Flattened {
25 Done,
27 NothingToDo,
29}
30
31#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
33#[error("not a flatten mode: {0}")]
34pub struct UnknownFlattenMode(String);
35
36impl std::fmt::Display for FlattenMode {
37 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44 f.write_str(match self {
45 FlattenMode::Display => "display",
46 FlattenMode::Print => "print",
47 })
48 }
49}
50
51impl std::str::FromStr for FlattenMode {
52 type Err = UnknownFlattenMode;
53
54 fn from_str(s: &str) -> core::result::Result<FlattenMode, UnknownFlattenMode> {
61 match s {
62 "display" => Ok(FlattenMode::Display),
63 "print" => Ok(FlattenMode::Print),
64 other => Err(UnknownFlattenMode(other.to_owned())),
65 }
66 }
67}
68
69impl std::fmt::Display for Flattened {
70 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75 f.write_str(match self {
76 Flattened::Done => "flattened",
77 Flattened::NothingToDo => "nothing to do",
78 })
79 }
80}
81
82fn flatten_candidates(edit: &EditDoc<'_>, page: &Dict, mode: FlattenMode) -> Vec<(usize, Dict)> {
86 let Some(annots) = page.array(&Name::from("Annots"), edit) else {
87 return Vec::new();
88 };
89 let mut out = Vec::new();
90 for index in 0..annots.len() {
91 let Some(dict) = annots.dict_at(index, edit) else {
92 continue;
93 };
94 let is_popup = dict
95 .get(&Name::from("Subtype"), edit)
96 .is_some_and(|o| o.get().as_name().is_some_and(|n| n.as_bytes() == b"Popup"));
97 if is_popup {
98 continue;
99 }
100 let flags = dict.int(&Name::from("F"), edit).unwrap_or(0);
101 if flags & 2 != 0 {
102 continue;
103 }
104 let keep = match mode {
105 FlattenMode::Display => flags & 1 == 0,
106 FlattenMode::Print => flags & 4 != 0,
107 };
108 if keep {
109 out.push((index, dict));
110 }
111 }
112 out
113}
114
115fn flatten_appearance(edit: &mut EditDoc<'_>, annot: &Dict) -> Option<(ObjRef, Stream)> {
120 let ap = annot.dict(&Name::from("AP"), edit)?;
121 let states: Dict = match ap.raw(&Name::from("N"))? {
122 Object::Ref(reference) => {
123 let object = edit.fetch(*reference).ok()?;
124 if let Some(stream) = object.as_stream() {
125 return Some((*reference, stream.clone()));
126 }
127 object.as_dict()?.clone()
128 }
129 Object::Stream(stream) => {
130 let stream = stream.clone();
131 let reference = edit.add(Object::Stream(stream.clone()));
132 return Some((reference, *stream));
133 }
134 Object::Dict(states) => states.clone(),
135 _ => return None,
136 };
137 let state = annot
138 .get(&Name::from("AS"), edit)
139 .and_then(|o| o.get().as_name().cloned());
140 let entry = match state {
141 Some(state) => states.raw(&state).cloned()?,
142 None => states.iter().next().map(|(_, o)| o.clone())?,
143 };
144 match entry {
145 Object::Ref(reference) => {
146 let object = edit.fetch(reference).ok()?;
147 Some((reference, object.as_stream()?.clone()))
148 }
149 Object::Stream(stream) => {
150 let reference = edit.add(Object::Stream(stream.clone()));
151 Some((reference, *stream))
152 }
153 _ => None,
154 }
155}
156
157fn sanitize_resources(edit: &mut EditDoc<'_>, mut resources: Dict) -> Dict {
163 let font_key = Name::from("Font");
164 let Some(fonts) = resources.dict(&font_key, edit) else {
165 return resources;
166 };
167 let mut fonts_out = fonts.clone();
168 for (key, value) in fonts.iter() {
169 let (reference, font): (Option<ObjRef>, Dict) = match value {
170 Object::Ref(reference) => {
171 let Ok(object) = edit.fetch(*reference) else {
172 continue;
173 };
174 let Some(dict) = object.as_dict().cloned() else {
175 continue;
176 };
177 (Some(*reference), dict)
178 }
179 Object::Dict(dict) => (None, dict.clone()),
180 _ => continue,
181 };
182 let Some(encoding) = font.dict(&Name::from("Encoding"), edit) else {
183 continue;
184 };
185 let base = encoding
186 .get(&Name::from("BaseEncoding"), edit)
187 .and_then(|o| o.get().as_name().cloned());
188 let valid = base.is_none_or(|name| {
189 matches!(
190 name.as_bytes(),
191 b"WinAnsiEncoding" | b"MacRomanEncoding" | b"MacExpertEncoding"
192 )
193 });
194 if valid {
195 continue;
196 }
197 let mut fixed = font.clone();
198 fixed.remove(&Name::from("Encoding"));
199 match reference {
200 Some(reference) => edit.replace(reference, Object::Dict(fixed)),
201 None => fonts_out.insert(key.clone(), Object::Dict(fixed)),
202 }
203 }
204 match resources.raw(&font_key).cloned() {
205 Some(Object::Ref(reference)) => edit.replace(reference, Object::Dict(fonts_out)),
206 Some(Object::Dict(_)) => resources.insert(font_key, Object::Dict(fonts_out)),
207 _ => {}
208 }
209 resources
210}
211
212fn set_page_contents(edit: &mut EditDoc<'_>, limits: &Limits, page: &mut Dict, key: &str) {
219 let contents = Name::from("Contents");
220 let do_text = format!("q 1 0 0 1 0 0 cm /{key} Do Q");
221 let mut array: Option<(Array, Option<ObjRef>)> = None;
222 let mut single: Option<(Stream, ObjRef)> = None;
223 match page.raw(&contents).cloned() {
224 Some(Object::Ref(reference)) => {
225 if let Ok(object) = edit.fetch(reference) {
226 if let Some(a) = object.as_array() {
227 array = Some((a.clone(), Some(reference)));
228 } else if let Some(s) = object.as_stream() {
229 single = Some((s.clone(), reference));
230 }
231 }
232 }
233 Some(Object::Array(a)) => array = Some((a, None)),
234 Some(Object::Stream(s)) => {
235 let reference = edit.add(Object::Stream(s.clone()));
236 single = Some((*s, reference));
237 }
238 _ => {}
239 }
240 if let Some((mut a, reference)) = array {
241 let q = raw_stream(edit, b"q".to_vec());
242 a.insert(0, Object::Ref(q));
243 let restore = raw_stream(edit, b"Q".to_vec());
244 a.push(Object::Ref(restore));
245 let draw = raw_stream(edit, do_text.into_bytes());
246 a.push(Object::Ref(draw));
247 match reference {
248 Some(reference) => edit.replace(reference, Object::Array(a)),
249 None => page.insert(contents, Object::Array(a)),
250 }
251 } else if let Some((stream, reference)) = single {
252 let mut diags = Diagnostics::default();
253 let decoded = pdfrum_filters::decode_chain(&stream, 0, edit, limits, &mut diags).data;
254 let mut bytes = b"q\n".to_vec();
255 bytes.extend_from_slice(&decoded);
256 bytes.extend_from_slice(b"\nQ");
257 let mut dict = stream.dict.clone();
258 dict.remove(&Name::from("Filter"));
259 dict.remove(&Name::from("DecodeParms"));
260 dict.remove(&Name::from("Length"));
261 edit.replace(
262 reference,
263 Object::Stream(Box::new(Stream::new(dict, ByteSpan::from(bytes)))),
264 );
265 let draw = raw_stream(edit, do_text.into_bytes());
266 let array_ref = edit.add(Object::Array(Array::of([
267 Object::Ref(reference),
268 Object::Ref(draw),
269 ])));
270 page.insert(contents, Object::Ref(array_ref));
271 } else {
272 let draw = raw_stream(edit, do_text.into_bytes());
273 page.insert(contents, Object::Ref(draw));
274 }
275}
276
277fn raw_stream(edit: &mut EditDoc<'_>, bytes: Vec<u8>) -> ObjRef {
279 edit.add(Object::Stream(Box::new(Stream::new(
280 Dict::new(),
281 ByteSpan::from(bytes),
282 ))))
283}
284
285fn flattened_widgets(edit: &EditDoc<'_>, page: &Dict, page_ref: Option<ObjRef>) -> HashSet<ObjRef> {
288 let annots_key = Name::from("Annots");
289 let mut widgets = HashSet::new();
290 if let Some(annots) = page.array(&annots_key, edit) {
291 for index in 0..annots.len() {
292 let (Some(reference), Some(dict)) =
293 (annots.reference_at(index), annots.dict_at(index, edit))
294 else {
295 continue;
296 };
297 let is_widget = dict
298 .get(&Name::from("Subtype"), edit)
299 .is_some_and(|o| o.get().as_name().is_some_and(|n| n.as_bytes() == b"Widget"));
300 if is_widget {
301 widgets.insert(reference);
302 }
303 }
304 }
305 for index in 0..edit.base().page_count() {
306 if widgets.is_empty() {
307 break;
308 }
309 let Ok(other) = edit.base().page(index) else {
310 continue;
311 };
312 if other.reference.is_some() && other.reference == page_ref {
313 continue;
314 }
315 let other_dict = other
319 .reference
320 .and_then(|reference| edit.fetch(reference).ok())
321 .and_then(|object| object.as_dict().cloned())
322 .unwrap_or(other.dict);
323 if let Some(annots) = other_dict.array(&annots_key, edit) {
324 for slot in 0..annots.len() {
325 if let Some(reference) = annots.reference_at(slot) {
326 widgets.remove(&reference);
327 }
328 }
329 }
330 }
331 widgets
332}
333
334fn prune_fields(
338 edit: &mut EditDoc<'_>,
339 fields: &mut Array,
340 widgets: &HashSet<ObjRef>,
341 level: u32,
342) -> bool {
343 if level > 32 {
344 return fields.is_empty();
345 }
346 let kids_key = Name::from("Kids");
347 for index in (0..fields.len()).rev() {
348 let Some(reference) = fields.reference_at(index) else {
349 continue;
350 };
351 let mut prune = widgets.contains(&reference);
352 if !prune
353 && let Ok(object) = edit.fetch(reference)
354 && let Some(field) = object.as_dict()
355 {
356 match field.raw(&kids_key).cloned() {
357 Some(Object::Ref(kids_ref)) => {
358 if let Ok(kids_object) = edit.fetch(kids_ref)
359 && let Some(kids) = kids_object.as_array()
360 {
361 let mut kids = kids.clone();
362 prune = prune_fields(edit, &mut kids, widgets, level + 1);
363 edit.replace(kids_ref, Object::Array(kids));
364 }
365 }
366 Some(Object::Array(mut kids)) => {
367 prune = prune_fields(edit, &mut kids, widgets, level + 1);
368 let mut field = field.clone();
369 field.insert(kids_key.clone(), Object::Array(kids));
370 edit.replace(reference, Object::Dict(field));
371 }
372 _ => {}
373 }
374 }
375 if prune {
376 fields.remove(index);
377 }
378 }
379 fields.is_empty()
380}
381
382fn remove_flattened_fields(edit: &mut EditDoc<'_>, widgets: &HashSet<ObjRef>) {
387 if widgets.is_empty() {
388 return;
389 }
390 let Some(root) = edit.base().trailer().reference(&Name::from("Root")) else {
391 return;
392 };
393 let Some(mut catalog) = edit
394 .fetch(root)
395 .ok()
396 .as_deref()
397 .and_then(Object::as_dict)
398 .cloned()
399 else {
400 return;
401 };
402 let acro_key = Name::from("AcroForm");
403 let fields_key = Name::from("Fields");
404 let (acro_ref, mut acro) = match catalog.raw(&acro_key).cloned() {
405 Some(Object::Ref(reference)) => {
406 let Some(dict) = edit
407 .fetch(reference)
408 .ok()
409 .as_deref()
410 .and_then(Object::as_dict)
411 .cloned()
412 else {
413 return;
414 };
415 (Some(reference), dict)
416 }
417 Some(Object::Dict(dict)) => (None, dict),
418 _ => return,
419 };
420 let (fields_ref, mut fields) = match acro.raw(&fields_key).cloned() {
421 Some(Object::Ref(reference)) => {
422 let Some(array) = edit
423 .fetch(reference)
424 .ok()
425 .as_deref()
426 .and_then(Object::as_array)
427 .cloned()
428 else {
429 return;
430 };
431 (Some(reference), array)
432 }
433 Some(Object::Array(array)) => (None, array),
434 _ => return,
435 };
436 let empty = prune_fields(edit, &mut fields, widgets, 0);
437 match fields_ref {
438 Some(reference) => edit.replace(reference, Object::Array(fields)),
439 None => acro.insert(fields_key, Object::Array(fields)),
440 }
441 if empty && !acro.contains_key(&Name::from("XFA")) {
442 catalog.remove(&acro_key);
443 edit.replace(root, Object::Dict(catalog));
444 return;
445 }
446 if let Some(reference) = acro_ref {
447 edit.replace(reference, Object::Dict(acro));
448 } else {
449 catalog.insert(acro_key, Object::Dict(acro));
450 edit.replace(root, Object::Dict(catalog));
451 }
452}
453
454fn flatten_one(
458 edit: &mut EditDoc<'_>,
459 index: usize,
460 annot: &Dict,
461 generated: Option<&pdfrum_doc::ap::GeneratedAp>,
462 xobject_dict: &mut Dict,
463 content: &mut String,
464) {
465 use std::fmt::Write as _;
466 let mut annot_rect = annot.rect(&Name::from("Rect"), edit).abs();
467 let appearance = match generated {
471 Some(made) if !made.stream.is_empty() => {
472 if let Some(rect) = made.rect_override {
473 annot_rect = rect.abs();
474 }
475 let stream = Stream::new(
476 pdfrum_doc::ap::stream_dict(made),
477 ByteSpan::from(made.stream.clone()),
478 );
479 let reference = edit.add(Object::Stream(Box::new(stream.clone())));
480 Some((reference, stream))
481 }
482 _ => flatten_appearance(edit, annot),
483 };
484 let Some((ap_ref, ap_stream)) = appearance else {
485 return;
486 };
487 let mut ap_dict = ap_stream.dict.clone();
488 let box_key = if ap_dict.contains_key(&Name::from("Rect")) {
489 Name::from("Rect")
490 } else {
491 Name::from("BBox")
492 };
493 let mut stream_rect = ap_dict.rect(&box_key, edit).abs();
494 if stream_rect.width() <= 0.0 || stream_rect.height() <= 0.0 {
495 if annot_rect.width() <= 0.0 || annot_rect.height() <= 0.0 {
499 return;
500 }
501 stream_rect = Rect::new(0.0, 0.0, annot_rect.width(), annot_rect.height());
502 ap_dict.insert(Name::from("BBox"), rect_object(stream_rect));
503 }
504 ap_dict.insert(Name::from("Type"), Object::Name(Name::from("XObject")));
505 ap_dict.insert(Name::from("Subtype"), Object::Name(Name::from("Form")));
506 if let Some(res) = ap_dict.dict(&Name::from("Resources"), edit) {
507 let res = sanitize_resources(edit, res);
508 ap_dict.insert(Name::from("Resources"), Object::Dict(res));
509 }
510 let matrix = flatten_matrix(
511 annot_rect,
512 stream_rect,
513 ap_stream.dict.matrix(&Name::from("Matrix"), edit),
514 );
515 edit.replace(
516 ap_ref,
517 Object::Stream(Box::new(Stream::new(ap_dict, ap_stream.data.clone()))),
518 );
519 let name = format!("F{index}");
520 xobject_dict.insert(Name::from(name.as_str()), Object::Ref(ap_ref));
521 let [sx, ky, kx, sy, tx, ty] = matrix.as_coeffs();
522 let _ = writeln!(content, "q {sx} {ky} {kx} {sy} {tx} {ty} cm /{name} Do Q");
523}
524
525pub fn flatten(
553 edit: &mut EditDoc<'_>,
554 limits: &Limits,
555 page: impl Into<PageIndex>,
556 mode: FlattenMode,
557 diags: &mut Diagnostics,
558) -> Result<Flattened> {
559 let index = page.into();
560 let page_dict = edit
561 .base()
562 .page(index)
563 .map_err(|_| Error::PageIndexOutOfRange(index))?;
564 let page_ref = page_dict.reference;
565 let mut dict = page_dict.dict.clone();
566 if dict.array(&Name::from("Annots"), edit).is_none() {
567 return Ok(Flattened::NothingToDo);
568 }
569 let candidates = flatten_candidates(edit, &dict, mode);
570 let catalog = edit.base().catalog().unwrap_or_default();
571 let mut build = pdfrum_page::BuildContext::new();
572 let fonts = pdfrum_doc::ap::FormFonts::load(&catalog, edit, &mut build);
573 let generated =
574 pdfrum_doc::ap::generate_appearances_with_text(&dict, &catalog, Some(&fonts), edit, diags);
575
576 let crop_key = Name::from("CropBox");
577 let mut media = dict.rect(&Name::from("MediaBox"), edit).abs();
578 if dict.contains_key(&crop_key) {
579 media = dict.rect(&crop_key, edit).abs();
580 }
581 if media.width() <= 0.0 || media.height() <= 0.0 {
582 media = Rect::new(0.0, 0.0, 612.0, 792.0);
583 }
584 let mut crop = if dict.contains_key(&crop_key) {
585 dict.rect(&crop_key, edit).abs()
586 } else {
587 Rect::ZERO
588 };
589 if crop.width() <= 0.0 || crop.height() <= 0.0 {
590 crop = media;
591 }
592 dict.insert(Name::from("MediaBox"), rect_object(media));
593 dict.insert(crop_key, rect_object(crop));
594
595 let mut resources = dict
596 .dict(&Name::from("Resources"), edit)
597 .unwrap_or_default();
598 let mut xobjects = resources
599 .dict(&Name::from("XObject"), edit)
600 .unwrap_or_default();
601 let key = if candidates.is_empty() {
602 None
603 } else {
604 (0..u32::MAX)
605 .map(|i| format!("FFT{i}"))
606 .find(|k| !xobjects.contains_key(&Name::from(k.as_str())))
607 };
608 if let Some(key) = key {
609 set_page_contents(edit, limits, &mut dict, &key);
610 let mut xobject_dict = Dict::new();
611 let mut content = String::new();
612 for (slot, (index, annot)) in candidates.iter().enumerate() {
613 let made_appearance = generated.get(*index);
614 flatten_one(
615 edit,
616 slot,
617 annot,
618 made_appearance,
619 &mut xobject_dict,
620 &mut content,
621 );
622 }
623 let mut form_resources = Dict::new();
624 form_resources.insert(Name::from("XObject"), Object::Dict(xobject_dict));
625 let mut form_dict = Dict::new();
626 form_dict.insert(Name::from("Type"), Object::Name(Name::from("XObject")));
627 form_dict.insert(Name::from("Subtype"), Object::Name(Name::from("Form")));
628 form_dict.insert(Name::from("FormType"), Object::Int(1));
629 form_dict.insert(Name::from("BBox"), rect_object(crop));
630 form_dict.insert(Name::from("Resources"), Object::Dict(form_resources));
631 let form_ref = edit.add(Object::Stream(Box::new(Stream::new(
632 form_dict,
633 ByteSpan::from(content.into_bytes()),
634 ))));
635 xobjects.insert(Name::from(key.as_str()), Object::Ref(form_ref));
636 resources.insert(Name::from("XObject"), Object::Dict(xobjects));
637 dict.insert(Name::from("Resources"), Object::Dict(resources));
638 }
639
640 let widgets = flattened_widgets(edit, &dict, page_ref);
641 dict.remove(&Name::from("Annots"));
642 if let Some(reference) = page_ref {
643 edit.replace(reference, Object::Dict(dict));
644 }
645 remove_flattened_fields(edit, &widgets);
646 Ok(Flattened::Done)
647}
648
649#[expect(
651 clippy::cast_possible_truncation,
652 reason = "page geometry fits f32, as the file wrote it"
653)]
654fn rect_object(rect: Rect) -> Object {
655 Object::Array(Array::of([
656 Object::Real(rect.x0 as f32),
657 Object::Real(rect.y0 as f32),
658 Object::Real(rect.x1 as f32),
659 Object::Real(rect.y1 as f32),
660 ]))
661}
662
663fn flatten_matrix(annot_rect: Rect, stream_rect: Rect, matrix: Affine) -> Affine {
667 let transformed = matrix.transform_rect_bbox(stream_rect).abs();
668 if transformed.width() <= 0.0 || transformed.height() <= 0.0 {
669 return Affine::IDENTITY;
670 }
671 let a = annot_rect.width() / transformed.width();
672 let d = annot_rect.height() / transformed.height();
673 Affine::new([
674 a,
675 0.0,
676 0.0,
677 d,
678 annot_rect.x0 - transformed.x0 * a,
679 annot_rect.y0 - transformed.y0 * d,
680 ])
681}