1use std::collections::BTreeMap;
2
3use lopdf::StringFormat;
4use serde_derive::{Deserialize, Serialize};
5
6use crate::{
7 date::OffsetDateTime,
8 deserialize::PageState,
9 image_types::{RawImage, ImageOptimizationOptions},
10 matrix::CurTransMat,
11 units::{Pt, Px},
12 Op,
13};
14
15#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
21#[serde(rename_all = "kebab-case", tag = "type", content = "data")]
22pub enum XObject {
23 Image(RawImage),
25 Form(FormXObject),
28 External(ExternalXObject),
38}
39
40impl XObject {
41 pub fn get_width_height(&self) -> Option<(Px, Px)> {
42 match self {
43 XObject::Image(raw_image) => Some((Px(raw_image.width), Px(raw_image.height))),
44 XObject::Form(form_xobject) => form_xobject.size,
45 XObject::External(external_xobject) => {
46 Some((external_xobject.width?, external_xobject.height?))
47 }
48 }
49 }
50}
51
52pub(crate) fn add_xobject_to_document(
54 xobj: &XObject,
55 doc: &mut lopdf::Document,
56 _image_opts: Option<&ImageOptimizationOptions>,
57) -> lopdf::ObjectId {
58 match xobj {
60 XObject::Image(_i) => {
61 #[cfg(feature = "images")]
62 {
63 let stream = crate::image::image_to_stream(_i.clone(), doc, _image_opts);
64 doc.add_object(stream)
65 }
66 #[cfg(not(feature = "images"))]
67 {
68 panic!("Image XObjects require the 'images' feature");
69 }
70 }
71 XObject::Form(f) => {
72 let stream = form_xobject_to_stream(f, doc);
73 doc.add_object(stream)
74 }
75 XObject::External(external_xobject) => {
76 let stream = external_xobject.stream.into_lopdf();
77 doc.add_object(stream)
78 }
79 }
80}
81
82#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
84#[serde(rename_all = "camelCase")]
85pub struct ExternalXObject {
86 pub stream: ExternalStream,
88 #[serde(default)]
90 pub width: Option<Px>,
91 #[serde(default)]
93 pub height: Option<Px>,
94 #[serde(default)]
96 pub dpi: Option<f32>,
97}
98
99#[derive(Debug, PartialEq, Default, Clone, Serialize, Deserialize)]
100#[serde(rename_all = "camelCase")]
101pub struct ExternalStream {
102 pub dict: BTreeMap<String, DictItem>,
104 pub content: Vec<u8>,
106 pub compress: bool,
108}
109
110impl ExternalStream {
111 pub(crate) fn into_lopdf(&self) -> lopdf::Stream {
112 lopdf::Stream::new(build_dict(&self.dict), self.content.clone())
113 .with_compression(self.compress)
114 }
115 pub fn decompressed_content(&self) -> Vec<u8> {
116 self.into_lopdf()
117 .decompressed_content()
118 .unwrap_or(self.content.clone())
119 }
120
121 pub fn decode_ops(s: &str) -> Result<Vec<Op>, String> {
123 Self::get_ops_internal(s.as_bytes())
124 }
125
126 pub fn get_ops(&self) -> Result<Vec<Op>, String> {
128 Self::get_ops_internal(&self.decompressed_content())
129 }
130
131 fn get_ops_internal(s: &[u8]) -> Result<Vec<Op>, String> {
132 let content = lopdf::content::Content::decode(&s)
134 .map_err(|e| format!("Failed to decode content stream: {}", e))?;
135
136 let mut page_state = PageState::default();
138 let mut printpdf_ops = Vec::new();
139
140 for (op_id, op) in content.operations.iter().enumerate() {
141 let parsed_op = crate::deserialize::parse_op(
142 0,
143 op_id,
144 &op,
145 &mut page_state,
146 &BTreeMap::new(),
147 &mut Vec::new(),
148 )?;
149 printpdf_ops.extend(parsed_op.into_iter());
150 }
151
152 Ok(printpdf_ops)
153 }
154}
155
156#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
158#[serde(rename_all = "kebab-case", tag = "type", content = "data")]
159pub enum DictItem {
160 Array(Vec<DictItem>),
161 String { data: Vec<u8>, literal: bool },
162 Bytes(Vec<u8>),
163 Bool(bool),
164 Float(f32),
165 Int(i64),
166 Real(f32),
167 Name(Vec<u8>),
168 Ref { obj: u32, gen: u16 },
169 Dict { map: BTreeMap<String, DictItem> },
170 Stream { stream: ExternalStream },
171 Null,
172}
173
174impl DictItem {
175 pub fn to_lopdf(&self) -> lopdf::Object {
176 use lopdf::{Object, StringFormat};
177 match self {
178 DictItem::Array(items) => {
179 let objs = items.iter().map(|item| item.to_lopdf()).collect();
180 Object::Array(objs)
181 }
182 DictItem::String { data, literal } => {
183 let format = if *literal {
184 StringFormat::Literal
185 } else {
186 StringFormat::Hexadecimal
187 };
188 Object::String(data.clone(), format)
189 }
190 DictItem::Bytes(data) => {
191 Object::String(data.clone(), StringFormat::Hexadecimal)
193 }
194 DictItem::Bool(b) => Object::Boolean(*b),
195 DictItem::Float(f) => Object::Real(*f),
196 DictItem::Int(i) => Object::Integer(*i),
197 DictItem::Real(f) => Object::Real(*f),
198 DictItem::Name(name) => Object::Name(name.clone()),
199 DictItem::Ref { obj, gen } => Object::Reference((*obj, *gen)),
200 DictItem::Dict { map } => {
201 let dict = map
202 .iter()
203 .map(|(k, v)| (k.as_bytes().to_vec(), v.to_lopdf()))
204 .collect();
205 Object::Dictionary(dict)
206 }
207 DictItem::Stream { stream } => {
208 let stream_obj = stream.into_lopdf();
209 Object::Stream(stream_obj)
210 }
211 DictItem::Null => Object::Null,
212 }
213 }
214
215 pub fn from_lopdf(o: &lopdf::Object) -> Self {
216 use lopdf::Object;
217 match o {
218 Object::Null => DictItem::Null,
219 Object::Boolean(t) => DictItem::Bool(*t),
220 Object::Integer(i) => DictItem::Int(*i),
221 Object::Real(r) => DictItem::Real(*r),
222 Object::Name(items) => DictItem::Name(items.clone()),
223 Object::String(items, string_format) => DictItem::String {
224 data: items.clone(),
225 literal: *string_format == StringFormat::Literal,
226 },
227 Object::Array(objects) => {
228 DictItem::Array(objects.iter().map(DictItem::from_lopdf).collect())
229 }
230 Object::Dictionary(dictionary) => DictItem::Dict {
231 map: dictionary
232 .iter()
233 .map(|s| {
234 (
235 String::from_utf8_lossy(&s.0).to_string(),
236 DictItem::from_lopdf(s.1),
237 )
238 })
239 .collect(),
240 },
241 Object::Stream(stream) => DictItem::Stream {
242 stream: ExternalStream {
243 compress: stream.allows_compression,
244 content: stream.content.clone(),
245 dict: stream
246 .dict
247 .iter()
248 .map(|s| {
249 (
250 String::from_utf8_lossy(&s.0).to_string(),
251 DictItem::from_lopdf(s.1),
252 )
253 })
254 .collect(),
255 },
256 },
257 Object::Reference((a, b)) => DictItem::Ref { obj: *a, gen: *b },
258 }
259 }
260}
261
262#[derive(Debug, PartialEq, Copy, Clone)]
264pub enum ImageFilter {
265 Ascii85,
267 Lzw,
269 DCT,
271 JPX,
273}
274
275#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
281#[serde(rename_all = "camelCase")]
282pub struct FormXObject {
283 pub form_type: FormType,
288 pub size: Option<(Px, Px)>,
290 pub bytes: Vec<u8>,
292 pub matrix: Option<CurTransMat>,
295 pub resources: Option<BTreeMap<String, DictItem>>,
314 pub group: Option<GroupXObject>,
323 pub ref_dict: Option<BTreeMap<String, DictItem>>,
328 pub metadata: Option<BTreeMap<String, DictItem>>,
332 pub piece_info: Option<BTreeMap<String, DictItem>>,
336 pub last_modified: Option<OffsetDateTime>,
343 pub struct_parent: Option<i64>,
348 pub struct_parents: Option<i64>,
357 pub opi: Option<BTreeMap<String, DictItem>>,
361 pub oc: Option<BTreeMap<String, DictItem>>,
367 pub name: Option<String>,
374}
375
376fn form_xobject_to_stream(f: &FormXObject, doc: &mut lopdf::Document) -> lopdf::Stream {
377 use lopdf::Object::{String as LoString, *};
378
379 let mut dict = lopdf::Dictionary::from_iter(vec![
380 ("Type", Name("XObject".into())),
381 ("Subtype", Name("Form".into())),
382 ("FormType", Name(f.form_type.get_id().into())),
383 ]);
384
385 if let Some(matrix) = f.matrix.as_ref() {
386 dict.set(
387 "Matrix",
388 Array(matrix.as_array().into_iter().map(Real).collect()),
389 );
390 }
391
392 if let Some(res) = f.resources.as_ref() {
393 dict.set("Resources", build_dict(res));
394 }
395
396 if let Some(g) = f.group.as_ref() {
397 let group_dict = lopdf::Dictionary::from_iter(vec![
398 ("Type", Name("Group".into())),
399 ("S", Name(g.group_type.get_id().into())),
400 ]);
401
402 dict.set("Group", Dictionary(group_dict));
403 }
404
405 if let Some(r) = f.ref_dict.as_ref() {
406 dict.set("Ref", build_dict(&r));
407 }
408
409 if let Some(r) = f.metadata.as_ref() {
410 dict.set("Metadata", doc.add_object(build_dict(&r)));
411 }
412
413 if let Some(r) = f.piece_info.as_ref() {
414 dict.set("PieceInfo", doc.add_object(build_dict(&r)));
415 }
416
417 if let Some(r) = f.last_modified.as_ref() {
418 dict.set(
419 "LastModified",
420 LoString(
421 crate::utils::to_pdf_time_stamp_metadata(r).into_bytes(),
422 lopdf::StringFormat::Literal,
423 ),
424 );
425 }
426
427 if let Some(r) = f.opi.as_ref() {
428 dict.set("OPI", build_dict(&r));
429 }
430
431 if let Some(r) = f.oc.as_ref() {
432 dict.set("OC", build_dict(&r));
433 }
434
435 if let Some(r) = f.name.as_ref() {
436 dict.set(
437 "Name",
438 LoString(r.clone().into(), lopdf::StringFormat::Literal),
439 );
440 }
441
442 if let Some(sp) = &f.struct_parents {
443 dict.set("StructParents", Integer(*sp));
444 } else if let Some(sp) = &f.struct_parent {
445 dict.set("StructParent", Integer(*sp));
446 }
447
448 let stream = lopdf::Stream::new(dict, f.bytes.clone()).with_compression(true);
449 stream
451}
452
453pub fn build_dict(r: &BTreeMap<String, DictItem>) -> lopdf::Dictionary {
454 lopdf::Dictionary::from_iter(r.iter().map(|(k, v)| (k.clone(), v.to_lopdf())))
455}
456
457#[derive(Debug, PartialEq, Copy, Clone, Serialize, Deserialize)]
458#[serde(rename_all = "kebab-case")]
459pub enum FormType {
460 Type1,
463}
464
465impl FormType {
466 fn get_id(&self) -> &'static str {
467 match self {
468 FormType::Type1 => "Type1",
469 }
470 }
471}
472
473#[derive(Debug, PartialEq, Copy, Clone, Serialize, Deserialize)]
475#[serde(rename_all = "camelCase")]
476pub struct GroupXObject {
477 #[serde(default)]
478 pub group_type: GroupXObjectType,
479}
480
481#[derive(Debug, Default, PartialEq, Copy, Clone, Serialize, Deserialize)]
482#[serde(rename_all = "kebab-case")]
483pub enum GroupXObjectType {
484 #[default]
486 TransparencyGroup,
487}
488
489impl GroupXObjectType {
490 pub fn get_id(&self) -> &'static str {
491 match self {
492 GroupXObjectType::TransparencyGroup => "Transparency",
493 }
494 }
495}
496
497#[derive(Debug, PartialEq, Clone, Default, Deserialize, Serialize)]
500pub struct ReferenceXObject {
501 pub file: Vec<u8>,
503 pub page: i64,
505 pub id: [i64; 2],
507}
508
509#[derive(Debug, PartialEq, Clone, Default, Deserialize, Serialize)]
511pub struct PostScriptXObject {
512 #[allow(dead_code)]
516 pub level1: Option<Vec<u8>>,
517}
518
519#[derive(Debug, Copy, Clone, Default, PartialEq, Deserialize, Serialize)]
523pub struct XObjectTransform {
524 #[serde(default)]
525 pub translate_x: Option<Pt>,
526 #[serde(default)]
527 pub translate_y: Option<Pt>,
528 #[serde(default)]
530 pub rotate: Option<XObjectRotation>,
531 #[serde(default)]
532 pub scale_x: Option<f32>,
533 #[serde(default)]
534 pub scale_y: Option<f32>,
535 #[serde(default)]
537 pub dpi: Option<f32>,
538}
539
540impl XObjectTransform {
541 pub fn get_ctms(&self, wh: Option<(Px, Px)>) -> Vec<CurTransMat> {
542 let mut transforms = Vec::new();
543 let dpi = self.dpi.unwrap_or(300.0);
544
545 if let Some((w, h)) = wh {
546 transforms.push(CurTransMat::Scale(w.into_pt(dpi).0, h.into_pt(dpi).0));
551 }
552
553 if self.scale_x.is_some() || self.scale_y.is_some() {
554 let scale_x = self.scale_x.unwrap_or(1.0);
555 let scale_y = self.scale_y.unwrap_or(1.0);
556 transforms.push(CurTransMat::Scale(scale_x, scale_y));
557 }
558
559 if let Some(rotate) = self.rotate.as_ref() {
560 transforms.push(CurTransMat::Translate(
561 Pt(-rotate.rotation_center_x.into_pt(dpi).0),
562 Pt(-rotate.rotation_center_y.into_pt(dpi).0),
563 ));
564 transforms.push(CurTransMat::Rotate(rotate.angle_ccw_degrees));
565 transforms.push(CurTransMat::Translate(
566 rotate.rotation_center_x.into_pt(dpi),
567 rotate.rotation_center_y.into_pt(dpi),
568 ));
569 }
570
571 if self.translate_x.is_some() || self.translate_y.is_some() {
572 transforms.push(CurTransMat::Translate(
573 self.translate_x.unwrap_or(Pt(0.0)),
574 self.translate_y.unwrap_or(Pt(0.0)),
575 ));
576 }
577
578 transforms
579 }
580
581 pub fn as_svg_transform(&self) -> String {
584 let ctms = self.get_ctms(None);
586
587 let mut combined = CurTransMat::Identity;
589
590 for t in ctms {
592 let new_arr = CurTransMat::combine_matrix(combined.as_array(), t.as_array());
594 combined = CurTransMat::Raw(new_arr);
595 }
596
597 let arr = combined.as_array();
599 format!(
601 "matrix({} {} {} {} {} {})",
602 arr[0], arr[1], arr[2], arr[3], arr[4], arr[5]
603 )
604 }
605}
606
607#[derive(Debug, Copy, Clone, Default, PartialEq, Serialize, Deserialize)]
608#[serde(rename_all = "camelCase")]
609pub struct XObjectRotation {
610 #[serde(default)]
611 pub angle_ccw_degrees: f32,
612 #[serde(default)]
613 pub rotation_center_x: Px,
614 #[serde(default)]
615 pub rotation_center_y: Px,
616}