Skip to content

Commit

Permalink
Set outlines of PDF (#91)
Browse files Browse the repository at this point in the history
* feat(outlines): delete outlines

* feat(destination): define destination

* feat(outlines): set outlines

* transform fitz point to pdf destination point
  • Loading branch information
TD-Sky authored Jul 28, 2024
1 parent 9e74911 commit 69aded2
Show file tree
Hide file tree
Showing 6 changed files with 270 additions and 4 deletions.
14 changes: 14 additions & 0 deletions mupdf-sys/wrapper.c
Original file line number Diff line number Diff line change
Expand Up @@ -3070,6 +3070,20 @@ fz_matrix mupdf_pdf_page_transform(fz_context *ctx, pdf_page *page, mupdf_error_
return ctm;
}

fz_matrix mupdf_pdf_page_obj_transform(fz_context *ctx, pdf_obj *page, mupdf_error_t **errptr)
{
fz_matrix ctm = fz_identity;
fz_try(ctx)
{
pdf_page_obj_transform(ctx, page, NULL, &ctm);
}
fz_catch(ctx)
{
mupdf_save_error(ctx, errptr);
}
return ctm;
}

/* PDFAnnotation */
int mupdf_pdf_annot_type(fz_context *ctx, pdf_annot *annot, mupdf_error_t **errptr)
{
Expand Down
110 changes: 110 additions & 0 deletions src/destination.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
use crate::pdf::PdfObject;
use crate::Error;

#[derive(Debug, Clone)]
pub struct Destination {
/// Indirect reference to page object.
page: PdfObject,
kind: DestinationKind,
}

#[derive(Debug, Clone, PartialEq)]
pub enum DestinationKind {
/// Display the page at a scale which just fits the whole page
/// in the window both horizontally and vertically.
Fit,
/// Display the page with the vertical coordinate `top` at the top edge of the window,
/// and the magnification set to fit the document horizontally.
FitH { top: f32 },
/// Display the page with the horizontal coordinate `left` at the left edge of the window,
/// and the magnification set to fit the document vertically.
FitV { left: f32 },
/// Display the page with (`left`, `top`) at the upper-left corner
/// of the window and the page magnified by factor `zoom`.
XYZ {
left: Option<f32>,
top: Option<f32>,
zoom: Option<f32>,
},
/// Display the page zoomed to show the rectangle specified by `left`, `bottom`, `right`, and `top`.
FitR {
left: f32,
bottom: f32,
right: f32,
top: f32,
},
/// Display the page like `/Fit`, but use the bounding box of the page’s contents,
/// rather than the crop box.
FitB,
/// Display the page like `/FitH`, but use the bounding box of the page’s contents,
/// rather than the crop box.
FitBH { top: f32 },
/// Display the page like `/FitV`, but use the bounding box of the page’s contents,
/// rather than the crop box.
FitBV { left: f32 },
}

impl Destination {
pub(crate) fn new(page: PdfObject, kind: DestinationKind) -> Self {
Self { page, kind }
}

/// Encode destination into a PDF array.
pub(crate) fn encode_into(self, array: &mut PdfObject) -> Result<(), Error> {
debug_assert_eq!(array.len()?, 0);

array.array_push(self.page)?;
match self.kind {
DestinationKind::Fit => array.array_push(PdfObject::new_name("Fit")?)?,
DestinationKind::FitH { top } => {
array.array_push(PdfObject::new_name("FitH")?)?;
array.array_push(PdfObject::new_real(top)?)?;
}
DestinationKind::FitV { left } => {
array.array_push(PdfObject::new_name("FitV")?)?;
array.array_push(PdfObject::new_real(left)?)?;
}
DestinationKind::XYZ { left, top, zoom } => {
array.array_push(PdfObject::new_name("XYZ")?)?;
array.array_push(
left.map(PdfObject::new_real)
.transpose()?
.unwrap_or(PdfObject::new_null()),
)?;
array.array_push(
top.map(PdfObject::new_real)
.transpose()?
.unwrap_or(PdfObject::new_null()),
)?;
array.array_push(
zoom.map(PdfObject::new_real)
.transpose()?
.unwrap_or(PdfObject::new_null()),
)?;
}
DestinationKind::FitR {
left,
bottom,
right,
top,
} => {
array.array_push(PdfObject::new_name("FitR")?)?;
array.array_push(PdfObject::new_real(left)?)?;
array.array_push(PdfObject::new_real(bottom)?)?;
array.array_push(PdfObject::new_real(right)?)?;
array.array_push(PdfObject::new_real(top)?)?;
}
DestinationKind::FitB => array.array_push(PdfObject::new_name("FitB")?)?,
DestinationKind::FitBH { top } => {
array.array_push(PdfObject::new_name("FitBH")?)?;
array.array_push(PdfObject::new_real(top)?)?;
}
DestinationKind::FitBV { left } => {
array.array_push(PdfObject::new_name("FitBV")?)?;
array.array_push(PdfObject::new_real(left)?)?;
}
}

Ok(())
}
}
3 changes: 3 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ pub mod colorspace;
pub mod context;
/// Provide two-way communication between application and library
pub mod cookie;
/// Destination
pub mod destination;
/// Device interface
pub mod device;
/// A way of packaging up a stream of graphical operations
Expand Down Expand Up @@ -72,6 +74,7 @@ pub use colorspace::Colorspace;
pub(crate) use context::context;
pub use context::Context;
pub use cookie::Cookie;
pub use destination::{Destination, DestinationKind};
pub use device::{BlendMode, Device};
pub use display_list::DisplayList;
pub use document::{Document, MetadataName};
Expand Down
112 changes: 110 additions & 2 deletions src/pdf/document.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ use num_enum::TryFromPrimitive;

use crate::pdf::{PdfGraftMap, PdfObject, PdfPage};
use crate::{
context, Buffer, CjkFontOrdering, Document, Error, Font, Image, SimpleFontEncoding, Size,
WriteMode,
context, Buffer, CjkFontOrdering, Destination, DestinationKind, Document, Error, Font, Image,
Outline, Point, SimpleFontEncoding, Size, WriteMode,
};

bitflags! {
Expand Down Expand Up @@ -572,6 +572,114 @@ impl PdfDocument {
}
Ok(())
}

pub fn set_outlines(&mut self, toc: &[Outline]) -> Result<(), Error> {
self.delete_outlines()?;

if !toc.is_empty() {
let mut outlines = self.new_dict()?;
outlines.dict_put("Type", PdfObject::new_name("Outlines")?)?;
// Now we access outlines indirectly
let mut outlines = self.add_object(&outlines)?;
self.walk_outlines_insert(toc, &mut outlines)?;
self.catalog()?.dict_put("Outlines", outlines)?;
}

Ok(())
}

fn walk_outlines_insert(
&mut self,
down: &[Outline],
parent: &mut PdfObject,
) -> Result<(), Error> {
debug_assert!(!down.is_empty() && parent.is_indirect()?);

// All the indirect references in the current level.
let mut refs = Vec::new();

for outline in down {
let mut item = self.new_dict()?;
item.dict_put("Title", PdfObject::new_string(&outline.title)?)?;
item.dict_put("Parent", parent.clone())?;
if let Some(dest) = outline
.page
.map(|page| {
let page = self.find_page(page as i32)?;

let matrix = page.page_ctm()?;
let fz_point = Point::new(outline.x, outline.y);
let Point { x, y } = fz_point.transform(&matrix);
let dest_kind = DestinationKind::XYZ {
left: Some(x),
top: Some(y),
zoom: None,
};
let dest = Destination::new(page, dest_kind);

let mut array = self.new_array()?;
dest.encode_into(&mut array)?;

Ok(array)
})
.or_else(|| outline.uri.as_deref().map(PdfObject::new_string))
.transpose()?
{
item.dict_put("Dest", dest)?;
}

refs.push(self.add_object(&item)?);
if !outline.down.is_empty() {
self.walk_outlines_insert(&outline.down, refs.last_mut().unwrap())?;
}
}

// NOTE: doing the same thing as mutation version of `slice::array_windows`
for i in 0..down.len().saturating_sub(1) {
let [prev, next, ..] = &mut refs[i..] else {
unreachable!();
};
prev.dict_put("Next", next.clone())?;
next.dict_put("Prev", prev.clone())?;
}

let mut refs = refs.into_iter();
let first = refs.next().unwrap();
let last = refs.last().unwrap_or_else(|| first.clone());

parent.dict_put("First", first)?;
parent.dict_put("Last", last)?;

Ok(())
}

/// Delete `/Outlines` in document catalog and all the **outline items** it points to.
///
/// Do nothing if document has no outlines.
pub fn delete_outlines(&mut self) -> Result<(), Error> {
if let Some(outlines) = self.catalog()?.get_dict("Outlines")? {
if let Some(outline) = outlines.get_dict("First")? {
self.walk_outlines_del(outline)?;
}
self.delete_object(outlines.as_indirect()?)?;
}

Ok(())
}

fn walk_outlines_del(&mut self, outline: PdfObject) -> Result<(), Error> {
let mut cur = Some(outline);

while let Some(item) = cur.take() {
if let Some(down) = item.get_dict("First")? {
self.walk_outlines_del(down)?;
}
cur = item.get_dict("Next")?;
self.delete_object(item.as_indirect()?)?;
}

Ok(())
}
}

impl Deref for PdfDocument {
Expand Down
9 changes: 8 additions & 1 deletion src/pdf/object.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use std::slice;
use mupdf_sys::*;

use crate::pdf::PdfDocument;
use crate::{context, Buffer, Error};
use crate::{context, Buffer, Error, Matrix};

pub trait IntoPdfDictKey {
fn into_pdf_dict_key(self) -> Result<PdfObject, Error>;
Expand Down Expand Up @@ -388,6 +388,13 @@ impl PdfObject {
Some(PdfDocument::from_raw(ptr))
}
}

pub fn page_ctm(&self) -> Result<Matrix, Error> {
let matrix =
unsafe { ffi_try!(mupdf_pdf_page_obj_transform(context(), self.inner)).into() };

Ok(matrix)
}
}

impl Write for PdfObject {
Expand Down
26 changes: 25 additions & 1 deletion src/point.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use mupdf_sys::fz_point;
use mupdf_sys::{fz_point, fz_transform_point};

use crate::Matrix;

/// A point in a two-dimensional space.
#[derive(Debug, Clone, Copy, PartialEq)]
Expand All @@ -11,6 +13,22 @@ impl Point {
pub const fn new(x: f32, y: f32) -> Self {
Self { x, y }
}

/// Apply a transformation to a point.
///
/// The NaN coordinates will be reset to 0.0,
/// which make `fz_transform_point` works normally.
/// Otherwise `(NaN, NaN)` will be returned.
pub fn transform(mut self, matrix: &Matrix) -> Self {
if self.x.is_nan() {
self.x = 0.0;
}
if self.y.is_nan() {
self.y = 0.0;
}

unsafe { fz_transform_point(self.into(), matrix.into()).into() }
}
}

impl From<fz_point> for Point {
Expand All @@ -19,6 +37,12 @@ impl From<fz_point> for Point {
}
}

impl From<Point> for fz_point {
fn from(p: Point) -> Self {
fz_point { x: p.x, y: p.y }
}
}

impl From<(f32, f32)> for Point {
fn from(p: (f32, f32)) -> Self {
Self { x: p.0, y: p.1 }
Expand Down

0 comments on commit 69aded2

Please sign in to comment.