Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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: 0 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

55 changes: 24 additions & 31 deletions crates/pdf-renderer/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use pdf_canvas::{
canvas_backend::CanvasBackend, error::PdfCanvasError, pdf_canvas::PdfCanvas,
PageViewport, canvas_backend::CanvasBackend, error::PdfCanvasError, pdf_canvas::PdfCanvas,
recording_canvas::RecordingCanvas,
};
use pdf_document::document::PdfDocument;
Expand All @@ -18,25 +18,6 @@ pub use text_selection::{
TextGlyph, TextHit, TextSelection, TextSelectionError, TextSelectionResult,
};

/// Renders page content and captures text using a shared viewport, excluding annotations.
/// Content in optional content groups that are off in `optional_content` is hidden.
/// Backend lifecycle and any separate annotation pass remain the caller's responsibility.
pub fn render_page_content_with_text_layout<B: CanvasBackend>(
backend: &mut B,
page: &pdf_document::page::PdfPage,
viewport: &pdf_canvas::PageViewport,
fonts: Arc<FontSystem>,
optional_content: &OptionalContentState,
) -> Result<PageTextLayout, PdfCanvasError> {
let mut canvas = PdfCanvas::new_with_viewport(backend, page, viewport, fonts)?
.with_text_recording()
.with_optional_content(optional_content);
if let Some(content) = &page.contents {
canvas.render_content_stream(content, None, None, page.resources.clone(), None)?;
}
Ok(PageTextLayout::new(canvas.take_text_glyphs()))
}

