Skip to content
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ serde = ["dep:serde"]
mupdf-sys = { version = "0.6.0", path = "mupdf-sys", features = ["zerocopy"] }
once_cell = "1.3.1"
bitflags = "2.0.2"
percent-encoding = "2.3.1"
serde = { version = "1.0.201", features = ["derive"], optional = true }
zerocopy = { version = "0.8.17", features = ["derive"] }

Expand Down
322 changes: 218 additions & 104 deletions src/destination.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::fmt;

use crate::pdf::PdfObject;
use crate::{Error, Matrix, Rect};

Expand All @@ -16,113 +18,11 @@ impl Destination {
}

/// Encode this destination into a PDF "Dest" array.
///
/// # MuPDF parity / Source
/// Ported from MuPDF [`pdf_new_dest_from_link`] (pdf/pdf-link.c).
///
/// In MuPDF logic:
///
/// - optional parameters are represented internally by `NaN` (missing).
/// - when serializing into a PDF destination array, MuPDF writes `null`
/// for `NaN` (missing) values.
///
/// In this Rust crate:
///
/// - **`Option::None`** represents a missing parameter.
/// - **`Option::None`** is serialized as the PDF `null` object.
///
/// Additionally for `/XYZ`:
///
/// - MuPDF stores zoom internally as a percentage (100 == 100% zoom).
/// - PDF `/XYZ` expects a scale factor (1.0 == 100%), so we write `zoom/100`.
///
/// # Coordinate space
///
/// This method does **not** apply any coordinate transforms.
/// It expects `self.kind` coordinates to already be in PDF user space for the *target page*.
/// See [DestinationKind::encode_into] documentation for more information
pub(crate) fn encode_into(self, array: &mut PdfObject) -> Result<(), Error> {
debug_assert_eq!(array.len()?, 0);

#[cold]
fn push_null(array: &mut PdfObject) -> Result<(), Error> {
array.array_push(PdfObject::new_null())
}

#[inline]
fn push_real_or_null(array: &mut PdfObject, v: Option<f32>) -> Result<(), Error> {
match v {
Some(v) => {
if !v.is_nan() {
array.array_push(PdfObject::new_real(v)?)
} else {
push_null(array) // move out from hot path
}
}
None => array.array_push(PdfObject::new_null()),
}
}

// 1) Page reference (local destination)
array.array_push(self.page)?;

// 2) Kind
match self.kind {
DestinationKind::Fit => array.array_push(PdfObject::new_name("Fit")?),

DestinationKind::FitH { top } => {
array.array_push(PdfObject::new_name("FitH")?)?;
// MuPDF: if isnan(p.y) push NULL else real
// https://github.com/ArtifexSoftware/mupdf/blob/60bf95d09f496ab67a5e4ea872bdd37a74b745fe/source/pdf/pdf-link.c#L1340
push_real_or_null(array, top)
}

DestinationKind::FitV { left } => {
array.array_push(PdfObject::new_name("FitV")?)?;
// https://github.com/ArtifexSoftware/mupdf/blob/60bf95d09f496ab67a5e4ea872bdd37a74b745fe/source/pdf/pdf-link.c#L1356
push_real_or_null(array, left)
}

DestinationKind::XYZ { left, top, zoom } => {
array.array_push(PdfObject::new_name("XYZ")?)?;
// https://github.com/ArtifexSoftware/mupdf/blob/60bf95d09f496ab67a5e4ea872bdd37a74b745fe/source/pdf/pdf-link.c#L1391
push_real_or_null(array, left)?;
// https://github.com/ArtifexSoftware/mupdf/blob/60bf95d09f496ab67a5e4ea872bdd37a74b745fe/source/pdf/pdf-link.c#L1395
push_real_or_null(array, top)?;

// MuPDF: stores zoom as percent (100 == 100%), but PDF wants scale (1.0 == 100%)
// https://github.com/ArtifexSoftware/mupdf/blob/60bf95d09f496ab67a5e4ea872bdd37a74b745fe/source/pdf/pdf-link.c#L1399
push_real_or_null(array, zoom.map(|z| z / 100.0))
}

DestinationKind::FitR {
left,
bottom,
right,
top,
} => {
array.array_push(PdfObject::new_name("FitR")?)?;
// In PDF all 4 coordinates are required -> always real.
// https://github.com/ArtifexSoftware/mupdf/blob/60bf95d09f496ab67a5e4ea872bdd37a74b745fe/source/pdf/pdf-link.c#L1411
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")?)?;
// https://github.com/ArtifexSoftware/mupdf/blob/60bf95d09f496ab67a5e4ea872bdd37a74b745fe/source/pdf/pdf-link.c#L1348
push_real_or_null(array, top)
}

DestinationKind::FitBV { left } => {
array.array_push(PdfObject::new_name("FitBV")?)?;
// https://github.com/ArtifexSoftware/mupdf/blob/60bf95d09f496ab67a5e4ea872bdd37a74b745fe/source/pdf/pdf-link.c#L1364
push_real_or_null(array, left)
}
}
self.kind.encode_into(array)
}
}

