use std::ffi::{CStr, CString};
use std::io::Write;
use std::ptr;
use mupdf_sys::*;
use crate::link::LinkDestination;
use crate::pdf::PdfDocument;
use crate::{context, Buffer, Colorspace, Cookie, Error, FilePath, Outline, Page};
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum MetadataName {
Format,
Encryption,
Author,
Title,
Producer,
Creator,
CreationDate,
ModDate,
Subject,
Keywords,
}
impl MetadataName {
pub fn to_str(&self) -> &'static str {
use MetadataName::*;
match *self {
Format => "format",
Encryption => "encryption",
Author => "info:Author",
Title => "info:Title",
Producer => "info:Producer",
Creator => "info:Creator",
CreationDate => "info:CreationDate",
ModDate => "info:ModDate",
Subject => "info:Subject",
Keywords => "info:Keywords",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Location {
pub chapter: u32,
pub page_in_chapter: u32,
pub page_number: u32,
}
#[derive(Debug)]
pub struct Document {
pub(crate) inner: *mut fz_document,
}
impl Document {
pub(crate) unsafe fn from_raw(ptr: *mut fz_document) -> Self {
Self { inner: ptr }
}
pub fn open<P: AsRef<FilePath> + ?Sized>(p: &P) -> Result<Self, Error> {
let c_name = CString::new(p.as_ref().as_bytes())?;
unsafe { ffi_try!(mupdf_open_document(context(), c_name.as_ptr())) }
.map(|inner| Self { inner })
}
pub fn from_bytes(bytes: &[u8], magic: &str) -> Result<Self, Error> {
let c_magic = CString::new(magic)?;
let len = bytes.len();
let mut buf = Buffer::with_capacity(len);
buf.write_all(bytes)?;
unsafe {
ffi_try!(mupdf_open_document_from_bytes(
context(),
buf.inner,
c_magic.as_ptr()
))
}
.map(|inner| Self { inner })
}
pub fn recognize(magic: &str) -> Result<bool, Error> {
let c_magic = CString::new(magic)?;
unsafe { ffi_try!(mupdf_recognize_document(context(), c_magic.as_ptr())) }
}
pub fn needs_password(&self) -> Result<bool, Error> {
unsafe { ffi_try!(mupdf_needs_password(context(), self.inner)) }
}
pub fn authenticate(&mut self, password: &str) -> Result<bool, Error> {
let c_pass = CString::new(password)?;
unsafe {
ffi_try!(mupdf_authenticate_password(
context(),
self.inner,
c_pass.as_ptr()
))
}
}
pub fn page_count(&self) -> Result<i32, Error> {
unsafe { ffi_try!(mupdf_document_page_count(context(), self.inner)) }
}
pub fn metadata(&self, name: MetadataName) -> Result<String, Error> {
let c_key = CString::new(name.to_str())?;
let info_ptr =
unsafe { ffi_try!(mupdf_lookup_metadata(context(), self.inner, c_key.as_ptr())) }?;
if info_ptr.is_null() {
return Ok(String::new());
}
let c_info = unsafe { CStr::from_ptr(info_ptr) };
let info = c_info.to_string_lossy().into_owned();
unsafe {
mupdf_drop_str(info_ptr);
}
Ok(info)
}
pub fn resolve_link(&self, uri: &str) -> Result<Option<LinkDestination>, Error> {
let c_uri = CString::new(uri)?;
LinkDestination::from_uri(self, &c_uri)
}
pub fn is_reflowable(&self) -> Result<bool, Error> {
unsafe { ffi_try!(mupdf_is_document_reflowable(context(), self.inner)) }
}
pub fn is_pdf(&self) -> bool {
let pdf = unsafe { pdf_specifics(context(), self.inner) };
if !pdf.is_null() {
return true;
}
false
}
pub fn convert_to_pdf(
&self,
start_page: i32,
end_page: i32,
rotate: u32,
) -> Result<PdfDocument, Error> {
self.convert_to_pdf_internal(start_page, end_page, rotate, None)
}
pub fn convert_to_pdf_with_cookie(
&self,
start_page: i32,
end_page: i32,
rotate: u32,
cookie: &Cookie,
) -> Result<PdfDocument, Error> {
self.convert_to_pdf_internal(start_page, end_page, rotate, Some(cookie))
}
fn convert_to_pdf_internal(
&self,
start_page: i32,
end_page: i32,
rotate: u32,
cookie: Option<&Cookie>,
) -> Result<PdfDocument, Error> {
let page_count = self.page_count()? as i32;
let start_page = if start_page > page_count - 1 {
page_count - 1
} else {
start_page
};
let end_page = if end_page > page_count - 1 || end_page < 0 {
page_count - 1
} else {
end_page
};
let cookie_ptr = if let Some(ck) = cookie {
ck.inner
} else {
ptr::null_mut()
};
unsafe {
ffi_try!(mupdf_convert_to_pdf(
context(),
self.inner,
start_page,
end_page,
rotate as i32,
cookie_ptr
))
}
.map(|inner| unsafe { PdfDocument::from_raw(inner) })
}
pub fn layout(&mut self, width: f32, height: f32, em: f32) -> Result<(), Error> {
unsafe {
ffi_try!(mupdf_layout_document(
context(),
self.inner,
width,
height,
em
))
}
}
pub fn load_page(&self, page_no: i32) -> Result<Page, Error> {
unsafe { ffi_try!(mupdf_load_page(context(), self.inner, page_no)) }
.and_then(|fz_page| unsafe { Page::from_raw(fz_page) })
}
pub fn pages(&self) -> Result<PageIter<'_>, Error> {
Ok(PageIter {
index: 0,
total: self.page_count()?,
doc: self,
})
}
pub fn output_intent(&self) -> Result<Option<Colorspace>, Error> {
let inner = unsafe { ffi_try!(mupdf_document_output_intent(context(), self.inner)) }?;
if inner.is_null() {
return Ok(None);
}
Ok(Some(unsafe { Colorspace::from_raw(inner) }))
}
unsafe fn walk_outlines(&self, outline: *mut fz_outline) -> Vec<Outline> {
let mut outlines = Vec::new();
let mut next = outline;
while !next.is_null() {
let title = CStr::from_ptr((*next).title).to_string_lossy().into_owned();
let (uri, dest) = if !(*next).uri.is_null() {
let uri = CStr::from_ptr((*next).uri);
let dest = LinkDestination::from_uri(self, uri).unwrap();
(Some(uri.to_string_lossy().into_owned()), dest)
} else {
(None, None)
};
let down = if !(*next).down.is_null() {
self.walk_outlines((*next).down)
} else {
Vec::new()
};
outlines.push(Outline {
title,
uri,
dest,
down,
});
next = (*next).next;
}
outlines
}
pub fn outlines(&self) -> Result<Vec<Outline>, Error> {
let outline = unsafe { ffi_try!(mupdf_load_outline(context(), self.inner)) }?;
if outline.is_null() {
return Ok(Vec::new());
}
unsafe {
let toc = self.walk_outlines(outline);
fz_drop_outline(context(), outline);
Ok(toc)
}
}
}
impl Drop for Document {
fn drop(&mut self) {
if !self.inner.is_null() {
unsafe {
fz_drop_document(context(), self.inner);
}
}
}
}
impl Clone for Document {
fn clone(&self) -> Self {
unsafe { Document::from_raw(fz_keep_document(context(), self.inner)) }
}
}
#[derive(Debug)]
pub struct PageIter<'a> {
index: i32,
total: i32,
doc: &'a Document,
}
impl Iterator for PageIter<'_> {
type Item = Result<Page, Error>;
fn next(&mut self) -> Option<Self::Item> {
if self.index >= self.total {
return None;
}
let page = self.doc.load_page(self.index);
self.index += 1;
Some(page)
}
}
impl<'a> IntoIterator for &'a Document {
type Item = Result<Page, Error>;
type IntoIter = PageIter<'a>;
fn into_iter(self) -> Self::IntoIter {
self.pages().unwrap()
}
}
impl<'a> IntoIterator for &'a mut Document {
type Item = Result<Page, Error>;
type IntoIter = PageIter<'a>;
fn into_iter(self) -> Self::IntoIter {
self.pages().unwrap()
}
}
#[cfg(test)]
macro_rules! test_document {
($root:literal, $path:literal as PdfDocument) => {{
#[cfg(not(target_arch = "wasm32"))]
let doc = PdfDocument::open(concat!("tests/", $path));
#[cfg(target_arch = "wasm32")]
let doc = PdfDocument::from_bytes(include_bytes!(concat!($root, "/tests/", $path)));
doc
}};
($root:literal, $path:literal) => {{
#[cfg(not(target_arch = "wasm32"))]
let doc = Document::open(concat!("tests/", $path));
#[cfg(target_arch = "wasm32")]
let doc = Document::from_bytes(include_bytes!(concat!($root, "/tests/", $path)), $path);
doc
}};
}
#[cfg(test)]
pub(crate) use test_document;
#[cfg(test)]
mod test {
use crate::{document::Location, link::LinkDestination, DestinationKind};
use super::{Document, MetadataName, Page};
#[test]
fn test_recognize_document() {
assert!(Document::recognize("test.pdf").unwrap());
assert!(Document::recognize("application/pdf").unwrap());
assert!(Document::recognize("text/html").unwrap());
assert!(!Document::recognize("test.doc").unwrap());
}
#[test]
fn test_document_open_html() {
let doc = test_document!("..", "files/dummy.html").unwrap();
assert!(!doc.is_pdf());
}
#[test]
fn test_document_load_page() {
let doc = test_document!("..", "files/dummy.pdf").unwrap();
assert!(doc.is_pdf());
assert_eq!(doc.page_count().unwrap(), 1);
let page0 = doc.load_page(0).unwrap();
let bounds = page0.bounds().unwrap();
assert_eq!(bounds.x0, 0.0);
assert_eq!(bounds.y0, 0.0);
assert_eq!(bounds.x1, 595.0);
assert_eq!(bounds.y1, 842.0);
let cs = doc.output_intent().unwrap();
assert!(cs.is_none());
}
#[test]
fn test_encrypted_document_load_page() {
let mut doc = test_document!("..", "files/dummy-encrypted.pdf").unwrap();
assert!(doc.is_pdf());
assert!(doc.needs_password().unwrap());
let outlines = doc.outlines().unwrap();
assert_eq!(outlines.len(), 0);
doc.authenticate("123456").unwrap();
let outlines = doc.outlines().unwrap();
assert_eq!(outlines.len(), 0);
assert_eq!(doc.page_count().unwrap(), 1);
let page0 = doc.load_page(0).unwrap();
let bounds = page0.bounds().unwrap();
assert_eq!(bounds.x0, 0.0);
assert_eq!(bounds.y0, 0.0);
assert_eq!(bounds.x1, 595.0);
assert_eq!(bounds.y1, 842.0);
}
#[test]
fn test_document_page_iterator() {
let doc = test_document!("..", "files/dummy.pdf").unwrap();
let pages: Result<Vec<Page>, _> = doc.into_iter().collect();
let pages = pages.unwrap();
assert_eq!(pages.len(), 1);
let page0 = &pages[0];
let bounds = page0.bounds().unwrap();
assert_eq!(bounds.x0, 0.0);
assert_eq!(bounds.y0, 0.0);
assert_eq!(bounds.x1, 595.0);
assert_eq!(bounds.y1, 842.0);
}
#[test]
fn test_document_metadata() {
let doc = test_document!("..", "files/dummy.pdf").unwrap();
let format = doc.metadata(MetadataName::Format).unwrap();
assert_eq!(format, "PDF 1.4");
let encryption = doc.metadata(MetadataName::Encryption).unwrap();
assert_eq!(encryption, "None");
let author = doc.metadata(MetadataName::Author).unwrap();
assert_eq!(author, "Evangelos Vlachogiannis");
let title = doc.metadata(MetadataName::Title).unwrap();
assert!(title.is_empty());
let producer = doc.metadata(MetadataName::Producer).unwrap();
assert_eq!(producer, "OpenOffice.org 2.1");
let creator = doc.metadata(MetadataName::Creator).unwrap();
assert_eq!(creator, "Writer");
let creation_date = doc.metadata(MetadataName::CreationDate).unwrap();
assert_eq!(creation_date, "D:20070223175637+02'00'");
let mod_date = doc.metadata(MetadataName::ModDate).unwrap();
assert!(mod_date.is_empty());
let subject = doc.metadata(MetadataName::Subject).unwrap();
assert!(subject.is_empty());
let keywords = doc.metadata(MetadataName::Keywords).unwrap();
assert!(keywords.is_empty());
}
#[test]
fn test_document_outlines() {
let doc = test_document!("..", "files/dummy.pdf").unwrap();
let outlines = doc.outlines().unwrap();
assert_eq!(outlines.len(), 1);
let out1 = &outlines[0];
assert_eq!(
out1.dest,
Some(LinkDestination {
loc: Location {
chapter: 0,
page_in_chapter: 0,
page_number: 0,
},
kind: DestinationKind::XYZ {
left: Some(56.7),
top: Some(68.70001),
zoom: Some(100.0),
}
})
);
assert_eq!(out1.title, "Dummy PDF file");
assert_eq!(out1.uri.as_deref(), Some("#page=1&zoom=100,56.7,68.70001"));
}
}