From 4322f104fcde28977c585ffa7c39a002d1029c3b Mon Sep 17 00:00:00 2001 From: Velli20 Date: Sun, 18 Oct 2026 21:55:05 +0300 Subject: [PATCH] Render web page content through PdfRenderer::render_with_text_layout Remove the free function render_page_content_with_text_layout. The renderer method now takes the page viewport and optional content state, so the web page renderer owns a PdfRenderer and uses the same path as the Skia example and the conformance harness. Co-Authored-By: Claude Opus 5.5 --- Cargo.lock | 1 - crates/pdf-renderer/src/lib.rs | 55 ++++++++++++---------------- crates/pdf-web/Cargo.toml | 1 - crates/pdf-web/src/error.rs | 3 ++ crates/pdf-web/src/page_renderer.rs | 44 ++++++++++++---------- examples/skia.rs | 18 ++++++--- examples/web-canvas/renderer.rs | 20 +++++----- tools/conformance/src/safe_render.rs | 12 +++++- 8 files changed, 85 insertions(+), 69 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 99cb2dd5..8891cb5c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2534,7 +2534,6 @@ dependencies = [ "pdf-graphics-web", "pdf-optional-content", "pdf-renderer", - "pdf-text-engine", "serde", "serde_json", "thiserror 2.0.12", diff --git a/crates/pdf-renderer/src/lib.rs b/crates/pdf-renderer/src/lib.rs index 99cd26bd..6f09809e 100644 --- a/crates/pdf-renderer/src/lib.rs +++ b/crates/pdf-renderer/src/lib.rs @@ -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; @@ -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( - backend: &mut B, - page: &pdf_document::page::PdfPage, - viewport: &pdf_canvas::PageViewport, - fonts: Arc, - optional_content: &OptionalContentState, -) -> Result { - 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 { @@ -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))? @@ -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( &self, canvas_backend: &mut B, page_index: usize, + viewport: &PageViewport, + optional_content: &OptionalContentState, ) -> Result { 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. @@ -196,7 +185,7 @@ impl PdfRenderer { ) -> Result { 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))? @@ -214,7 +203,7 @@ 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() @@ -222,7 +211,11 @@ impl PdfRenderer { .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)); }; diff --git a/crates/pdf-web/Cargo.toml b/crates/pdf-web/Cargo.toml index 976e1bac..dbbef14a 100644 --- a/crates/pdf-web/Cargo.toml +++ b/crates/pdf-web/Cargo.toml @@ -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" diff --git a/crates/pdf-web/src/error.rs b/crates/pdf-web/src/error.rs index cc71ce3d..71c95381 100644 --- a/crates/pdf-web/src/error.rs +++ b/crates/pdf-web/src/error.rs @@ -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), diff --git a/crates/pdf-web/src/page_renderer.rs b/crates/pdf-web/src/page_renderer.rs index 2c2da79d..f4c54e80 100644 --- a/crates/pdf-web/src/page_renderer.rs +++ b/crates/pdf-web/src/page_renderer.rs @@ -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. @@ -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, + renderer: PdfRenderer, } impl WebPageRenderer { - /// Uses the application's shared font system for page content. - pub fn new(fonts: Arc) -> 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. /// @@ -51,7 +55,6 @@ impl WebPageRenderer { pub fn render( &self, page_index: u32, - page: &PdfPage, content_revision: u32, backend: &mut WebCanvasBackend, viewport: &WebViewport, @@ -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()?; @@ -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() @@ -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) } } diff --git a/examples/skia.rs b/examples/skia.rs index 446de921..bea85537 100644 --- a/examples/skia.rs +++ b/examples/skia.rs @@ -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}; @@ -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}")] @@ -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)?; } diff --git a/examples/web-canvas/renderer.rs b/examples/web-canvas/renderer.rs index 49cda488..465fd972 100644 --- a/examples/web-canvas/renderer.rs +++ b/examples/web-canvas/renderer.rs @@ -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::{ @@ -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, @@ -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::>()).map_err(error)?, @@ -49,7 +50,7 @@ impl WebDocument { /// Number of pages in this document. pub fn page_count(&self) -> Result { - 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`. @@ -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. @@ -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(); @@ -88,7 +89,6 @@ impl WebDocument { .renderer .render( page_index, - page, self.content_revision, &mut backend, &viewport, @@ -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 diff --git a/tools/conformance/src/safe_render.rs b/tools/conformance/src/safe_render.rs index a8676773..ee8ce3dc 100644 --- a/tools/conformance/src/safe_render.rs +++ b/tools/conformance/src/safe_render.rs @@ -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}; @@ -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)) }