diff --git a/.DS_Store b/.DS_Store index ddb808a..c1b8113 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/examples/assets/.DS_Store b/examples/assets/.DS_Store index 2d45635..0b4c561 100644 Binary files a/examples/assets/.DS_Store and b/examples/assets/.DS_Store differ diff --git a/examples/simple.rs b/examples/simple.rs index e6cda43..5cd62c0 100644 --- a/examples/simple.rs +++ b/examples/simple.rs @@ -101,15 +101,18 @@ fn main() { let xobject_id = doc.add_xobject(&svg); for i in 0..10 { + let transform = XObjectTransform { rotate: Some(XObjectRotation { angle_ccw_degrees: i as f32 * 36.0, - rotation_center_x: rotation_center_x.into_pt(300.0), - rotation_center_y: rotation_center_y.into_pt(300.0), + rotation_center_x: rotation_center_x, + rotation_center_y: rotation_center_y, }), translate_x: Some(Mm(i as f32 * 20.0 % 50.0).into()), translate_y: Some(Mm(i as f32 * 30.0).into()), - ..Default::default() + dpi: Some(300.0), + scale_x: None, + scale_y: None, }; ops.extend_from_slice(&[ diff --git a/src/annotation.rs b/src/annotation.rs index c7d3761..28984ad 100644 --- a/src/annotation.rs +++ b/src/annotation.rs @@ -14,26 +14,26 @@ pub struct PageAnnotation { pub struct LinkAnnotation { pub rect: Rect, pub border: BorderArray, - pub c: ColorArray, - pub a: Actions, - pub h: HighlightingMode, + pub color: ColorArray, + pub actions: Actions, + pub highlighting: HighlightingMode, } impl LinkAnnotation { /// Creates a new LinkAnnotation pub fn new( rect: Rect, + actions: Actions, border: Option, - c: Option, - a: Actions, - h: Option, + color: Option, + highlighting: Option, ) -> Self { Self { rect, border: border.unwrap_or_default(), - c: c.unwrap_or_default(), - a, - h: h.unwrap_or_default(), + color: color.unwrap_or_default(), + actions, + highlighting: highlighting.unwrap_or_default(), } } } @@ -44,6 +44,31 @@ pub enum BorderArray { Dashed([f32; 3], DashPhase), } +impl BorderArray { + pub fn to_array(&self) -> Vec { + match self { + BorderArray::Solid(s) => s.to_vec(), + BorderArray::Dashed(s, dash_phase) => { + let mut s = s.to_vec(); + s.push(dash_phase.phase); + s + }, + } + } +} + +/* + + impl Into for DashPhase { + fn into(self) -> Object { + Object::Array(vec![ + Object::Array(self.dash_array.into_iter().map(|x| Object::Real(x.into())).collect()), + Object::Real(self.phase.into()), + ]) + } + } +*/ + impl Default for BorderArray { fn default() -> Self { BorderArray::Solid([0.0, 0.0, 1.0]) @@ -86,6 +111,26 @@ pub enum Destination { }, } +/* + GoTo Go to a destination in the current document. “Go-To Actions” on page 654 + GoToR (“Go-to remote”) Go to a destination in another document. “Remote Go-To Actions” on page 655 + GoToE (“Go-to embedded”; PDF 1.6) Go to a destination in an embedded file. “Embedded Go-To Actions” on page 655 + Launch Launch an application, usually to open a file. “Launch Actions” on page 659 + Thread Begin reading an article thread. “Thread Actions” on page 661 + URI Resolve a uniform resource identifier. “URI Actions” on page 662 + Sound (PDF 1.2) Play a sound. “Sound Actions” on page 663 + Movie (PDF 1.2) Play a movie. “Movie Actions” on page 664 + Hide (PDF 1.2) Set an annotation’s Hidden flag. “Hide Actions” on page 665 + Named (PDF 1.2) Execute an action predefined by the viewer application. “Named Actions” on page 666 + SubmitForm (PDF 1.2) Send data to a uniform resource locator. “Submit-Form Actions” on page 703 + ResetForm (PDF 1.2) Set fields to their default values. “Reset-Form Actions” on page 707 + ImportData (PDF 1.2) Import field values from a file. “Import-Data Actions” on page 708 + JavaScript (PDF 1.3) Execute a JavaScript script. “JavaScript Actions” on page 709 + SetOCGState (PDF 1.5) Set the states of optional content groups. “Set-OCG-State Actions” on page 667 + Rendition (PDF 1.5) Controls the playing of multimedia content. “Rendition Actions” on page 668 + Trans (PDF 1.5) Updates the display of a document, using a transition dictionary. “Transition Actions” on page 670 + GoTo3DView (PDF 1.6) Set the current view of a 3D annotation “Go-To-3D-View Actions” on page 670 +*/ #[derive(Debug, PartialEq, Clone)] pub enum Actions { GoTo(Destination), @@ -93,6 +138,18 @@ pub enum Actions { } impl Actions { + + /// 8.5.3 Action Types: PDF supports the standard action types listed in Table 8.48. + /// + /// The following sections describe each of these types in detail. + /// Plug-in extensions may add new action types. + pub fn get_action_type_id(&self) -> &'static str { + match self { + Actions::GoTo(_) => "GoTo", + Actions::URI(_) => "URI", + } + } + pub fn go_to(destination: Destination) -> Self { Self::GoTo(destination) } @@ -115,3 +172,15 @@ impl Default for HighlightingMode { HighlightingMode::Invert } } + +impl HighlightingMode { + pub fn get_id(&self) -> &'static str { + use self::HighlightingMode::*; + match self { + None => "N", + Invert => "I", + Outline => "O", + Push => "P", + } + } +} \ No newline at end of file diff --git a/src/extgstate.rs b/src/extgstate.rs deleted file mode 100644 index e69de29..0000000 diff --git a/src/graphics.rs b/src/graphics.rs index 7d28ac7..41eba66 100644 --- a/src/graphics.rs +++ b/src/graphics.rs @@ -1,8 +1,7 @@ use std::collections::HashSet; - use crate::units::{Mm, Pt}; - use crate::FontId; +use lopdf::Dictionary as LoDictionary; /// Fill path using nonzero winding number rule pub const OP_PATH_PAINT_FILL_NZ: &str = "f"; @@ -21,7 +20,7 @@ pub const OP_PATH_CONST_CLIP_NZ: &str = "W"; /// Current path is a clip path, non-zero winding order pub const OP_PATH_CONST_CLIP_EO: &str = "W*"; -/// Rectangle struct (x, y, width, height) +/// Rectangle struct (x, y, width, height) from the LOWER LEFT corner of the page #[derive(Debug, PartialEq, Clone)] pub struct Rect { pub x: Pt, @@ -31,6 +30,21 @@ pub struct Rect { } impl Rect { + + pub fn lower_left(&self) -> Point { + Point { + x: self.x, + y: self.y, + } + } + + pub fn upper_right(&self) -> Point { + Point { + x: self.x + self.width, + y: self.y + self.height, + } + } + pub fn from_wh(width: Pt, height: Pt) -> Self { Self { x: Pt(0.0), @@ -579,6 +593,154 @@ pub struct ExtendedGraphicsState { pub(crate) text_knockout: bool, } +pub fn extgstate_to_dict(val: &ExtendedGraphicsState) -> LoDictionary { + + use lopdf::Object::*; + use lopdf::Object::String as LoString; + use std::string::String; + + let mut gs_operations = Vec::<(String, lopdf::Object)>::new(); + + // for each field, look if it was contained in the "changed fields" + if val.changed_fields.contains(LINE_WIDTH) { + gs_operations.push(("LW".to_string(), Real(val.line_width))); + } + + if val.changed_fields.contains(LINE_CAP) { + gs_operations.push(("LC".to_string(), Integer(val.line_cap.id()))); + } + + if val.changed_fields.contains(LINE_JOIN) { + gs_operations.push(("LJ".to_string(), Integer(val.line_join.id()))); + } + + if val.changed_fields.contains(MITER_LIMIT) { + gs_operations.push(("ML".to_string(), Real(val.miter_limit))); + } + + if val.changed_fields.contains(FLATNESS_TOLERANCE) { + gs_operations.push(("FL".to_string(), Real(val.flatness_tolerance))); + } + + if val.changed_fields.contains(RENDERING_INTENT) { + gs_operations.push(("RI".to_string(), Name(val.rendering_intent.get_id().into()))); + } + + if val.changed_fields.contains(STROKE_ADJUSTMENT) { + gs_operations.push(("SA".to_string(), Boolean(val.stroke_adjustment))); + } + + if val.changed_fields.contains(OVERPRINT_FILL) { + gs_operations.push(("OP".to_string(), Boolean(val.overprint_fill))); + } + + if val.changed_fields.contains(OVERPRINT_STROKE) { + gs_operations.push(("op".to_string(), Boolean(val.overprint_stroke))); + } + + if val.changed_fields.contains(OVERPRINT_MODE) { + gs_operations.push(("OPM".to_string(), Integer(val.overprint_mode.get_id()))); + } + + if val.changed_fields.contains(CURRENT_FILL_ALPHA) { + gs_operations.push(("CA".to_string(), Real(val.current_fill_alpha))); + } + + if val.changed_fields.contains(CURRENT_STROKE_ALPHA) { + gs_operations.push(("ca".to_string(), Real(val.current_stroke_alpha))); + } + + if val.changed_fields.contains(BLEND_MODE) { + gs_operations.push(("BM".to_string(), Name(val.blend_mode.get_id().into()))); + } + + if val.changed_fields.contains(ALPHA_IS_SHAPE) { + gs_operations.push(("AIS".to_string(), Boolean(val.alpha_is_shape))); + } + + if val.changed_fields.contains(TEXT_KNOCKOUT) { + gs_operations.push(("TK".to_string(), Boolean(val.text_knockout))); + } + + // set optional parameters + if let Some(ldp) = val.line_dash_pattern { + if val.changed_fields.contains(LINE_DASH_PATTERN) { + let array = ldp.as_array().into_iter().map(Integer).collect(); + gs_operations.push(("D".to_string(), Array(array))); + } + } + + if let Some(font) = val.font.as_ref() { + if val.changed_fields.contains(FONT) { + gs_operations.push(("Font".to_string(), Name(font.0.clone().into_bytes()))); + } + } + + // todo: transfer functions, halftone functions, + // black generation, undercolor removal + // these types cannot yet be converted into lopdf::Objects, + // need to implement Into for them + + if val.changed_fields.contains(BLACK_GENERATION) { + if let Some(ref black_generation) = val.black_generation { + // TODO + } + } + + if val.changed_fields.contains(BLACK_GENERATION_EXTRA) { + if let Some(ref black_generation_extra) = val.black_generation_extra { + // TODO + } + } + + if val.changed_fields.contains(UNDERCOLOR_REMOVAL) { + if let Some(ref under_color_removal) = val.under_color_removal { + // TODO + } + } + + if val.changed_fields.contains(UNDERCOLOR_REMOVAL_EXTRA) { + if let Some(ref under_color_removal_extra) = val.under_color_removal_extra { + // TODO + } + } + + if val.changed_fields.contains(TRANSFER_FUNCTION) { + if let Some(ref transfer_function) = val.transfer_function { + // TODO + } + } + + if val.changed_fields.contains(TRANSFER_FUNCTION_EXTRA) { + if let Some(ref transfer_extra_function) = val.transfer_extra_function { + // TODO + } + } + + if val.changed_fields.contains(HALFTONE_DICTIONARY) { + if let Some(ref halftone_dictionary) = val.halftone_dictionary { + // TODO + } + } + + if val.changed_fields.contains(SOFT_MASK) { + if let Some(ref soft_mask) = val.soft_mask { + + } else { + gs_operations.push(("SM".to_string(), Name("None".as_bytes().to_vec()))); + } + } + + // if there are operations, push the "Type > ExtGState" + // otherwise, just return an empty dictionary + if !gs_operations.is_empty() { + gs_operations.push(("Type".to_string(), "ExtGState".into())); + } + + LoDictionary::from_iter(gs_operations) +} + + #[derive(Debug, Clone, Default)] pub struct ExtendedGraphicsStateBuilder { /// Private field so we can control the `changed_fields` parameter @@ -862,6 +1024,15 @@ pub enum OverprintMode { KeepUnderlying, /* 1 */ } +impl OverprintMode { + pub fn get_id(&self) -> i64 { + match self { + OverprintMode::EraseUnderlying => 0, + OverprintMode::KeepUnderlying => 1, + } + } +} + /// Black generation calculates the amount of black to be used when trying to /// reproduce a particular color. #[derive(Debug, PartialEq, Copy, Clone)] @@ -1064,6 +1235,35 @@ impl BlendMode { pub fn saturation() -> BlendMode { BlendMode::NonSeperable(NonSeperableBlendMode::Saturation) } pub fn color() -> BlendMode { BlendMode::NonSeperable(NonSeperableBlendMode::Color) } pub fn luminosity() -> BlendMode { BlendMode::NonSeperable(NonSeperableBlendMode::Luminosity) } + pub fn get_id(&self) -> &'static str { + + use self::BlendMode::*; + use self::NonSeperableBlendMode::*; + use self::SeperableBlendMode::*; + + match self { + Seperable(s) => match s { + Normal => "Normal", + Multiply => "Multiply", + Screen => "Screen", + Overlay => "Overlay", + Darken => "Darken", + Lighten => "Lighten", + ColorDodge => "ColorDodge", + ColorBurn => "ColorBurn", + HardLight => "HardLight", + SoftLight => "SoftLight", + Difference => "Difference", + Exclusion => "Exclusion", + }, + NonSeperable(n) => match n { + Hue => "Hue", + Saturation => "Saturation", + Color => "Color", + Luminosity => "Luminosity", + }, + } + } } /// PDF Reference 1.7, Page 520, Table 7.2 @@ -1329,6 +1529,18 @@ pub enum RenderingIntent { Perceptual, } +impl RenderingIntent { + pub fn get_id(&self) -> &'static str { + use self::RenderingIntent::*; + match self { + AbsoluteColorimetric => "AbsoluteColorimetric", + RelativeColorimetric => "RelativeColorimetric", + Saturation => "Saturation", + Perceptual => "Perceptual", + } + } +} + /// A soft mask is used for transparent images such as PNG with an alpha component /// The bytes range from 0xFF (opaque) to 0x00 (transparent). The alpha channel of a /// PNG image have to be sorted out. diff --git a/src/lib.rs b/src/lib.rs index bf4e2d2..3b17603 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -61,14 +61,6 @@ impl PageAnnotId { pub fn new() -> Self { Self(crate::utils::random_character_string_32()) } } -/// Internal ID for link annotations -#[derive(Debug, PartialEq, Clone, Eq, PartialOrd, Ord)] -pub struct LinkAnnotId(pub String); - -impl LinkAnnotId { - pub fn new() -> Self { Self(crate::utils::random_character_string_32()) } -} - /// Internal ID for XObjects #[derive(Debug, PartialEq, Clone, Eq, PartialOrd, Ord)] pub struct XObjectId(pub String); @@ -175,13 +167,6 @@ impl PdfDocument { id } - // Adds a link (hyperlink or self-referential link) to the document resources, returning the links internal ID - pub fn add_link(&mut self, link: LinkAnnotation) -> LinkAnnotId { - let id = LinkAnnotId::new(); - self.resources.links.map.insert(id.clone(), link); - id - } - /// Adds a new page-level bookmark on page `$page`, returning the bookmarks internal ID pub fn add_bookmark(&mut self, name: &str, page: usize) -> PageAnnotId { let id = PageAnnotId::new(); @@ -218,8 +203,6 @@ pub struct PdfResources { pub fonts: PdfFontMap, /// XObjects (forms, images, embedded PDF contents, etc.) pub xobjects: XObjectMap, - /// Annotations for links between rects on pages - pub links: LinkAnnotMap, /// Map of explicit extended graphics states pub extgstates: ExtendedGraphicsStateMap, /// Map of optional content groups @@ -251,11 +234,6 @@ pub struct PageAnnotMap { pub map: BTreeMap, } -#[derive(Debug, PartialEq, Default, Clone)] -pub struct LinkAnnotMap { - pub map: BTreeMap, -} - #[derive(Debug, PartialEq, Default, Clone)] pub struct ExtendedGraphicsStateMap { pub map: BTreeMap, diff --git a/src/ops.rs b/src/ops.rs index a1481b6..bf3895b 100644 --- a/src/ops.rs +++ b/src/ops.rs @@ -1,4 +1,4 @@ -use crate::{color::Color, graphics::{Line, LineCapStyle, LineDashPattern, LineJoinStyle, Point, Polygon, Rect, TextRenderingMode}, matrix::{CurTransMat, TextMatrix}, units::{Mm, Pt}, BuiltinFont, ExtendedGraphicsStateId, FontId, LayerInternalId, LinkAnnotId, XObjectId, XObjectTransform}; +use crate::{color::Color, graphics::{Line, LineCapStyle, LineDashPattern, LineJoinStyle, Point, Polygon, Rect, TextRenderingMode}, matrix::{CurTransMat, TextMatrix}, units::{Mm, Pt}, BuiltinFont, ExtendedGraphicsStateId, FontId, LayerInternalId, LinkAnnotation, XObjectId, XObjectTransform}; use lopdf::Object as LoObject; #[derive(Debug, PartialEq, Clone)] @@ -138,7 +138,7 @@ pub enum Op { /// Sets a matrix that only affects subsequent text objects. SetTextMatrix { matrix: TextMatrix }, /// Adds a link annotation (use `PdfDocument::add_link` to register the `LinkAnnotation` on the document) - LinkAnnotation { link: LinkAnnotId }, + LinkAnnotation { link: LinkAnnotation }, /// Instantiates an XObject with a given transform (if the XObject has a width / height). /// Use `PdfDocument::add_xobject` to register the object and get the ID. UseXObject { id: XObjectId, transform: XObjectTransform }, diff --git a/src/serialize.rs b/src/serialize.rs index 97b9227..ed396be 100644 --- a/src/serialize.rs +++ b/src/serialize.rs @@ -1,11 +1,16 @@ use std::collections::BTreeMap; use std::collections::BTreeSet; +use crate::Actions; use crate::BuiltinFont; use crate::Color; +use crate::ColorArray; +use crate::DashPhase; +use crate::Destination; use crate::FontId; use crate::IccProfileType; use crate::Line; +use crate::LinkAnnotation; use crate::Op; use crate::PaintMode; use crate::ParsedFont; @@ -16,6 +21,8 @@ use crate::font::SubsetFont; use crate::PdfPage; use crate::PdfResources; use crate::Polygon; +use crate::XObject; +use crate::XObjectId; use lopdf::Dictionary as LoDictionary; use lopdf::Object::{Name, Integer, Null, Dictionary, Array, Real, Stream, Reference, String as LoString}; use lopdf::StringFormat::{Hexadecimal, Literal}; @@ -172,17 +179,33 @@ pub fn serialize_pdf_into_bytes(pdf: &PdfDocument, opts: &PdfSaveOptions) -> Vec } let global_xobject_dict_id = doc.add_object(global_xobject_dict); + let mut global_extgstate_dict = LoDictionary::new(); + for (k, v) in pdf.resources.extgstates.map.iter() { + global_extgstate_dict.set(k.0.clone(), crate::graphics::extgstate_to_dict(v)); + } + let global_extgstate_dict_id = doc.add_object(global_extgstate_dict); + + let page_ids_reserved = pdf.pages.iter().map(|_| doc.new_object_id()).collect::>(); + // Render pages - let page_ids = pdf.pages.iter().map(|page| { + let page_ids = pdf.pages.iter().zip(page_ids_reserved.iter()).map(|(page, page_id)| { + // gather page annotations let mut page_resources = LoDictionary::new(); // get_page_resources(&mut doc, &page); + let links = page.ops.iter().filter_map(|l| match l { + Op::LinkAnnotation { link } => Some(link.clone()), + _ => None, + }).collect::>(); + page_resources.set("Annots", Array( + links.iter().map(|l| Dictionary(link_annotation_to_dict(l, &page_ids_reserved))).collect() + )); + page_resources.set("Font", Reference(global_font_dict_id)); page_resources.set("XObject", Reference(global_xobject_dict_id)); - // page_resources.set("Annots"); - // page_resources.set("ExtGState"); + page_resources.set("ExtGState", Reference(global_extgstate_dict_id)); // page_resources.et("Properties", Dictionary(ocg_dict)); - let layer_stream = translate_operations(&page.ops, &prepared_fonts); // Vec + let layer_stream = translate_operations(&page.ops, &prepared_fonts, &pdf.resources.xobjects.map); // Vec let merged_layer_stream = LoStream::new(LoDictionary::new(), layer_stream) .with_compression(false); @@ -197,7 +220,9 @@ pub fn serialize_pdf_into_bytes(pdf: &PdfDocument, opts: &PdfSaveOptions) -> Vec ("Contents", Reference(doc.add_object(merged_layer_stream))), ]); - doc.add_object(page_obj) + doc.set_object(*page_id, page_obj); + + *page_id }).collect::>(); // Now that the page objs are rendered, resolve which bookmarks reference which page objs @@ -305,7 +330,11 @@ fn builtin_font_to_dict(font: &BuiltinFont) -> LoDictionary { ]) } -fn translate_operations(ops: &[Op], fonts: &BTreeMap) -> Vec { +fn translate_operations( + ops: &[Op], + fonts: &BTreeMap, + xobjects: &BTreeMap, +) -> Vec { let mut content = Vec::new(); @@ -479,8 +508,15 @@ fn translate_operations(ops: &[Op], fonts: &BTreeMap) -> V // TODO! }, Op::UseXObject { id, transform } => { + + use crate::matrix::CurTransMat; + let mut t = CurTransMat::Identity; + for q in transform.get_ctms(xobjects.get(id).and_then(|xobj| xobj.get_width_height())) { + t = CurTransMat::Raw(CurTransMat::combine_matrix(t.as_array(), q.as_array())); + } + content.push(LoOp::new("q", vec![])); - // self.add_operation(transform); + content.push(LoOp::new("cm", t.as_array().into_iter().map(Real).collect())); content.push(LoOp::new("Do", vec![Name(id.0.as_bytes().to_vec())])); content.push(LoOp::new("Q", vec![])); }, @@ -896,3 +932,63 @@ fn icc_to_stream(val: &IccProfile) -> LoStream { LoStream::new(stream_dict, val.icc.clone()) } + +fn link_annotation_to_dict(la: &LinkAnnotation, page_ids: &[lopdf::ObjectId]) -> LoDictionary { + + let ll = la.rect.lower_left(); + let ur = la.rect.upper_right(); + + let mut dict: LoDictionary = LoDictionary::new(); + dict.set("Type", Name("Annot".into())); + dict.set("Subtype", Name("Link".into())); + dict.set("Rect", Array(vec![Real(ll.x.0), Real(ll.y.0), Real(ur.x.0), Real(ur.y.0)])); + dict.set("A", Dictionary(actions_to_dict(&la.actions, page_ids))); + dict.set("Border", Array(la.border.to_array().into_iter().map(Real).collect())); + dict.set("C", Array(color_array_to_f32(&la.color).into_iter().map(Real).collect())); + dict.set("H", Name(la.highlighting.get_id().into())); + dict +} + +fn dashphase_to_object(dp: &DashPhase) -> Vec { + vec![ + Array(dp.dash_array.iter().map(|x| Real(*x)).collect()), + Real(dp.phase), + ] +} + +fn actions_to_dict(a: &Actions, page_ids: &[lopdf::ObjectId]) -> LoDictionary { + let mut dict = LoDictionary::new(); + dict.set("S", Name(a.get_action_type_id().into())); + match a { + Actions::GoTo(destination) => { + dict.set("D", destination_to_obj(destination, page_ids)); + }, + Actions::URI(uri) => { + dict.set("URI", LoString(uri.clone().into_bytes(), Literal)); + }, + } + dict +} + +fn destination_to_obj(d: &Destination, page_ids: &[lopdf::ObjectId]) -> lopdf::Object { + match d { + Destination::XYZ { page, left, top, zoom } => { + Array(vec![ + page_ids.get(page.saturating_sub(1)).copied().map(Reference).unwrap_or(Null), + Name("XYZ".into()), + left.map(Real).unwrap_or(Null), + top.map(Real).unwrap_or(Null), + zoom.map(Real).unwrap_or(Null), + ]) + }, + } +} + +fn color_array_to_f32(c: &ColorArray) -> Vec { + match c { + ColorArray::Transparent => Vec::new(), + ColorArray::Gray(arr) => arr.to_vec(), + ColorArray::RGB(arr) => arr.to_vec(), + ColorArray::CMYK(arr) => arr.to_vec(), + } +} diff --git a/src/xobject.rs b/src/xobject.rs index 2b0173f..ec70587 100644 --- a/src/xobject.rs +++ b/src/xobject.rs @@ -11,7 +11,7 @@ pub enum XObject { Image(RawImage), /// Form XObject, NOT A PDF FORM, this just allows repeatable content /// on a page - Form(Box), + Form(FormXObject), /// XObject embedded from an external stream /// /// This is mainly used to add XObjects to the resources that the library @@ -24,6 +24,19 @@ pub enum XObject { External(ExternalXObject), } +impl XObject { + pub fn get_width_height(&self) -> Option<(Px, Px)> { + match self { + XObject::Image(raw_image) => Some((Px(raw_image.width), Px(raw_image.height))), + XObject::Form(form_xobject) => form_xobject.size.clone(), + XObject::External(external_xobject) => Some(( + external_xobject.width.clone()?, + external_xobject.height.clone()? + )), + } + } +} + // translates the xobject to a document object ID pub(crate) fn add_xobject_to_document(xobj: &XObject, doc: &mut lopdf::Document) -> lopdf::ObjectId { @@ -85,10 +98,11 @@ pub enum ImageFilter { pub struct FormXObject { /* /Type /XObject */ /* /Subtype /Form */ - /* /FormType Integer */ /// Form type (currently only Type1) pub form_type: FormType, + /// Optional width / height, affects the width / height on instantiation + pub size: Option<(Px, Px)>, /// The actual content of this FormXObject pub bytes: Vec, /* /Matrix [Integer , 6] */ @@ -314,9 +328,52 @@ pub struct XObjectTransform { pub dpi: Option, } +impl XObjectTransform { + pub fn get_ctms(&self, wh: Option<(Px, Px)>) -> Vec { + + let mut transforms = Vec::new(); + let dpi = self.dpi.unwrap_or(300.0); + + if let Some((w, h)) = wh { + // PDF maps an image to a 1x1 square, we have to + // adjust the transform matrix to fix the distortion + + // Image at the given dpi should 1px = 1pt + transforms.push(CurTransMat::Scale(w.into_pt(dpi).0, h.into_pt(dpi).0)); + } + + if self.scale_x.is_some() || self.scale_y.is_some() { + let scale_x = self.scale_x.unwrap_or(1.0); + let scale_y = self.scale_y.unwrap_or(1.0); + transforms.push(CurTransMat::Scale(scale_x, scale_y)); + } + + if let Some(rotate) = self.rotate.as_ref() { + transforms.push(CurTransMat::Translate( + Pt(-rotate.rotation_center_x.into_pt(dpi).0), + Pt(-rotate.rotation_center_y.into_pt(dpi).0), + )); + transforms.push(CurTransMat::Rotate(rotate.angle_ccw_degrees)); + transforms.push(CurTransMat::Translate( + rotate.rotation_center_x.into_pt(dpi), + rotate.rotation_center_y.into_pt(dpi), + )); + } + + if self.translate_x.is_some() || self.translate_y.is_some() { + transforms.push(CurTransMat::Translate( + self.translate_x.unwrap_or(Pt(0.0)), + self.translate_y.unwrap_or(Pt(0.0)), + )); + } + + transforms + } +} + #[derive(Debug, Copy, Clone, PartialEq, Default)] pub struct XObjectRotation { pub angle_ccw_degrees: f32, - pub rotation_center_x: Pt, - pub rotation_center_y: Pt, + pub rotation_center_x: Px, + pub rotation_center_y: Px, }