diff --git a/src/document.rs b/src/document.rs index b91f5ac3..b5eb6ce5 100644 --- a/src/document.rs +++ b/src/document.rs @@ -1,3 +1,4 @@ +use crate::object::outline::Outline; use crate::object::page::PageLabel; use crate::serialize::{SerializeSettings, SerializerContext}; use crate::surface::PageBuilder; @@ -26,6 +27,10 @@ impl Document { PageBuilder::new_with(&mut self.serializer_context, size, page_label) } + pub fn set_outline(&mut self, outline: Outline) { + self.serializer_context.set_outline(outline); + } + pub fn finish(self) -> Vec { self.serializer_context.finish().finish() } diff --git a/src/object/ext_g_state.rs b/src/object/ext_g_state.rs index 27dbb6be..85100333 100644 --- a/src/object/ext_g_state.rs +++ b/src/object/ext_g_state.rs @@ -119,7 +119,6 @@ impl Object for ExtGState { ext_st.blend_mode(bm); } - if let Some(mask_ref) = mask_ref { ext_st.pair(Name(b"SMask"), mask_ref); } diff --git a/src/object/outline.rs b/src/object/outline.rs index cde55cbf..66ad938d 100644 --- a/src/object/outline.rs +++ b/src/object/outline.rs @@ -1,8 +1,8 @@ -use pdf_writer::{Chunk, Ref}; -use tiny_skia_path::Point; use crate::serialize::{Object, SerializerContext}; +use pdf_writer::{Chunk, Finish, Ref, TextStr}; +use tiny_skia_path::{Point, Transform}; -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Outline { children: Vec, } @@ -17,8 +17,8 @@ impl Outline { } } -#[derive(Debug)] -struct OutlineNode { +#[derive(Debug, Clone)] +pub struct OutlineNode { children: Vec>, text: String, page_index: u32, @@ -39,11 +39,168 @@ impl OutlineNode { self.children.push(Box::new(node)) } + pub fn serialize_into( + &self, + sc: &mut SerializerContext, + parent: Ref, + root: Ref, + next: Option, + prev: Option, + ) -> Chunk { + let mut chunk = Chunk::new(); + let mut sub_chunks = vec![]; + + let mut outline_entry = chunk.outline_item(root); + outline_entry.parent(parent); + + if let Some(next) = next { + outline_entry.next(next); + } + + if let Some(prev) = prev { + outline_entry.prev(prev); + } + + if !self.children.is_empty() { + let first = sc.new_ref(); + let mut last = first; + + let mut prev = None; + let mut cur = Some(first); + + for i in 0..self.children.len() { + let next = if i < self.children.len() - 1 { + Some(sc.new_ref()) + } else { + None + }; + + last = cur.unwrap(); + + sub_chunks.push(self.children[i].serialize_into(sc, root, last, next, prev)); + + prev = cur; + cur = next; + } + + outline_entry.first(first); + outline_entry.last(last); + outline_entry.count(-i32::try_from(self.children.len()).unwrap()); + } + + if !self.text.is_empty() { + outline_entry.title(TextStr(&self.text)); + } + + let page_ref = sc.page_infos()[self.page_index as usize].ref_; + let page_size = sc.page_infos()[self.page_index as usize].media_box.height(); + let mut mapped_point = self.pos; + let invert_transform = Transform::from_row(1.0, 0.0, 0.0, -1.0, 0.0, page_size); + invert_transform.map_point(&mut mapped_point); + + outline_entry + .dest() + .page(page_ref) + .xyz(mapped_point.x, mapped_point.y, None); + + outline_entry.finish(); + + for sub_chunk in sub_chunks { + chunk.extend(&sub_chunk); + } + + chunk + } } impl Object for Outline { fn serialize_into(self, sc: &mut SerializerContext, root_ref: Ref) -> Chunk { - todo!() + let mut chunk = Chunk::new(); + + let mut sub_chunks = vec![]; + + let mut outline = chunk.outline(root_ref); + + if !self.children.is_empty() { + let first = sc.new_ref(); + let mut last = first; + + let mut prev = None; + let mut cur = Some(first); + + for i in 0..self.children.len() { + let next = if i < self.children.len() - 1 { + Some(sc.new_ref()) + } else { + None + }; + + last = cur.unwrap(); + + sub_chunks.push(self.children[i].serialize_into(sc, root_ref, last, next, prev)); + + prev = cur; + cur = next; + } + + outline.first(first); + outline.last(last); + outline.count(i32::try_from(self.children.len()).unwrap()); + } + + outline.finish(); + + for sub_chunk in sub_chunks { + chunk.extend(&sub_chunk); + } + + chunk + } +} + +#[cfg(test)] +mod tests { + use crate::document::Document; + use crate::object::outline::{Outline, OutlineNode}; + use crate::rgb::Rgb; + use crate::serialize::SerializeSettings; + use crate::test_utils::check_snapshot; + use crate::Fill; + use tiny_skia_path::{PathBuilder, Point, Rect, Size}; + + #[test] + fn simple() { + let mut builder = PathBuilder::new(); + builder.push_rect(Rect::from_xywh(50.0, 50.0, 100.0, 100.0).unwrap()); + let path = builder.finish().unwrap(); + + let mut db = Document::new(SerializeSettings::default_test()); + let mut page = db.start_page(Size::from_wh(200.0, 200.0).unwrap()); + let mut surface = page.surface(); + surface.fill_path(&path, Fill::::default()); + surface.finish(); + page.finish(); + + db.start_page(Size::from_wh(200.0, 500.0).unwrap()); + db.start_page(Size::from_wh(250.0, 700.0).unwrap()); + + let mut outline = Outline::new(); + + let mut child1 = OutlineNode::new("Level 1".to_string(), 0, Point::from_xy(50.0, 50.0)); + child1.push_child(OutlineNode::new( + "Level 2".to_string(), + 0, + Point::from_xy(50.0, 150.0), + )); + + let child2 = OutlineNode::new("Level 1 try 2".to_string(), 1, Point::from_xy(75.0, 150.0)); + + outline.push_child(child1); + outline.push_child(child2); + + db.set_outline(outline); + + check_snapshot("outline/simple", &db.finish()); } -} \ No newline at end of file +} diff --git a/src/object/page.rs b/src/object/page.rs index 9bf5bacc..af6066d8 100644 --- a/src/object/page.rs +++ b/src/object/page.rs @@ -250,7 +250,6 @@ mod tests { check_snapshot("page/page_label", sc.finish().as_bytes()); } - // TODO: Fix issues with not being able to create empty pages with just start_page_with. // TODO: Fix issue with two duplicate pages not showing up. #[test] diff --git a/src/serialize.rs b/src/serialize.rs index a9a6f39e..b1fba496 100644 --- a/src/serialize.rs +++ b/src/serialize.rs @@ -3,6 +3,7 @@ use crate::object::cid_font::CIDFont; use crate::object::color_space::luma::SGray; use crate::object::color_space::rgb::Srgb; use crate::object::color_space::{DEVICE_GRAY, DEVICE_RGB}; +use crate::object::outline::Outline; use crate::object::page::{PageLabel, PageLabelContainer}; use crate::object::type3_font::Type3Font; use crate::resource::{ColorSpaceEnum, FontResource}; @@ -17,7 +18,6 @@ use std::collections::HashMap; use std::hash::Hash; use std::sync::Arc; use tiny_skia_path::Rect; -use crate::object::outline::Outline; #[derive(Copy, Clone, Debug)] pub struct SvgSettings { @@ -83,6 +83,7 @@ pub struct SerializerContext { catalog_ref: Ref, page_tree_ref: Ref, page_labels_ref: Option, + outline_ref: Option, page_infos: Vec, outline: Option, cached_mappings: HashMap, @@ -126,6 +127,7 @@ impl SerializerContext { page_tree_ref, catalog_ref, outline: None, + outline_ref: None, page_labels_ref: None, page_infos: vec![], chunks_len: 0, @@ -134,6 +136,10 @@ impl SerializerContext { } } + pub fn page_infos(&self) -> &[PageInfo] { + &self.page_infos + } + pub fn set_outline(&mut self, outline: Outline) { self.outline = Some(outline); } @@ -299,6 +305,13 @@ impl SerializerContext { self.page_labels_ref = Some(self.add(container)); } + if let Some(outline) = self.outline.clone() { + let outline_ref = self.new_ref(); + self.outline_ref = Some(outline_ref); + let chunk = outline.serialize_into(&mut self, outline_ref); + self.push_chunk(chunk); + } + // Write fonts // TODO: Make more efficient let fonts = std::mem::take(&mut self.font_map); @@ -341,6 +354,10 @@ impl SerializerContext { catalog.pair(Name(b"PageLabels"), plr); } + if let Some(olr) = self.outline_ref { + catalog.outlines(olr); + } + catalog.finish(); pdf.extend(&page_tree_chunk); diff --git a/src/surface.rs b/src/surface.rs index 9587eac6..d9787d79 100644 --- a/src/surface.rs +++ b/src/surface.rs @@ -218,6 +218,10 @@ impl<'a> PageBuilder<'a> { } } + pub(crate) fn root_transform(&self) -> Transform { + Transform::from_row(1.0, 0.0, 0.0, -1.0, 0.0, self.size.height()) + } + pub(crate) fn new_with( sc: &'a mut SerializerContext, size: Size, @@ -234,14 +238,7 @@ impl<'a> PageBuilder<'a> { pub fn surface(&mut self) -> Surface { let mut root_builder = ContentBuilder::new(); // Invert the y-axis. - root_builder.concat_transform(&Transform::from_row( - 1.0, - 0.0, - 0.0, - -1.0, - 0.0, - self.size.height(), - )); + root_builder.concat_transform(&self.root_transform()); let finish_fn = Box::new(|stream| self.page_stream = stream);