Skip to content

Commit e47b3d7

Browse files
committed
fix: add skip_images flag to Rust renderer — thumbnails skip image decoding (17s → ms per page)
1 parent a61d6cb commit e47b3d7

4 files changed

Lines changed: 61 additions & 10 deletions

File tree

open-pdf-render/src/interpreter.rs

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -111,12 +111,36 @@ impl TextState {
111111
pub struct Interpreter;
112112

113113
impl Interpreter {
114+
/// Execute content stream, rendering all content including images.
114115
pub fn execute(
115116
content_bytes: &[u8],
116117
renderer: &mut SkiaRenderer,
117118
state: &mut GraphicsStateStack,
118119
doc: &Document,
119120
resources: &Dictionary,
121+
) -> Result<(), RenderError> {
122+
Self::execute_internal(content_bytes, renderer, state, doc, resources, false)
123+
}
124+
125+
/// Execute content stream but skip image XObjects. Used for fast
126+
/// thumbnail rendering where image decoding would take seconds.
127+
pub fn execute_skip_images(
128+
content_bytes: &[u8],
129+
renderer: &mut SkiaRenderer,
130+
state: &mut GraphicsStateStack,
131+
doc: &Document,
132+
resources: &Dictionary,
133+
) -> Result<(), RenderError> {
134+
Self::execute_internal(content_bytes, renderer, state, doc, resources, true)
135+
}
136+
137+
fn execute_internal(
138+
content_bytes: &[u8],
139+
renderer: &mut SkiaRenderer,
140+
state: &mut GraphicsStateStack,
141+
doc: &Document,
142+
resources: &Dictionary,
143+
skip_images: bool,
120144
) -> Result<(), RenderError> {
121145
let content = Content::decode(content_bytes)
122146
.map_err(|e| RenderError::ParseError(format!("Content decode: {}", e)))?;
@@ -198,7 +222,7 @@ impl Interpreter {
198222
"W" | "W*" => {}
199223
"BT" | "ET" | "Tf" | "Td" | "TD" | "Tm" | "Tj" | "TJ" | "T*" | "'" | "\"" | "Tc" | "Tw" | "Tz" | "TL" | "Ts" | "Tr" => {}
200224
"Do" => {
201-
Self::handle_do_execute(&op.operands, renderer, state, doc, resources);
225+
Self::handle_do_execute(&op.operands, renderer, state, doc, resources, skip_images);
202226
}
203227
"gs" | "ri" | "i" => {}
204228
_ => {}
@@ -213,6 +237,7 @@ impl Interpreter {
213237
state: &mut GraphicsStateStack,
214238
doc: &Document,
215239
resources: &Dictionary,
240+
skip_images: bool,
216241
) {
217242
let name = match operands.first() {
218243
Some(Object::Name(n)) => n,
@@ -240,7 +265,9 @@ impl Interpreter {
240265
};
241266
let subtype = stream.dict.get(b"Subtype").ok().and_then(|s| s.as_name().ok());
242267
if subtype == Some(b"Image" as &[u8]) {
243-
Self::handle_image_execute(stream, renderer, state, doc);
268+
if !skip_images {
269+
Self::handle_image_execute(stream, renderer, state, doc);
270+
}
244271
return;
245272
}
246273
if subtype != Some(b"Form" as &[u8]) {
@@ -261,7 +288,7 @@ impl Interpreter {
261288
let form_resources = Self::extract_form_resources(&stream.dict, doc);
262289
let res = form_resources.as_ref().unwrap_or(resources);
263290
if let Ok(content_bytes) = stream.decompressed_content() {
264-
let _ = Self::execute(&content_bytes, renderer, state, doc, res);
291+
let _ = Self::execute_internal(&content_bytes, renderer, state, doc, res, skip_images);
265292
}
266293
state.restore();
267294
}

open-pdf-render/src/parser.rs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,17 @@ impl DocumentHandle {
4141
/// app (e.g. user-applied rotation via the rotate-left/right buttons).
4242
/// Both rotations are clockwise-when-displayed, in degrees.
4343
pub fn render_page(&self, page: usize, scale: f32, extra_rotation: i32) -> Result<RenderedPage, RenderError> {
44+
self.render_page_internal(page, scale, extra_rotation, false)
45+
}
46+
47+
/// Render a page without decoding embedded images. Produces vector-only
48+
/// output suitable for thumbnails — runs in milliseconds instead of
49+
/// seconds for image-heavy pages.
50+
pub fn render_page_no_images(&self, page: usize, scale: f32, extra_rotation: i32) -> Result<RenderedPage, RenderError> {
51+
self.render_page_internal(page, scale, extra_rotation, true)
52+
}
53+
54+
fn render_page_internal(&self, page: usize, scale: f32, extra_rotation: i32, skip_images: bool) -> Result<RenderedPage, RenderError> {
4455
let page_id = self.get_page_id(page)?;
4556
let (x0, y0, w_pt, h_pt) = self.extract_media_box_full(page_id)?;
4657

@@ -78,7 +89,11 @@ impl DocumentHandle {
7889

7990
let content_bytes = self.get_content_stream(page_id)?;
8091
let resources = self.get_page_resources(page_id)?;
81-
crate::interpreter::Interpreter::execute(&content_bytes, &mut renderer, &mut state, &self.doc, &resources)?;
92+
if skip_images {
93+
crate::interpreter::Interpreter::execute_skip_images(&content_bytes, &mut renderer, &mut state, &self.doc, &resources)?;
94+
} else {
95+
crate::interpreter::Interpreter::execute(&content_bytes, &mut renderer, &mut state, &self.doc, &resources)?;
96+
}
8297

8398
Ok(RenderedPage { width, height, rgba: renderer.into_rgba() })
8499
}

open-pdf-studio/js/ui/panels/left-panel.js

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -413,22 +413,24 @@ async function renderThumbnailToDataURL(pdfDoc, pageNum) {
413413
if (!pdfDoc || pageNum > pdfDoc.numPages) return null;
414414
const _th0 = performance.now();
415415

416-
// Try Rust thumbnail rendering first (10-50ms vs 5-10 sec with PDF.js)
416+
// Try Rust thumbnail rendering first — uses skip_images=true so only
417+
// vector content is rendered (fast). Image decoding is skipped because
418+
// it can take 17+ seconds per page for complex PDFs, blocking the Rust
419+
// backend and preventing page navigation.
417420
const doc = getActiveDocument();
418421
if (doc?.filePath && window.__TAURI__) {
419422
try {
420-
console.log(`[PERF-THUMB] page ${pageNum}: Rust render START`);
421423
const { invoke } = window.__TAURI__.core;
422424
const result = await invoke('render_thumbnail', {
423425
path: doc.filePath,
424426
pageIndex: pageNum - 1,
425427
maxWidth: 200,
428+
skipImages: true,
426429
});
427430
const data = JSON.parse(result);
428-
console.log(`[PERF-THUMB] page ${pageNum}: Rust render DONE: ${(performance.now() - _th0).toFixed(0)}ms`);
429431
return { dataURL: data.dataURL, width: data.width, height: data.height };
430432
} catch (e) {
431-
console.warn(`[PERF-THUMB] page ${pageNum}: Rust render FAILED (${(performance.now() - _th0).toFixed(0)}ms):`, e);
433+
console.warn(`[Thumbnails] Rust render failed for page ${pageNum}:`, e);
432434
// Fall through to PDF.js fallback
433435
}
434436
}

open-pdf-studio/src-tauri/src/lib.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -911,11 +911,13 @@ fn render_thumbnail(
911911
page_index: u32,
912912
max_width: u32,
913913
rotation: Option<i32>,
914+
skip_images: Option<bool>,
914915
bytes_cache: tauri::State<PdfBytesCache>,
915916
handle_cache: tauri::State<DocHandleCache>,
916917
) -> Result<String, String> {
917918
let doc = get_or_load_doc(&path, &bytes_cache, &handle_cache)?;
918919
let extra_rot = rotation.unwrap_or(0);
920+
let skip_img = skip_images.unwrap_or(false);
919921

920922
// Get page dimensions to calculate thumbnail scale
921923
let (w_pt, h_pt) = doc.page_dimensions(page_index as usize)
@@ -924,8 +926,13 @@ fn render_thumbnail(
924926
// Scale so the longest side fits within max_width pixels
925927
let scale = max_width as f32 / w_pt.max(h_pt);
926928

927-
// Render at thumbnail scale
928-
let page = doc.render_page(page_index as usize, scale, extra_rot).map_err(|e| format!("{}", e))?;
929+
// Render at thumbnail scale — skip_images=true skips heavy image
930+
// decoding so thumbnails render in milliseconds instead of seconds.
931+
let page = if skip_img {
932+
doc.render_page_no_images(page_index as usize, scale, extra_rot)
933+
} else {
934+
doc.render_page(page_index as usize, scale, extra_rot)
935+
}.map_err(|e| format!("{}", e))?;
929936

930937
// Convert RGBA to RGB (JPEG doesn't support alpha)
931938
let pixel_count = (page.width * page.height) as usize;

0 commit comments

Comments
 (0)