Skip to content

Commit 69aded2

Browse files
authored
Set outlines of PDF (#91)
* feat(outlines): delete outlines * feat(destination): define destination * feat(outlines): set outlines * transform fitz point to pdf destination point
1 parent 9e74911 commit 69aded2

File tree

6 files changed

+270
-4
lines changed

6 files changed

+270
-4
lines changed

mupdf-sys/wrapper.c

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3070,6 +3070,20 @@ fz_matrix mupdf_pdf_page_transform(fz_context *ctx, pdf_page *page, mupdf_error_
30703070
return ctm;
30713071
}
30723072

3073+
fz_matrix mupdf_pdf_page_obj_transform(fz_context *ctx, pdf_obj *page, mupdf_error_t **errptr)
3074+
{
3075+
fz_matrix ctm = fz_identity;
3076+
fz_try(ctx)
3077+
{
3078+
pdf_page_obj_transform(ctx, page, NULL, &ctm);
3079+
}
3080+
fz_catch(ctx)
3081+
{
3082+
mupdf_save_error(ctx, errptr);
3083+
}
3084+
return ctm;
3085+
}
3086+
30733087
/* PDFAnnotation */
30743088
int mupdf_pdf_annot_type(fz_context *ctx, pdf_annot *annot, mupdf_error_t **errptr)
30753089
{

src/destination.rs

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
use crate::pdf::PdfObject;
2+
use crate::Error;
3+
4+
#[derive(Debug, Clone)]
5+
pub struct Destination {
6+
/// Indirect reference to page object.
7+
page: PdfObject,
8+
kind: DestinationKind,
9+
}
10+
11+
#[derive(Debug, Clone, PartialEq)]
12+
pub enum DestinationKind {
13+
/// Display the page at a scale which just fits the whole page
14+
/// in the window both horizontally and vertically.
15+
Fit,
16+
/// Display the page with the vertical coordinate `top` at the top edge of the window,
17+
/// and the magnification set to fit the document horizontally.
18+
FitH { top: f32 },
19+
/// Display the page with the horizontal coordinate `left` at the left edge of the window,
20+
/// and the magnification set to fit the document vertically.
21+
FitV { left: f32 },
22+
/// Display the page with (`left`, `top`) at the upper-left corner
23+
/// of the window and the page magnified by factor `zoom`.
24+
XYZ {
25+
left: Option<f32>,
26+
top: Option<f32>,
27+
zoom: Option<f32>,
28+
},
29+
/// Display the page zoomed to show the rectangle specified by `left`, `bottom`, `right`, and `top`.
30+
FitR {
31+
left: f32,
32+
bottom: f32,
33+
right: f32,
34+
top: f32,
35+
},
36+
/// Display the page like `/Fit`, but use the bounding box of the page’s contents,
37+
/// rather than the crop box.
38+
FitB,
39+
/// Display the page like `/FitH`, but use the bounding box of the page’s contents,
40+
/// rather than the crop box.
41+
FitBH { top: f32 },
42+
/// Display the page like `/FitV`, but use the bounding box of the page’s contents,
43+
/// rather than the crop box.
44+
FitBV { left: f32 },
45+
}
46+
47+
impl Destination {
48+
pub(crate) fn new(page: PdfObject, kind: DestinationKind) -> Self {
49+
Self { page, kind }
50+
}
51+
52+
/// Encode destination into a PDF array.
53+
pub(crate) fn encode_into(self, array: &mut PdfObject) -> Result<(), Error> {
54+
debug_assert_eq!(array.len()?, 0);
55+
56+
array.array_push(self.page)?;
57+
match self.kind {
58+
DestinationKind::Fit => array.array_push(PdfObject::new_name("Fit")?)?,
59+
DestinationKind::FitH { top } => {
60+
array.array_push(PdfObject::new_name("FitH")?)?;
61+
array.array_push(PdfObject::new_real(top)?)?;
62+
}
63+
DestinationKind::FitV { left } => {
64+
array.array_push(PdfObject::new_name("FitV")?)?;
65+
array.array_push(PdfObject::new_real(left)?)?;
66+
}
67+
DestinationKind::XYZ { left, top, zoom } => {
68+
array.array_push(PdfObject::new_name("XYZ")?)?;
69+
array.array_push(
70+
left.map(PdfObject::new_real)
71+
.transpose()?
72+
.unwrap_or(PdfObject::new_null()),
73+
)?;
74+
array.array_push(
75+
top.map(PdfObject::new_real)
76+
.transpose()?
77+
.unwrap_or(PdfObject::new_null()),
78+
)?;
79+
array.array_push(
80+
zoom.map(PdfObject::new_real)
81+
.transpose()?
82+
.unwrap_or(PdfObject::new_null()),
83+
)?;
84+
}
85+
DestinationKind::FitR {
86+
left,
87+
bottom,
88+
right,
89+
top,
90+
} => {
91+
array.array_push(PdfObject::new_name("FitR")?)?;
92+
array.array_push(PdfObject::new_real(left)?)?;
93+
array.array_push(PdfObject::new_real(bottom)?)?;
94+
array.array_push(PdfObject::new_real(right)?)?;
95+
array.array_push(PdfObject::new_real(top)?)?;
96+
}
97+
DestinationKind::FitB => array.array_push(PdfObject::new_name("FitB")?)?,
98+
DestinationKind::FitBH { top } => {
99+
array.array_push(PdfObject::new_name("FitBH")?)?;
100+
array.array_push(PdfObject::new_real(top)?)?;
101+
}
102+
DestinationKind::FitBV { left } => {
103+
array.array_push(PdfObject::new_name("FitBV")?)?;
104+
array.array_push(PdfObject::new_real(left)?)?;
105+
}
106+
}
107+
108+
Ok(())
109+
}
110+
}

src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ pub mod colorspace;
1414
pub mod context;
1515
/// Provide two-way communication between application and library
1616
pub mod cookie;
17+
/// Destination
18+
pub mod destination;
1719
/// Device interface
1820
pub mod device;
1921
/// A way of packaging up a stream of graphical operations
@@ -72,6 +74,7 @@ pub use colorspace::Colorspace;
7274
pub(crate) use context::context;
7375
pub use context::Context;
7476
pub use cookie::Cookie;
77+
pub use destination::{Destination, DestinationKind};
7578
pub use device::{BlendMode, Device};
7679
pub use display_list::DisplayList;
7780
pub use document::{Document, MetadataName};

src/pdf/document.rs

Lines changed: 110 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ use num_enum::TryFromPrimitive;
1010

1111
use crate::pdf::{PdfGraftMap, PdfObject, PdfPage};
1212
use crate::{
13-
context, Buffer, CjkFontOrdering, Document, Error, Font, Image, SimpleFontEncoding, Size,
14-
WriteMode,
13+
context, Buffer, CjkFontOrdering, Destination, DestinationKind, Document, Error, Font, Image,
14+
Outline, Point, SimpleFontEncoding, Size, WriteMode,
1515
};
1616

1717
bitflags! {
@@ -572,6 +572,114 @@ impl PdfDocument {
572572
}
573573
Ok(())
574574
}
575+
576+
pub fn set_outlines(&mut self, toc: &[Outline]) -> Result<(), Error> {
577+
self.delete_outlines()?;
578+
579+
if !toc.is_empty() {
580+
let mut outlines = self.new_dict()?;
581+
outlines.dict_put("Type", PdfObject::new_name("Outlines")?)?;
582+
// Now we access outlines indirectly
583+
let mut outlines = self.add_object(&outlines)?;
584+
self.walk_outlines_insert(toc, &mut outlines)?;
585+
self.catalog()?.dict_put("Outlines", outlines)?;
586+
}
587+
588+
Ok(())
589+
}
590+
591+
fn walk_outlines_insert(
592+
&mut self,
593+
down: &[Outline],
594+
parent: &mut PdfObject,
595+
) -> Result<(), Error> {
596+
debug_assert!(!down.is_empty() && parent.is_indirect()?);
597+
598+
// All the indirect references in the current level.
599+
let mut refs = Vec::new();
600+
601+
for outline in down {
602+
let mut item = self.new_dict()?;
603+
item.dict_put("Title", PdfObject::new_string(&outline.title)?)?;
604+
item.dict_put("Parent", parent.clone())?;
605+
if let Some(dest) = outline
606+
.page
607+
.map(|page| {
608+
let page = self.find_page(page as i32)?;
609+
610+
let matrix = page.page_ctm()?;
611+
let fz_point = Point::new(outline.x, outline.y);
612+
let Point { x, y } = fz_point.transform(&matrix);
613+
let dest_kind = DestinationKind::XYZ {
614+
left: Some(x),
615+
top: Some(y),
616+
zoom: None,
617+
};
618+
let dest = Destination::new(page, dest_kind);
619+
620+
let mut array = self.new_array()?;
621+
dest.encode_into(&mut array)?;
622+
623+
Ok(array)
624+
})
625+
.or_else(|| outline.uri.as_deref().map(PdfObject::new_string))
626+
.transpose()?
627+
{
628+
item.dict_put("Dest", dest)?;
629+
}
630+
631+
refs.push(self.add_object(&item)?);
632+
if !outline.down.is_empty() {
633+
self.walk_outlines_insert(&outline.down, refs.last_mut().unwrap())?;
634+
}
635+
}
636+
637+
// NOTE: doing the same thing as mutation version of `slice::array_windows`
638+
for i in 0..down.len().saturating_sub(1) {
639+
let [prev, next, ..] = &mut refs[i..] else {
640+
unreachable!();
641+
};
642+
prev.dict_put("Next", next.clone())?;
643+
next.dict_put("Prev", prev.clone())?;
644+
}
645+
646+
let mut refs = refs.into_iter();
647+
let first = refs.next().unwrap();
648+
let last = refs.last().unwrap_or_else(|| first.clone());
649+
650+
parent.dict_put("First", first)?;
651+
parent.dict_put("Last", last)?;
652+
653+
Ok(())
654+
}
655+
656+
/// Delete `/Outlines` in document catalog and all the **outline items** it points to.
657+
///
658+
/// Do nothing if document has no outlines.
659+
pub fn delete_outlines(&mut self) -> Result<(), Error> {
660+
if let Some(outlines) = self.catalog()?.get_dict("Outlines")? {
661+
if let Some(outline) = outlines.get_dict("First")? {
662+
self.walk_outlines_del(outline)?;
663+
}
664+
self.delete_object(outlines.as_indirect()?)?;
665+
}
666+
667+
Ok(())
668+
}
669+
670+
fn walk_outlines_del(&mut self, outline: PdfObject) -> Result<(), Error> {
671+
let mut cur = Some(outline);
672+
673+
while let Some(item) = cur.take() {
674+
if let Some(down) = item.get_dict("First")? {
675+
self.walk_outlines_del(down)?;
676+
}
677+
cur = item.get_dict("Next")?;
678+
self.delete_object(item.as_indirect()?)?;
679+
}
680+
681+
Ok(())
682+
}
575683
}
576684

577685
impl Deref for PdfDocument {

src/pdf/object.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use std::slice;
77
use mupdf_sys::*;
88

99
use crate::pdf::PdfDocument;
10-
use crate::{context, Buffer, Error};
10+
use crate::{context, Buffer, Error, Matrix};
1111

1212
pub trait IntoPdfDictKey {
1313
fn into_pdf_dict_key(self) -> Result<PdfObject, Error>;
@@ -388,6 +388,13 @@ impl PdfObject {
388388
Some(PdfDocument::from_raw(ptr))
389389
}
390390
}
391+
392+
pub fn page_ctm(&self) -> Result<Matrix, Error> {
393+
let matrix =
394+
unsafe { ffi_try!(mupdf_pdf_page_obj_transform(context(), self.inner)).into() };
395+
396+
Ok(matrix)
397+
}
391398
}
392399

393400
impl Write for PdfObject {

src/point.rs

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
use mupdf_sys::fz_point;
1+
use mupdf_sys::{fz_point, fz_transform_point};
2+
3+
use crate::Matrix;
24

35
/// A point in a two-dimensional space.
46
#[derive(Debug, Clone, Copy, PartialEq)]
@@ -11,6 +13,22 @@ impl Point {
1113
pub const fn new(x: f32, y: f32) -> Self {
1214
Self { x, y }
1315
}
16+
17+
/// Apply a transformation to a point.
18+
///
19+
/// The NaN coordinates will be reset to 0.0,
20+
/// which make `fz_transform_point` works normally.
21+
/// Otherwise `(NaN, NaN)` will be returned.
22+
pub fn transform(mut self, matrix: &Matrix) -> Self {
23+
if self.x.is_nan() {
24+
self.x = 0.0;
25+
}
26+
if self.y.is_nan() {
27+
self.y = 0.0;
28+
}
29+
30+
unsafe { fz_transform_point(self.into(), matrix.into()).into() }
31+
}
1432
}
1533

1634
impl From<fz_point> for Point {
@@ -19,6 +37,12 @@ impl From<fz_point> for Point {
1937
}
2038
}
2139

40+
impl From<Point> for fz_point {
41+
fn from(p: Point) -> Self {
42+
fz_point { x: p.x, y: p.y }
43+
}
44+
}
45+
2246
impl From<(f32, f32)> for Point {
2347
fn from(p: (f32, f32)) -> Self {
2448
Self { x: p.0, y: p.1 }

0 commit comments

Comments
 (0)