Expand Down Expand Up @@ -360,6 +260,220 @@ impl DestinationKind {
}
}
}

/// Encode this destination into a PDF "Dest" array.
///
/// # MuPDF parity / Source
///
/// Ported from MuPDF [`pdf_new_dest_from_link`] (pdf/pdf-link.c).
///
/// In MuPDF logic:
///
/// - optional parameters are represented internally by `NaN` (missing).
/// - when serializing into a PDF destination array, MuPDF writes `null`
/// for `NaN` (missing) values.
///
/// In this Rust crate:
///
/// - **`Option::None`** represents a missing parameter.
/// - **`Option::None`** is serialized as the PDF `null` object.
///
/// Additionally for `/XYZ`:
///
/// - MuPDF stores zoom internally as a percentage (100 == 100% zoom).
/// - PDF `/XYZ` expects a scale factor (1.0 == 100%), so we write `zoom/100`.
///
/// # Coordinate space
///
/// This method does **not** apply any coordinate transforms.
/// It expects `self` coordinates to already be in PDF user space for the *target page*.
pub fn encode_into(self, array: &mut PdfObject) -> Result<(), Error> {
#[cold]
fn push_null(array: &mut PdfObject) -> Result<(), Error> {
array.array_push(PdfObject::new_null())
}

#[inline]
fn push_real_or_null(array: &mut PdfObject, v: Option<f32>) -> Result<(), Error> {
match v {
Some(v) => {
if !v.is_nan() {
array.array_push(PdfObject::new_real(v)?)
} else {
push_null(array) // move out from hot path
}
}
None => array.array_push(PdfObject::new_null()),
}
}

match self {
DestinationKind::Fit => array.array_push(PdfObject::new_name("Fit")?),

DestinationKind::FitH { top } => {
array.array_push(PdfObject::new_name("FitH")?)?;
// MuPDF: if isnan(p.y) push NULL else real
// https://github.com/ArtifexSoftware/mupdf/blob/60bf95d09f496ab67a5e4ea872bdd37a74b745fe/source/pdf/pdf-link.c#L1340
push_real_or_null(array, top)
}

DestinationKind::FitV { left } => {
array.array_push(PdfObject::new_name("FitV")?)?;
// https://github.com/ArtifexSoftware/mupdf/blob/60bf95d09f496ab67a5e4ea872bdd37a74b745fe/source/pdf/pdf-link.c#L1356
push_real_or_null(array, left)
}

DestinationKind::XYZ { left, top, zoom } => {
array.array_push(PdfObject::new_name("XYZ")?)?;
// https://github.com/ArtifexSoftware/mupdf/blob/60bf95d09f496ab67a5e4ea872bdd37a74b745fe/source/pdf/pdf-link.c#L1391
push_real_or_null(array, left)?;
// https://github.com/ArtifexSoftware/mupdf/blob/60bf95d09f496ab67a5e4ea872bdd37a74b745fe/source/pdf/pdf-link.c#L1395
push_real_or_null(array, top)?;

// MuPDF: stores zoom as percent (100 == 100%), but PDF wants scale (1.0 == 100%)
// https://github.com/ArtifexSoftware/mupdf/blob/60bf95d09f496ab67a5e4ea872bdd37a74b745fe/source/pdf/pdf-link.c#L1399
push_real_or_null(array, zoom.map(|z| z / 100.0))
}

DestinationKind::FitR {
left,
bottom,
right,
top,
} => {
array.array_push(PdfObject::new_name("FitR")?)?;
// In PDF all 4 coordinates are required -> always real.
// https://github.com/ArtifexSoftware/mupdf/blob/60bf95d09f496ab67a5e4ea872bdd37a74b745fe/source/pdf/pdf-link.c#L1411
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")?)?;
// https://github.com/ArtifexSoftware/mupdf/blob/60bf95d09f496ab67a5e4ea872bdd37a74b745fe/source/pdf/pdf-link.c#L1348
push_real_or_null(array, top)
}

DestinationKind::FitBV { left } => {
array.array_push(PdfObject::new_name("FitBV")?)?;
// https://github.com/ArtifexSoftware/mupdf/blob/60bf95d09f496ab67a5e4ea872bdd37a74b745fe/source/pdf/pdf-link.c#L1364
push_real_or_null(array, left)
}
}
}
}

