image_viewer: Add PDF find in document and internal link jumps
Chrome-parity navigation work for the PDF viewer: - Find in document (ctrl-f): query editor, match counter, Enter/Shift+Enter and ctrl-g navigation, yellow highlights with a stronger tint on the active match, and a page jump per match. - Internal link destinations: pdfium now reports dest_page for GoTo links, so clicking a link that points inside the document jumps to that page instead of being dropped. - Space/Shift+Space scroll the viewport a screen at a time, unless the find bar has focus so the query stays typeable.
This commit is contained in:
1 parent
c8b11be511
commit
818b00d47e
9 files changed
+951
-147
No files matched your search
@@ -1626,6 +1626,9 @@
|
||||
"home": "image_viewer::FirstPage",
|
||||
"end": "image_viewer::LastPage",
|
||||
"ctrl-c": "image_viewer::CopyText",
|
||||
"ctrl-f": "image_viewer::FindInDocument",
|
||||
"ctrl-g": "image_viewer::FindNext",
|
||||
"ctrl-shift-g": "image_viewer::FindPrevious",
|
||||
"ctrl-shift-t": "image_viewer::ToggleTextPanel"
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1678,6 +1678,9 @@
|
||||
"home": "image_viewer::FirstPage",
|
||||
"end": "image_viewer::LastPage",
|
||||
"cmd-c": "image_viewer::CopyText",
|
||||
"cmd-f": "image_viewer::FindInDocument",
|
||||
"cmd-g": "image_viewer::FindNext",
|
||||
"cmd-shift-g": "image_viewer::FindPrevious",
|
||||
"cmd-shift-t": "image_viewer::ToggleTextPanel"
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1601,6 +1601,9 @@
|
||||
"home": "image_viewer::FirstPage",
|
||||
"end": "image_viewer::LastPage",
|
||||
"ctrl-c": "image_viewer::CopyText",
|
||||
"ctrl-f": "image_viewer::FindInDocument",
|
||||
"ctrl-g": "image_viewer::FindNext",
|
||||
"ctrl-shift-g": "image_viewer::FindPrevious",
|
||||
"ctrl-shift-t": "image_viewer::ToggleTextPanel"
|
||||
},
|
||||
},
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -336,12 +336,30 @@ mod tests {
|
||||
let key0 = CacheKey::new(0, 1.0, false);
|
||||
let key1 = CacheKey::new(1, 1.0, false);
|
||||
|
||||
cache.insert(key0, MockCustomPage { id: 100, size: 600_000 });
|
||||
cache.insert(key1, MockCustomPage { id: 101, size: 600_000 });
|
||||
cache.insert(
|
||||
key0,
|
||||
MockCustomPage {
|
||||
id: 100,
|
||||
size: 600_000,
|
||||
},
|
||||
);
|
||||
cache.insert(
|
||||
key1,
|
||||
MockCustomPage {
|
||||
id: 101,
|
||||
size: 600_000,
|
||||
},
|
||||
);
|
||||
|
||||
// Total 1.2 MB > 1 MB, key0 must be evicted
|
||||
assert!(cache.get(&key0).is_none());
|
||||
assert_eq!(cache.get(&key1), Some(MockCustomPage { id: 101, size: 600_000 }));
|
||||
assert_eq!(
|
||||
cache.get(&key1),
|
||||
Some(MockCustomPage {
|
||||
id: 101,
|
||||
size: 600_000
|
||||
})
|
||||
);
|
||||
assert_eq!(cache.len(), 1);
|
||||
assert_eq!(cache.memory_usage(), 600_000);
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ pub mod ui;
|
||||
pub mod view;
|
||||
pub mod watcher;
|
||||
|
||||
pub use cache::{CacheKey, CacheablePage, PageLruCache, RenderedPage, DEFAULT_MEMORY_BUDGET_BYTES};
|
||||
pub use cache::{CacheKey, CacheablePage, DEFAULT_MEMORY_BUDGET_BYTES, PageLruCache, RenderedPage};
|
||||
pub use document::{PageDimensions, PdfDocument};
|
||||
pub use pdfium::{
|
||||
PdfDocumentDetails, PdfLinkAnnotation, PdfPageDetails, PdfTextSegment, PdfiumEngine,
|
||||
@@ -27,5 +27,5 @@ pub use pdfium::{
|
||||
pub use rasterizer::{LuminosityToneMapper, PageRasterizer, RasterizerOptions};
|
||||
pub use settings::{DefaultZoomPolicy, PageLayoutMode, PdfViewerSettings};
|
||||
pub use ui::{PdfToolbarAction, PdfToolbarState};
|
||||
pub use view::{PdfView, PdfViewEvent, MAX_ZOOM, MIN_ZOOM, PAGE_SPACING_PX, ZOOM_STEP};
|
||||
pub use view::{MAX_ZOOM, MIN_ZOOM, PAGE_SPACING_PX, PdfView, PdfViewEvent, ZOOM_STEP};
|
||||
pub use watcher::{PdfReloadDebouncer, ViewerStateSnapshot};
|
||||
@@ -135,7 +135,9 @@ impl PdfiumEngine {
|
||||
}
|
||||
}
|
||||
|
||||
log::warn!("Dynamic libpdfium not found in standard system paths; engine will use synthetic fallback");
|
||||
log::warn!(
|
||||
"Dynamic libpdfium not found in standard system paths; engine will use synthetic fallback"
|
||||
);
|
||||
None
|
||||
}
|
||||
|
||||
@@ -445,7 +447,10 @@ impl PdfiumEngine {
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct PdfLinkAnnotation {
|
||||
/// Target URL for external links; empty for internal (in-document) links.
|
||||
pub url: String,
|
||||
/// Zero-based target page for internal links; `None` for external links.
|
||||
pub dest_page: Option<usize>,
|
||||
pub x: f32,
|
||||
pub y: f32,
|
||||
pub width: f32,
|
||||
@@ -672,6 +677,7 @@ fn extract_page_text_and_links(
|
||||
|
||||
links.push(PdfLinkAnnotation {
|
||||
url: url_clean,
|
||||
dest_page: None,
|
||||
x: norm_x,
|
||||
y: norm_y,
|
||||
width: norm_w,
|
||||
@@ -681,6 +687,32 @@ fn extract_page_text_and_links(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(PdfAction::LocalDestination(dest_action)) = link.action() {
|
||||
if let Ok(dest_page) = dest_action
|
||||
.destination()
|
||||
.and_then(|destination| destination.page_index())
|
||||
{
|
||||
if let Ok(rect) = link.rect() {
|
||||
let left = rect.left().value.min(rect.right().value);
|
||||
let right = rect.left().value.max(rect.right().value);
|
||||
let bottom = rect.bottom().value.min(rect.top().value);
|
||||
let top = rect.bottom().value.max(rect.top().value);
|
||||
|
||||
let (norm_x, norm_y, norm_w, norm_h) =
|
||||
normalize_coordinates(left, right, bottom, top, page_w, page_h);
|
||||
|
||||
links.push(PdfLinkAnnotation {
|
||||
url: String::new(),
|
||||
dest_page: Some(dest_page as usize),
|
||||
x: norm_x,
|
||||
y: norm_y,
|
||||
width: norm_w,
|
||||
height: norm_h,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Links from page.annotations()
|
||||
@@ -708,6 +740,7 @@ fn extract_page_text_and_links(
|
||||
if !exists {
|
||||
links.push(PdfLinkAnnotation {
|
||||
url: url_clean,
|
||||
dest_page: None,
|
||||
x: norm_x,
|
||||
y: norm_y,
|
||||
width: norm_w,
|
||||
@@ -720,6 +753,44 @@ fn extract_page_text_and_links(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(link_annot) = annot.as_link_annotation() {
|
||||
if let Ok(link) = link_annot.link() {
|
||||
if let Some(PdfAction::LocalDestination(dest_action)) = link.action() {
|
||||
if let Ok(dest_page) = dest_action
|
||||
.destination()
|
||||
.and_then(|destination| destination.page_index())
|
||||
{
|
||||
if let Ok(rect) = link.rect() {
|
||||
let left = rect.left().value.min(rect.right().value);
|
||||
let right = rect.left().value.max(rect.right().value);
|
||||
let bottom = rect.bottom().value.min(rect.top().value);
|
||||
let top = rect.bottom().value.max(rect.top().value);
|
||||
|
||||
let (norm_x, norm_y, norm_w, norm_h) =
|
||||
normalize_coordinates(left, right, bottom, top, page_w, page_h);
|
||||
|
||||
let dest_page = dest_page as usize;
|
||||
let exists = links.iter().any(|l| {
|
||||
l.dest_page == Some(dest_page)
|
||||
&& (l.x - norm_x).abs() < 0.01
|
||||
&& (l.y - norm_y).abs() < 0.01
|
||||
});
|
||||
if !exists {
|
||||
links.push(PdfLinkAnnotation {
|
||||
url: String::new(),
|
||||
dest_page: Some(dest_page),
|
||||
x: norm_x,
|
||||
y: norm_y,
|
||||
width: norm_w,
|
||||
height: norm_h,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Text URLs from segments (heuristic for raw text URLs)
|
||||
@@ -737,6 +808,7 @@ fn extract_page_text_and_links(
|
||||
if !exists {
|
||||
links.push(PdfLinkAnnotation {
|
||||
url,
|
||||
dest_page: None,
|
||||
x: seg.x,
|
||||
y: seg.y,
|
||||
width: seg.width,
|
||||
|
||||
@@ -4,7 +4,7 @@ use anyhow::{Context as _, Result};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::cache::{CacheKey, PageLruCache, RenderedPage, DEFAULT_MEMORY_BUDGET_BYTES};
|
||||
use crate::cache::{CacheKey, DEFAULT_MEMORY_BUDGET_BYTES, PageLruCache, RenderedPage};
|
||||
use crate::document::{PageDimensions, PdfDocument};
|
||||
use crate::pdfium::PdfiumEngine;
|
||||
use crate::rasterizer::RasterizerOptions;
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
//! 3. Active page index and selection states.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
/// Snapshot of viewer position to restore seamlessly across document reloads.
|
||||
|
||||
Reference in new issue
Block a user