image_viewer: Select PDF text by character
- Selections are character ranges backed by pdfium character boxes, fetched per page on demand and cached, so a drag stops mid-word and copy yields the exact substring. - Double clicks select the word under the pointer and right clicks outside the selection select the word before opening the context menu. - Highlight runs merge neighbouring characters on a line, keeping the painted quad count low, and margins clicks resolve to the nearest glyph. - Saving a copy of the document is available from the toolbar, the context menu, and ctrl-s; picking the opened file itself is a no-op.
This commit is contained in:
1 parent
ebf342c60b
commit
46d628e9c7
6 files changed
+742
-427
No files matched your search
File diff suppressed because it is too large.
Load diff
@@ -22,7 +22,7 @@ pub mod watcher;
|
||||
pub use cache::{CacheKey, CacheablePage, DEFAULT_MEMORY_BUDGET_BYTES, PageLruCache, RenderedPage};
|
||||
pub use document::{PageDimensions, PdfDocument};
|
||||
pub use pdfium::{
|
||||
PdfDocumentDetails, PdfLinkAnnotation, PdfMetadata, PdfOutlineItem, PdfPageDetails,
|
||||
PdfCharBox, PdfDocumentDetails, PdfLinkAnnotation, PdfMetadata, PdfOutlineItem, PdfPageDetails,
|
||||
PdfTextSegment, PdfiumEngine,
|
||||
};
|
||||
pub use rasterizer::{LuminosityToneMapper, PageRasterizer, RasterizerOptions};
|
||||
|
||||
@@ -504,6 +504,83 @@ impl PdfiumEngine {
|
||||
Ok(String::new())
|
||||
}
|
||||
}
|
||||
|
||||
/// Extracts every character on a zero-indexed page, in pdfium text index order, each with
|
||||
/// its box in normalized page coordinates (see [`normalize_coordinates`]).
|
||||
///
|
||||
/// The result is index-aligned with pdfium's per-page text indices, so a text index range
|
||||
/// `start..=end` maps directly onto a slice range of the returned vector: whitespace is
|
||||
/// included, characters whose Unicode value is unresolvable are emitted as
|
||||
/// [`UNKNOWN_TEXT_CHAR`], and characters without usable geometry are emitted as zero-sized
|
||||
/// boxes rather than skipped.
|
||||
pub fn extract_char_boxes_from_bytes(
|
||||
&self,
|
||||
bytes: &[u8],
|
||||
page_index: usize,
|
||||
) -> Result<Vec<PdfCharBox>> {
|
||||
let guard = self.inner.lock();
|
||||
if let Some(pdfium) = &guard.0 {
|
||||
let doc = pdfium
|
||||
.load_pdf_from_byte_slice(bytes, None)
|
||||
.context("Failed to load PDF for char box extraction")?;
|
||||
|
||||
let pages = doc.pages();
|
||||
let page_count = pages.len() as usize;
|
||||
if page_index >= page_count {
|
||||
anyhow::bail!(
|
||||
"Requested page index {page_index} out of bounds for document with {page_count} pages"
|
||||
);
|
||||
}
|
||||
|
||||
let page = pages
|
||||
.get(page_index as u16)
|
||||
.context("Requested page index out of bounds")?;
|
||||
|
||||
let page_w = page.width().value;
|
||||
let page_h = page.height().value;
|
||||
|
||||
let text_page = page
|
||||
.text()
|
||||
.context("Failed to load page text for char box extraction")?;
|
||||
|
||||
// `PdfPageText::chars()` enumerates every index in `0..FPDFText_CountChars(page)`, which
|
||||
// is pdfium's own text index order. Iterating segments instead (`seg.chars()`) routes
|
||||
// through `chars_inside_rect`, which filters characters by the segment rectangle and so
|
||||
// can drop indices: it is not a complete, index-ordered view of the page.
|
||||
let chars = text_page.chars();
|
||||
let mut boxes = Vec::with_capacity(chars.len());
|
||||
|
||||
for ch in chars.iter() {
|
||||
let character = ch.unicode_char().unwrap_or(UNKNOWN_TEXT_CHAR);
|
||||
|
||||
let (x, y, width, height) =
|
||||
match ch.loose_bounds().or_else(|_| ch.tight_bounds()) {
|
||||
Ok(bounds) => {
|
||||
let left = bounds.left().value.min(bounds.right().value);
|
||||
let right = bounds.left().value.max(bounds.right().value);
|
||||
let bottom = bounds.bottom().value.min(bounds.top().value);
|
||||
let top = bounds.bottom().value.max(bounds.top().value);
|
||||
|
||||
normalize_coordinates(left, right, bottom, top, page_w, page_h)
|
||||
}
|
||||
// Preserve the slot so index alignment survives geometry-less characters.
|
||||
Err(_) => (0.0, 0.0, 0.0, 0.0),
|
||||
};
|
||||
|
||||
boxes.push(PdfCharBox {
|
||||
character,
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(boxes)
|
||||
} else {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
@@ -527,6 +604,21 @@ pub struct PdfTextSegment {
|
||||
pub height: f32,
|
||||
}
|
||||
|
||||
/// Placeholder substituted for a page text index whose Unicode scalar value pdfium cannot
|
||||
/// resolve. The entry is still emitted so that returned boxes stay index-aligned with
|
||||
/// pdfium's page text indices, which callers use as selection range coordinates.
|
||||
const UNKNOWN_TEXT_CHAR: char = '\u{FFFD}';
|
||||
|
||||
/// A single character on a PDF page together with its box in normalized page coordinates.
|
||||
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct PdfCharBox {
|
||||
pub character: char,
|
||||
pub x: f32,
|
||||
pub y: f32,
|
||||
pub width: f32,
|
||||
pub height: f32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct PdfPageDetails {
|
||||
pub page_index: usize,
|
||||
|
||||
Reference in new issue
Block a user