use std::sync::Arc;
use pdfrum_common::{Diagnostics, LimitExceeded, Limits, Operation, PageIndex};
use pdfrum_object::{Name, Resolve, names};
use pdfrum_page::{BuildContext, Rotation};
use pdfrum_parser::PageDict;
use crate::{
Annotation, Document, Error, Pixmap, RasterBackend, RenderOptions, RenderSession, Result,
TextPage, Word,
};
#[derive(Debug, Clone)]
pub struct Page<'a> {
pub(crate) doc: &'a Document,
pub(crate) dict: Arc<PageDict>,
pub(crate) index: PageIndex,
pub(crate) media_box: kurbo::Rect,
pub(crate) crop_box: kurbo::Rect,
pub(crate) rotation: Rotation,
}
impl<'a> Page<'a> {
pub(crate) fn load(doc: &'a Document, index: PageIndex) -> Result<Page<'a>> {
doc.limits
.check_deadline(Operation::PageLoad)
.map_err(|limit| limit.on_page(index))?;
let dict = doc.inner.page(index)?;
let mut diags = Diagnostics::default();
let (media_box, crop_box) = pdfrum_page::derive_boxes(
&dict.dict,
|key| dict.inherited(key, &doc.inner),
&doc.inner,
&mut diags,
);
doc.note(&diags);
let rotation = pdfrum_page::Rotation::from_degrees(
dict.inherited(&Name::from("Rotate"), &doc.inner)
.as_ref()
.and_then(pdfrum_object::Object::as_int)
.unwrap_or(0),
);
Ok(Page {
doc,
dict: Arc::new(dict),
index,
media_box,
crop_box,
rotation,
})
}
#[must_use]
pub fn index(&self) -> PageIndex {
self.index
}
#[must_use]
pub fn width(&self) -> f64 {
self.display_size().0
}
#[must_use]
pub fn height(&self) -> f64 {
self.display_size().1
}
fn display_size(&self) -> (f64, f64) {
let (w, h) = (self.crop_box.width(), self.crop_box.height());
match self.rotation {
Rotation::Quarter | Rotation::ThreeQuarter => (h, w),
Rotation::None | Rotation::Half => (w, h),
}
}
#[must_use]
pub fn media_box(&self) -> kurbo::Rect {
self.media_box
}
#[must_use]
pub fn crop_box(&self) -> kurbo::Rect {
self.crop_box
}
#[must_use]
pub fn bleed_box(&self) -> Option<kurbo::Rect> {
self.named_box(names::BLEED_BOX)
}
#[must_use]
pub fn trim_box(&self) -> Option<kurbo::Rect> {
self.named_box(names::TRIM_BOX)
}
#[must_use]
pub fn art_box(&self) -> Option<kurbo::Rect> {
self.named_box(names::ART_BOX)
}
fn named_box(&self, key: &Name) -> Option<kurbo::Rect> {
self.dict
.dict
.array(key, &self.doc.inner)
.map(|array| array.as_rect())
}
#[must_use]
pub fn rotation(&self) -> Rotation {
self.rotation
}
pub fn render<B: RasterBackend>(&self, backend: &B, options: &RenderOptions) -> Result<Pixmap> {
self.render_on(backend, options, &mut RenderSession::default())
}
pub fn render_on<B: RasterBackend>(
&self,
backend: &B,
options: &RenderOptions,
session: &mut RenderSession,
) -> Result<Pixmap> {
check_pixel_cap(&self.doc.limits, self.display_size(), options)?;
self.prepare(options, session).render_on(backend, session)
}
#[cfg(feature = "svg-export")]
pub fn to_svg<B: RasterBackend>(
&self,
backend: &B,
options: &RenderOptions,
) -> Result<crate::svg::SvgPage> {
self.to_svg_on(backend, options, &mut RenderSession::default())
}
#[cfg(feature = "svg-export")]
pub fn to_svg_on<B: RasterBackend>(
&self,
backend: &B,
options: &RenderOptions,
session: &mut RenderSession,
) -> Result<crate::svg::SvgPage> {
check_pixel_cap(&self.doc.limits, self.display_size(), options)?;
self.prepare(options, session).to_svg_on(backend, session)
}
#[must_use]
pub fn prepare(
&self,
options: &RenderOptions,
session: &mut RenderSession,
) -> PreparedPage<'a> {
let ctx = &mut session.build;
let previous = std::mem::replace(&mut ctx.decode_target, self.decode_target(options));
let mut page = self.build(ctx);
if options.annotations {
let mut diags = Diagnostics::default();
pdfrum_doc::annot_render::overlay(
&mut page,
&self.dict.dict,
&self.doc.catalog(),
&self.doc.inner,
ctx,
&self.doc.limits,
&mut diags,
);
self.doc.note(&diags);
}
ctx.decode_target = previous;
PreparedPage {
doc: self.doc,
index: self.index,
graph: page,
options: options.clone(),
}
}
fn decode_target(&self, options: &RenderOptions) -> pdfrum_page::RequestedSize {
let corners = device_box(self.display_size(), options);
pdfrum_page::RequestedSize::for_device(corners.width(), corners.height())
}
#[must_use]
pub fn text(&self) -> TextPage {
self.text_on(&mut RenderSession::default())
}
#[must_use]
pub fn words(&self) -> Vec<Word> {
self.text().words()
}
#[must_use]
pub fn text_on(&self, session: &mut RenderSession) -> TextPage {
let previous = std::mem::replace(
&mut session.build.decode_target,
pdfrum_page::RequestedSize::NoSamples,
);
let page = self.build(&mut session.build);
session.build.decode_target = previous;
let mut diags = Diagnostics::default();
let options = pdfrum_text::ExtractOptions {
rtl: self.doc.reads_right_to_left(),
};
let text = pdfrum_text::extract(
&page,
&self.doc.inner,
&options,
&self.doc.limits,
&mut diags,
);
self.doc.note(&diags);
text
}
#[must_use]
pub fn annotations(&self) -> Vec<Annotation<'a>> {
#[expect(
clippy::cast_possible_truncation,
reason = "the annotation list places synthesized pop-ups relative to the \
page width, which upstream measures in f32; a page wider than \
f32 can express is not one this changes the behaviour of"
)]
let width = self.crop_box.width() as f32;
pdfrum_doc::annot::AnnotList::load(&self.dict.dict, width, &self.doc.inner)
.annots
.into_iter()
.map(|inner| Annotation {
inner,
doc: self.doc,
})
.collect()
}
#[must_use]
pub fn links(&self) -> Vec<pdfrum_doc::Link> {
pdfrum_doc::nav::page_links(&self.dict.dict, &self.doc.inner)
.into_iter()
.flatten()
.collect()
}
#[must_use]
pub fn page_links(&self) -> Vec<PageLink> {
let mut diags = Diagnostics::default();
let catalog = self.doc.catalog();
let resolver = &self.doc.inner;
let links = self
.links()
.into_iter()
.map(|link| {
let rect = link.rect(resolver);
let uri = link
.action(resolver)
.filter(|action| action.kind() == pdfrum_doc::ActionKind::Uri)
.map(|action| action.uri(&catalog, resolver));
let target = match uri {
Some(uri) => LinkTarget::Uri(String::from_utf8_lossy(&uri).into_owned()),
None => link
.dest(&catalog, resolver, &self.doc.limits, &mut diags)
.page_index(resolver, |num| self.doc.page_index_of(num), &mut diags)
.map_or(LinkTarget::Other, LinkTarget::Page),
};
PageLink { rect, target }
})
.collect();
self.doc.note(&diags);
links
}
#[cfg(feature = "markdown")]
#[must_use]
pub fn markdown(&self) -> String {
pdfrum_markdown::render(&self.markdown_blocks())
}
#[cfg(feature = "markdown")]
#[must_use]
pub fn markdown_blocks(&self) -> Vec<crate::Block> {
let (graph, tree, options, diags) = self.markdown_inputs(WITH_IMAGE_PLACES);
let blocks = pdfrum_markdown::page_blocks(
&graph,
tree.as_ref(),
&self.doc.inner,
options,
&self.doc.limits,
);
self.doc.note(&diags);
blocks
}
#[cfg(feature = "markdown")]
#[must_use]
pub fn layout_text(&self) -> String {
let (graph, _, options, diags) =
self.markdown_inputs(pdfrum_page::RequestedSize::NoSamples);
let text = pdfrum_markdown::page_layout(&graph, &self.doc.inner, options, &self.doc.limits);
self.doc.note(&diags);
text
}
#[cfg(feature = "markdown")]
fn markdown_inputs(
&self,
images: pdfrum_page::RequestedSize,
) -> (
pdfrum_page::Page,
Option<pdfrum_doc::structure::StructTree>,
pdfrum_markdown::Options,
Diagnostics,
) {
let mut ctx = self.context();
ctx.decode_target = images;
let graph = self.build(&mut ctx);
let mut diags = Diagnostics::default();
let catalog = self.doc.catalog();
let tree = pdfrum_doc::structure::StructTree::load_page(
&catalog,
&self.dict.dict,
self.dict.reference.map_or(0, |r| r.num),
&self.doc.inner,
&self.doc.limits,
&mut diags,
);
let options = pdfrum_markdown::Options {
rtl: self.doc.reads_right_to_left(),
};
(graph, tree, options, diags)
}
#[must_use]
pub fn images(&self) -> Vec<PageImage> {
let mut ctx = self.context();
let graph = self.build(&mut ctx);
let mut out = Vec::new();
collect_images(&graph.objects, &self.doc.inner, &mut out);
out
}
#[must_use]
pub fn structure(&self) -> Option<pdfrum_doc::structure::StructTree> {
let mut diags = Diagnostics::default();
let catalog = self.doc.catalog();
let tree = pdfrum_doc::structure::StructTree::load_page(
&catalog,
&self.dict.dict,
self.dict.reference.map_or(0, |r| r.num),
&self.doc.inner,
&self.doc.limits,
&mut diags,
);
self.doc.note(&diags);
tree
}
#[must_use]
pub fn objects(&self) -> pdfrum_page::Page {
self.build(&mut self.context())
}
fn context(&self) -> BuildContext {
let mut ctx = BuildContext::new();
ctx.fonts = self.doc.fonts();
ctx
}
fn build(&self, ctx: &mut BuildContext) -> pdfrum_page::Page {
use crate::profile::{Stage, stage};
let mut diags = Diagnostics::default();
let ops = stage(Stage::ContentParse, || {
let bytes = self.content_bytes(&mut diags);
pdfrum_page::parse_content(&bytes, &self.doc.limits, &mut diags)
});
let resources = pdfrum_page::Resources::for_page(
self.dict
.inherited(&Name::from("Resources"), &self.doc.inner)
.and_then(|object| object.resolve(&self.doc.inner).ok()?.as_dict().cloned()),
);
let page = stage(Stage::Interpretation, || {
pdfrum_page::build_page_from_dict(
&ops,
&self.dict.dict,
|key| self.dict.inherited(key, &self.doc.inner),
&resources,
&self.doc.inner,
ctx,
&self.doc.limits,
&mut diags,
)
});
self.doc.note(&diags);
page
}
fn content_bytes(&self, diags: &mut Diagnostics) -> Vec<u8> {
self.content_segments(diags).0
}
fn content_segments(&self, diags: &mut Diagnostics) -> (Vec<u8>, Vec<usize>) {
content_segments(self.doc, &self.dict, &self.doc.inner, diags)
}
#[cfg(feature = "edit")]
pub(crate) fn build_for_edit(&self, ctx: &mut BuildContext) -> pdfrum_page::Page {
build_graph(self.doc, &self.dict, &self.doc.inner, ctx)
}
}
fn content_segments(
doc: &Document,
dict: &PageDict,
r: &impl Resolve,
diags: &mut Diagnostics,
) -> (Vec<u8>, Vec<usize>) {
let Some(contents) = dict.dict.get(&Name::from("Contents"), r) else {
return (Vec::new(), Vec::new());
};
let mut out = Vec::new();
let mut ends = Vec::new();
let mut push = |object: &pdfrum_object::Object, out: &mut Vec<u8>, ends: &mut Vec<usize>| {
if let Some(stream) = object.as_stream() {
let decoded = pdfrum_filters::decode_chain(stream, 0, r, &doc.limits, diags);
out.extend_from_slice(&decoded.data);
out.push(b' ');
}
ends.push(out.len());
};
let Some(direct) = contents.as_direct() else {
return (out, ends);
};
match direct {
pdfrum_object::Object::Stream(_) => push(direct, &mut out, &mut ends),
pdfrum_object::Object::Array(array) => {
for element in array.iter() {
if let Ok(resolved) = element.resolve(r) {
push(resolved.get(), &mut out, &mut ends);
} else {
ends.push(out.len());
}
}
}
_ => {}
}
(out, ends)
}
#[cfg(feature = "edit")]
pub(crate) fn build_graph(
doc: &Document,
dict: &PageDict,
r: &impl Resolve,
ctx: &mut BuildContext,
) -> pdfrum_page::Page {
let mut diags = Diagnostics::default();
let (bytes, ends) = content_segments(doc, dict, r, &mut diags);
let ops = pdfrum_page::parse_content(&bytes, &doc.limits, &mut diags);
let bounds = pdfrum_page::StreamBounds::from_joined(&bytes, ops.len(), &ends, &doc.limits);
let resources = pdfrum_page::Resources::for_page(
dict.inherited(&Name::from("Resources"), r)
.and_then(|object| object.resolve(r).ok()?.as_dict().cloned()),
);
let page = pdfrum_page::build_page_streams(
&ops,
&bounds,
&dict.dict,
|key| dict.inherited(key, r),
&resources,
r,
ctx,
&doc.limits,
&mut diags,
);
doc.note(&diags);
page
}
#[derive(Debug)]
pub struct PreparedPage<'a> {
doc: &'a Document,
index: PageIndex,
graph: pdfrum_page::Page,
options: RenderOptions,
}
impl PreparedPage<'_> {
pub fn render<B: RasterBackend>(&self, backend: &B) -> Result<Pixmap> {
self.render_on(backend, &mut RenderSession::default())
}
pub fn render_on<B: RasterBackend>(
&self,
backend: &B,
session: &mut RenderSession,
) -> Result<Pixmap> {
check_pixel_cap(&self.doc.limits, self.graph.display_size(), &self.options)?;
let inner = self.options.to_inner();
let mut diags = Diagnostics::default();
let render_session = pdfrum_render::RenderSession {
caches: Some(&mut session.caches),
deadline: self.doc.limits.deadline.as_ref(),
..Default::default()
};
let pixmap = crate::profile::stage(crate::profile::Stage::Raster, || {
pdfrum_render::render_page_with(
&self.graph,
&inner,
backend,
render_session,
&mut diags,
)
});
self.doc.note(&diags);
pixmap.map_err(|error| match error {
pdfrum_render::Error::Limit(limit) => Error::Limit(limit.on_page(self.index)),
other => Error::Render(other),
})
}
#[cfg(feature = "svg-export")]
pub fn to_svg<B: RasterBackend>(&self, backend: &B) -> Result<crate::svg::SvgPage> {
self.to_svg_on(backend, &mut RenderSession::default())
}
#[cfg(feature = "svg-export")]
pub fn to_svg_on<B: RasterBackend>(
&self,
backend: &B,
session: &mut RenderSession,
) -> Result<crate::svg::SvgPage> {
check_pixel_cap(&self.doc.limits, self.graph.display_size(), &self.options)?;
let inner = self.options.to_inner();
let mut diags = Diagnostics::default();
let render_session = pdfrum_render::RenderSession {
caches: Some(&mut session.caches),
deadline: self.doc.limits.deadline.as_ref(),
..Default::default()
};
let converted =
pdfrum_svg::page_to_svg_with(&self.graph, &inner, backend, render_session, &mut diags);
self.doc.note(&diags);
converted.map_err(|error| match error {
pdfrum_render::Error::Limit(limit) => Error::Limit(limit.on_page(self.index)),
other => Error::Render(other),
})
}
}
fn device_box(display_size: (f64, f64), options: &RenderOptions) -> kurbo::Rect {
let (pw, ph) = display_size;
options
.transform
.transform_rect_bbox(kurbo::Rect::new(0.0, 0.0, pw, ph))
}
fn check_pixel_cap(
limits: &Limits,
display_size: (f64, f64),
options: &RenderOptions,
) -> Result<()> {
let Some(allowed) = limits.max_render_pixels else {
return Ok(());
};
let corners = device_box(display_size, options);
#[expect(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
reason = "the engine's own truncation: `as u32` saturates, so a huge, \
negative or NaN axis becomes a reported number rather than \
wrapping"
)]
let (width, height) = (
corners.width().trunc() as u32,
corners.height().trunc() as u32,
);
let asked = u64::from(width) * u64::from(height);
if asked > allowed {
return Err(LimitExceeded::RenderPixels {
width,
height,
allowed,
}
.into());
}
Ok(())
}
#[cfg(feature = "markdown")]
const WITH_IMAGE_PLACES: pdfrum_page::RequestedSize = pdfrum_page::RequestedSize::Reduced {
width: 1,
height: 1,
};
#[cfg(feature = "markdown")]
impl Document {
#[must_use]
pub fn markdown(&self) -> String {
self.markdown_pages().join("\n---\n\n")
}
#[must_use]
pub fn markdown_pages(&self) -> Vec<String> {
let loadable: Vec<PageIndex> = (0..self.page_count())
.map(PageIndex::from)
.filter(|&index| self.page(index).is_ok())
.collect();
let mut out: Vec<String> = (0..self.page_count()).map(|_| String::new()).collect();
if let Ok(pages) = self.markdown_blocks(loadable.iter().copied()) {
for (index, blocks) in loadable.iter().zip(&pages) {
let at = usize::try_from(u32::from(*index)).unwrap_or(usize::MAX);
if let Some(slot) = out.get_mut(at) {
*slot = pdfrum_markdown::render(blocks);
}
}
}
out
}
pub fn markdown_blocks(
&self,
pages: impl IntoIterator<Item = PageIndex>,
) -> Result<Vec<Vec<crate::Block>>> {
let pages: Vec<Page<'_>> = pages
.into_iter()
.map(|index| self.page(index))
.collect::<Result<_>>()?;
let inputs: Vec<_> = pages
.iter()
.map(|page| page.markdown_inputs(WITH_IMAGE_PLACES))
.collect();
let options = pdfrum_markdown::Options {
rtl: self.reads_right_to_left(),
};
let page_inputs: Vec<pdfrum_markdown::PageInput<'_>> = inputs
.iter()
.map(|(graph, tree, _, _)| pdfrum_markdown::PageInput {
page: graph,
tree: tree.as_ref(),
})
.collect();
let blocks =
pdfrum_markdown::document_blocks(&page_inputs, &self.inner, options, &self.limits);
for (_, _, _, diags) in &inputs {
self.note(diags);
}
Ok(blocks)
}
}
impl Document {
pub(crate) fn reads_right_to_left(&self) -> bool {
let Some(prefs) = self
.catalog()
.dict(&Name::from("ViewerPreferences"), &self.inner)
else {
return false;
};
prefs
.name(&Name::from("Direction"))
.map(pdfrum_object::Name::as_bytes)
== Some(b"R2L".as_slice())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum LinkTarget {
Page(PageIndex),
Uri(String),
Other,
}
#[derive(Debug, Clone, PartialEq)]
pub struct PageLink {
pub rect: kurbo::Rect,
pub target: LinkTarget,
}
#[derive(Debug, Clone)]
pub struct PageImage {
pub source: Option<pdfrum_object::ObjRef>,
pub width: u32,
pub height: u32,
pub is_mask: bool,
pub raw: Option<RawImage>,
image: std::sync::Arc<pdfrum_page::ImageData>,
}
impl PageImage {
#[must_use]
pub fn pixmap(&self) -> Pixmap {
pdfrum_render::image_to_pixmap(&self.image)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RawImage {
pub data: Vec<u8>,
pub encoding: ImageEncoding,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ImageEncoding {
Jpeg,
Jpeg2000,
Jbig2,
CcittFax,
}
impl ImageEncoding {
#[must_use]
pub fn extension(self) -> &'static str {
match self {
Self::Jpeg => "jpg",
Self::Jpeg2000 => "jp2",
Self::Jbig2 => "jb2",
Self::CcittFax => "ccitt",
}
}
#[must_use]
pub fn name(self) -> &'static str {
match self {
Self::Jpeg => "jpeg",
Self::Jpeg2000 => "jpeg2000",
Self::Jbig2 => "jbig2",
Self::CcittFax => "ccittfax",
}
}
fn of_filter(name: &[u8]) -> Option<Self> {
match name {
b"DCTDecode" | b"DCT" => Some(Self::Jpeg),
b"JPXDecode" => Some(Self::Jpeg2000),
b"JBIG2Decode" => Some(Self::Jbig2),
b"CCITTFaxDecode" | b"CCF" => Some(Self::CcittFax),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error("not an image encoding: {0}")]
pub struct UnknownImageEncoding(String);
impl std::fmt::Display for ImageEncoding {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.name())
}
}
impl std::str::FromStr for ImageEncoding {
type Err = UnknownImageEncoding;
fn from_str(s: &str) -> core::result::Result<ImageEncoding, UnknownImageEncoding> {
match s {
"jpeg" => Ok(ImageEncoding::Jpeg),
"jpeg2000" => Ok(ImageEncoding::Jpeg2000),
"jbig2" => Ok(ImageEncoding::Jbig2),
"ccittfax" => Ok(ImageEncoding::CcittFax),
other => Err(UnknownImageEncoding(other.to_owned())),
}
}
}
fn collect_images(
objects: &[pdfrum_page::PageObject],
resolver: &pdfrum_parser::Document,
out: &mut Vec<PageImage>,
) {
for object in objects {
match object {
pdfrum_page::PageObject::Image(content) => {
let image = &content.object;
let raw = image.source.and_then(|r| raw_image(r, resolver));
out.push(PageImage {
source: image.source,
width: image.image.width,
height: image.image.height,
is_mask: image.is_mask,
raw,
image: std::sync::Arc::clone(&image.image),
});
}
pdfrum_page::PageObject::Form(content) => {
collect_images(&content.object.objects, resolver, out);
}
_ => {}
}
}
}
fn raw_image(
reference: pdfrum_object::ObjRef,
resolver: &pdfrum_parser::Document,
) -> Option<RawImage> {
let object = resolver.fetch(reference).ok()?;
let stream = object.as_stream()?;
let filters: Vec<Name> = match stream.dict.get(names::FILTER, resolver)?.get() {
pdfrum_object::Object::Name(n) => vec![n.clone()],
pdfrum_object::Object::Array(a) => a.iter().filter_map(|o| o.as_name().cloned()).collect(),
_ => return None,
};
let [only] = filters.as_slice() else {
return None;
};
let encoding = ImageEncoding::of_filter(only.as_bytes())?;
Some(RawImage {
data: stream.data.as_ref().to_vec(),
encoding,
})
}