impl Default for DestinationKind {
fn default() -> Self {
// This analogue of MuPDF's `fz_make_link_dest_none` function
// (https://github.com/ArtifexSoftware/mupdf/blob/60bf95d09f496ab67a5e4ea872bdd37a74b745fe/source/fitz/link.c#L96)
DestinationKind::XYZ {
left: None,
top: None,
zoom: None,
}
}
}

impl fmt::Display for DestinationKind {
/// Formats this destination as a [MuPDF-compatible] URI fragment suffix based on the Adobe specification
/// ["Parameters for Opening PDF Files"] from the Adobe Acrobat SDK, version 8.1.
///
/// This is only the parameter tail appended after `#page=...` in a link URI, so any emitted suffix starts
/// with `&`. The output may also be empty (e.g. for the default `XYZ` with all fields missing).
///
/// Output shapes:
///
/// - `Fit` / `FitB` -> `&view=Fit` / `&view=FitB`
/// - `FitH` / `FitV` / `FitBH` / `FitBV` -> `&view=Name` or `&view=Name,<coord>`
/// - `XYZ` -> `&zoom=<zoom>,<left>,<top>`
/// - `FitR` -> `&viewrect=<left>,<bottom>,<width>,<height>`
///
/// Missing values:
///
/// - `None` means “unspecified” (`null` as per [PDF 32000-1:2008, 12.3.2.2]). `Some(NaN)` is treated as `None`.
/// - For `Fit*`, a missing coordinate is omitted.
/// - For `XYZ`, the fragment is omitted when all fields are missing, otherwise missing fields are written as `nan`.
/// - For `XYZ`, `zoom == 0` is treated as missing.
///
/// Values are written as-is (no transforms applied).
///
/// [MuPDF-compatible]: https://github.com/ArtifexSoftware/mupdf/blob/60bf95d09f496ab67a5e4ea872bdd37a74b745fe/include/mupdf/pdf/annot.h#L317
/// [PDF 32000-1:2008, 12.3.2.2]: https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/PDF32000_2008.pdf#G11.1696125
/// ["Parameters for Opening PDF Files"]: https://web.archive.org/web/20170921000830/http://www.adobe.com/content/dam/Adobe/en/devnet/acrobat/pdfs/pdf_open_parameters.pdf
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
#[cold]
#[inline(never)]
fn cold_nan_handler() {}

#[inline(always)]
fn filter_nan(v: Option<f32>) -> Option<f32> {
match v {
Some(val) => {
if !val.is_nan() {
Some(val)
} else {
cold_nan_handler(); // move out from hot path
None
}
}
None => None,
}
}

match *self {
Self::Fit => f.write_str("&view=Fit"),
Self::FitB => f.write_str("&view=FitB"),
Self::FitH { top } => write_fit_view(f, "FitH", filter_nan(top)),
Self::FitBH { top } => write_fit_view(f, "FitBH", filter_nan(top)),
Self::FitV { left } => write_fit_view(f, "FitV", filter_nan(left)),
Self::FitBV { left } => write_fit_view(f, "FitBV", filter_nan(left)),
Self::XYZ { left, top, zoom } => {
let x = filter_nan(left);
let y = filter_nan(top);
let z = filter_nan(zoom).filter(|&v| v != 0.0);

if x.is_some() || y.is_some() || z.is_some() {
f.write_str("&zoom=")?;
write_f32_or_nan(f, z)?;
f.write_str(",")?;
write_f32_or_nan(f, x)?;
f.write_str(",")?;
write_f32_or_nan(f, y)?;
}
Ok(())
}
Self::FitR {
left,
bottom,
right,
top,
} => {
let w = right - left;
let h = top - bottom;
write!(f, "&viewrect={left},{bottom},{w},{h}")
}
}
}
}

/// Writes `&view=Name,val` or `&view=Name` depending on whether `val` is present.
fn write_fit_view(f: &mut fmt::Formatter<'_>, name: &str, val: Option<f32>) -> fmt::Result {
match val {
Some(v) => write!(f, "&view={name},{v}"),
None => write!(f, "&view={name}"),
}
}

fn write_f32_or_nan(f: &mut fmt::Formatter<'_>, v: Option<f32>) -> fmt::Result {
match v {
Some(v) => write!(f, "{v}"),
None => f.write_str("nan"),
}
}

impl From<fz_link_dest> for DestinationKind {
Expand Down
Loading
Loading