/// A page's drawing commands and selectable text captured in one render pass.
#[derive(Clone)]
pub struct RecordedPage {
Expand Down Expand Up @@ -154,7 +135,7 @@ impl PdfRenderer {
page_index: usize,
) -> Result<(), PdfRendererError> {
let page = self.page(page_index)?;
let optional_content = self.optional_content();
let optional_content = self.default_optional_content();
{
let mut canvas =
PdfCanvas::new(canvas_backend, page, None, Arc::clone(&self.font_system))?
Expand All @@ -166,22 +147,30 @@ impl PdfRenderer {
Ok(())
}

/// Renders a page and returns selectable text captured during the same pass.
/// Renders page content and captures selectable text in the same pass, mapping the page
/// through `viewport`. Annotations are excluded, and content in optional content groups
/// that are off in `optional_content` is hidden.
/// Backend lifecycle and any separate annotation pass remain the caller's responsibility.
pub fn render_with_text_layout<B: CanvasBackend>(
&self,
canvas_backend: &mut B,
page_index: usize,
viewport: &PageViewport,
optional_content: &OptionalContentState,
) -> Result<PageTextLayout, PdfRendererError> {
let page = self.page(page_index)?;
let optional_content = self.optional_content();
let mut canvas = PdfCanvas::new(canvas_backend, page, None, Arc::clone(&self.font_system))?
.with_text_recording()
.with_optional_content(&optional_content);
let mut canvas = PdfCanvas::new_with_viewport(
canvas_backend,
page,
viewport,
Arc::clone(&self.font_system),
)?
.with_text_recording()
.with_optional_content(optional_content);
if let Some(cs) = &page.contents {
canvas.render_content_stream(cs, None, None, page.resources.clone(), None)?;
}
let glyphs = canvas.take_text_glyphs();
Ok(PageTextLayout::new(glyphs))
Ok(PageTextLayout::new(canvas.take_text_glyphs()))
}

/// Records page content and text for caching, excluding native annotations.
Expand All @@ -196,7 +185,7 @@ impl PdfRenderer {
) -> Result<RecordedPage, PdfRendererError> {
let page = self.page(page_index)?;
let mut recording = RecordingCanvas::new(width, height);
let optional_content = self.optional_content();
let optional_content = self.default_optional_content();
let glyphs = {
let mut canvas =
PdfCanvas::new(&mut recording, page, None, Arc::clone(&self.font_system))?
Expand All @@ -214,15 +203,19 @@ impl PdfRenderer {
}

/// Returns the visibility the document's default optional content configuration sets.
fn optional_content(&self) -> OptionalContentState {
pub fn default_optional_content(&self) -> OptionalContentState {
self.document
.optional_content
.as_ref()
.map(OptionalContentState::from_properties)
.unwrap_or_default()
}

fn page(&self, page_index: usize) -> Result<&pdf_document::page::PdfPage, PdfRendererError> {
/// Returns the page at zero-based `page_index`, or [`PdfRendererError::PageNotFound`].
pub fn page(
&self,
page_index: usize,
) -> Result<&pdf_document::page::PdfPage, PdfRendererError> {
let Some(page) = self.document.pages.get(page_index) else {
return Err(PdfRendererError::PageNotFound(page_index));
};
Expand Down
1 change: 0 additions & 1 deletion crates/pdf-web/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ pdf-graphics = { path = "../pdf-graphics" }
pdf-graphics-web = { path = "../pdf-graphics-web" }
pdf-document = { path = "../pdf-document", features = ["serde"] }
pdf-renderer = { path = "../pdf-renderer" }
pdf-text-engine = { path = "../pdf-text-engine" }
serde_json = "1"
serde = { version = "1", features = ["derive"] }
thiserror = "2.0.12"
Expand Down
3 changes: 3 additions & 0 deletions crates/pdf-web/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ pub enum WebError {
/// Host coordinate conversion failed.
#[error(transparent)]
Transform(#[from] pdf_graphics::transform::TransformError),
/// Page lookup or rendering failed.
#[error(transparent)]
Renderer(#[from] pdf_renderer::PdfRendererError),
/// A renderer-owned text-selection operation failed.
#[error(transparent)]
TextSelection(#[from] pdf_renderer::TextSelectionError),
Expand Down
44 changes: 24 additions & 20 deletions crates/pdf-web/src/page_renderer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,10 @@ use crate::{
annotation_overlay::{PreparedAnnotationOverlay, WebAnnotationOverlay},
error::{WebError as Error, WebResult},
};
use pdf_document::page::PdfPage;
use pdf_document::document::PdfDocument;
use pdf_graphics::size::Size;
use pdf_graphics_web::WebCanvasBackend;
use pdf_renderer::PageTextLayout;
use pdf_text_engine::FontSystem;
use pdf_renderer::{PageTextLayout, PdfRenderer};
use std::sync::Arc;

/// Results of one page-content pass and independently prepared annotation presentation.
Expand All @@ -24,24 +23,29 @@ pub struct WebPageOutput {
pub annotations: PreparedAnnotationOverlay,
}

/// Thin orchestration around PdfCanvas; no new content-stream or font rendering engine.
/// Thin orchestration around PdfRenderer; no new content-stream or font rendering engine.
pub struct WebPageRenderer {
fonts: Arc<FontSystem>,
renderer: PdfRenderer,
}

impl WebPageRenderer {
/// Uses the application's shared font system for page content.
pub fn new(fonts: Arc<FontSystem>) -> Self {
Self { fonts }
/// Renders page content of the document `renderer` owns.
pub fn new(renderer: PdfRenderer) -> Self {
Self { renderer }
}

/// Renders only page content via PdfCanvas, collects its existing TextGlyph output,
/// Returns the document whose pages this renderer draws.
pub fn document(&self) -> &PdfDocument {
self.renderer.document()
}

/// Renders only page content via PdfRenderer, collects its existing TextGlyph output,
/// and prepares annotations separately through the supplied overlay layer.
///
/// Uses the shared content-and-text pass; annotation preparation produces native
/// models only. Both passes use the supplied viewport.
/// Finishes with balanced backend state, retaining no backend borrow in the output.
/// `page_index` selects the Core page in `overlay`; `page` supplies PDF content.
/// `page_index` selects both the document page and the Core page in `overlay`.
/// `content_revision` identifies the text layout independently of annotation edits.
/// `backend` and `viewport` must describe the same device layout.
///
Expand All @@ -51,7 +55,6 @@ impl WebPageRenderer {
pub fn render(
&self,
page_index: u32,
page: &PdfPage,
content_revision: u32,
backend: &mut WebCanvasBackend,
viewport: &WebViewport,
Expand All @@ -65,11 +68,11 @@ impl WebPageRenderer {
));
}
let result = (|| {
let text_layout = Arc::new(pdf_renderer::render_page_content_with_text_layout(
let index = usize::try_from(page_index).map_err(|_| Error::ResourceLimit)?;
let text_layout = Arc::new(self.renderer.render_with_text_layout(
backend,
page,
index,
viewport.page(),
Arc::clone(&self.fonts),
overlay.optional_content(),
)?);
backend.finish_page()?;
Expand Down Expand Up @@ -103,12 +106,13 @@ mod tests {
WebPageOutput,
WebCanvasBackend,
WebAnnotationOverlay,
pdf_document::document::PdfDocument,
WebPageRenderer,
) {
let document = pdf_document::reader::PdfReader
.read_from_bytes(bytes, None)
.unwrap();
let page = document.pages.first().unwrap();
let renderer = WebPageRenderer::new(PdfRenderer::new(document));
let page = renderer.document().pages.first().unwrap();
let canvas = web_sys::window()
.unwrap()
.document()
Expand All @@ -129,10 +133,10 @@ mod tests {
.unwrap();
let mut backend =
WebCanvasBackend::new(canvas, *viewport.canvas(), WebCanvasOptions::default()).unwrap();
let mut overlay = WebAnnotationOverlay::new(&document).unwrap();
let output = WebPageRenderer::new(pdf_text_engine::bundled_font_system())
.render(0, page, 1, &mut backend, &viewport, &mut overlay)
let mut overlay = WebAnnotationOverlay::new(renderer.document()).unwrap();
let output = renderer
.render(0, 1, &mut backend, &viewport, &mut overlay)
.unwrap();
(output, backend, overlay, document)
(output, backend, overlay, renderer)
}
}
18 changes: 13 additions & 5 deletions examples/skia.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use glutin::{
};
use glutin_winit::DisplayBuilder;
use pdf_canvas::canvas_backend::{CanvasBackend, Shader};
use pdf_graphics::{BlendMode, PathFillType, color::Color, pdf_path::PdfPath};
use pdf_graphics::{BlendMode, PathFillType, color::Color, pdf_path::PdfPath, size::Size};
use pdf_graphics_skia::skia_canvas_backend::SkiaCanvasBackend;
use raw_window_handle::HasWindowHandle;
use skia_safe::{Color as SkiaColor, Surface};
Expand Down Expand Up @@ -52,6 +52,8 @@ pub enum AppError {
PdfRendererError(#[from] pdf_renderer::PdfRendererError),
#[error("Failed to draw PDF canvas overlay: {0}")]
PdfCanvasError(#[from] pdf_canvas::error::PdfCanvasError),
#[error("Failed to map PDF page to the window: {0}")]
Viewport(#[from] pdf_canvas::ViewportError),
#[error("Failed to create event loop: {0}")]
EventLoop(#[from] winit::error::EventLoopError),
#[error("Failed to create window: {0}")]
Expand Down Expand Up @@ -145,10 +147,16 @@ impl Application {
height,
};
if self.text_layout.is_none() {
self.text_layout = Some(
self.renderer
.render_with_text_layout(&mut backend, self.current_page)?,
);
let viewport = self
.renderer
.page(self.current_page)?
.viewport(None, Size::new(width, height))?;
self.text_layout = Some(self.renderer.render_with_text_layout(
&mut backend,
self.current_page,
&viewport,
&self.renderer.default_optional_content(),
)?);
} else {
self.renderer.render(&mut backend, self.current_page)?;
}
Expand Down
20 changes: 10 additions & 10 deletions examples/web-canvas/renderer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
use pdf_annotation_core::pdf_data::AnnotationAction;
use pdf_document::{document::PdfDocument, reader::PdfReader};
use pdf_graphics_web::{WebCanvasBackend, WebCanvasOptions};
use pdf_renderer::DocumentTextSelection;
use pdf_renderer::{DocumentTextSelection, PdfRenderer};
use pdf_web::{
WebAnnotationOverlay, WebPageRenderer, WebViewport,
interop::{
Expand All @@ -12,10 +12,9 @@ use pdf_web::{
use std::{collections::BTreeMap, sync::Arc};
use wasm_bindgen::prelude::*;

/// Browser example's document, layouts and overlay resources.
/// Browser example's renderer-owned document, layouts and overlay resources.
#[wasm_bindgen]
pub struct WebDocument {
document: PdfDocument,
renderer: WebPageRenderer,
overlay: WebAnnotationOverlay,
selection: WebSelectionController,
Expand All @@ -34,8 +33,10 @@ impl WebDocument {
let count = u32::try_from(document.pages.len()).map_err(error)?;
let overlay = WebAnnotationOverlay::new(&document).map_err(error)?;
Ok(Self {
document,
renderer: WebPageRenderer::new(pdf_text_engine::bundled_font_system()),
renderer: WebPageRenderer::new(PdfRenderer::new_with_font_system(
document,
pdf_text_engine::bundled_font_system(),
)),
overlay,
selection: WebSelectionController::from_selection(
DocumentTextSelection::new(&(0..count).collect::<Vec<_>>()).map_err(error)?,
Expand All @@ -49,7 +50,7 @@ impl WebDocument {

/// Number of pages in this document.
pub fn page_count(&self) -> Result<u32, JsValue> {
u32::try_from(self.document.pages.len()).map_err(error)
u32::try_from(self.renderer.document().pages.len()).map_err(error)
}

/// Returns the displayed page dimensions in points, honoring CropBox and `/Rotate`.
Expand All @@ -61,7 +62,7 @@ impl WebDocument {
}

fn page(&self, page: u32) -> Result<&pdf_document::page::PdfPage, JsValue> {
page_of(&self.document, page)
page_of(self.renderer.document(), page)
}

/// Renders page pixels and returns independently prepared annotation models.
Expand All @@ -77,7 +78,7 @@ impl WebDocument {
.viewport_revision
.checked_add(1)
.ok_or_else(|| error("viewport revision overflow"))?;
let page = page_of(&self.document, page_index)?;
let page = page_of(self.renderer.document(), page_index)?;
let viewport = WebViewport::for_page(page, zoom, dpr, rotation, self.viewport_revision)
.map_err(error)?;
let size = viewport.page().device_size();
Expand All @@ -88,7 +89,6 @@ impl WebDocument {
.renderer
.render(
page_index,
page,
self.content_revision,
&mut backend,
&viewport,
Expand Down Expand Up @@ -170,7 +170,7 @@ impl WebDocument {
let viewport = page.and_then(|p| self.viewports.get(&p));
let bounds = page
.and_then(|p| usize::try_from(p).ok())
.and_then(|p| self.document.pages.get(p))
.and_then(|p| self.renderer.document().pages.get(p))
.and_then(|p| p.crop_box.or(p.media_box))
.map(|r| r.normalized());
let receipt = self
Expand Down
12 changes: 11 additions & 1 deletion tools/conformance/src/safe_render.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
use crate::{model::ErrorDetail, trace_backend::TraceBackend};
use anyhow::{Result, anyhow, bail};
use image::RgbaImage;
use pdf_canvas::canvas_backend::CanvasBackend;
use pdf_graphics::size::Size;
use pdf_graphics_skia::skia_canvas_backend::SkiaCanvasBackend;
use pdf_renderer::{PdfRenderer, PdfRendererError, RecordedPage, text_selection::PageTextLayout};

Expand Down Expand Up @@ -75,7 +77,15 @@ pub fn trace(
f32::from(u16::try_from(width)?),
f32::from(u16::try_from(height)?),
);
let layout = renderer.render_with_text_layout(&mut backend, page)?;
let viewport = renderer
.page(page)?
.viewport(None, Size::new(backend.width(), backend.height()))?;
let layout = renderer.render_with_text_layout(
&mut backend,
page,
&viewport,
&renderer.default_optional_content(),
)?;
Ok((backend, layout))
}

Expand Down
Loading