diff --git a/crates/rofd-core/src/document.rs b/crates/rofd-core/src/document.rs index 7cee168c..0ac4ac65 100644 --- a/crates/rofd-core/src/document.rs +++ b/crates/rofd-core/src/document.rs @@ -123,6 +123,19 @@ struct DocumentInner { bookmarks: OnceLock, bookmark_initialization: Mutex<()>, navigation_warning_state: Mutex, + /// Font-resolved per-scalar advances for runs without explicit deltas, when the + /// loader supplied a provider (see [`crate::TextAdvanceProvider`]). + text_advances: Option, +} + +/// Debug-friendly owner of the optional text advance provider. +#[derive(Clone)] +struct TextAdvances(Arc); + +impl std::fmt::Debug for TextAdvances { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("TextAdvances()") + } } #[derive(Debug, Default)] @@ -233,10 +246,11 @@ impl Page { /// physical-page millimetres, in effective paint order. /// /// Visible annotation appearances follow the page layers. Distinct nonempty - /// text objects are separated by synthesized newlines. Geometry uses only - /// validated OFD positioning and font size, without font lookup or shaping. - /// The complete result is cached and shared by cloned page handles. Failed - /// builds are not cached and may be retried on a later call. + /// text objects are separated by synthesized newlines. Geometry uses validated + /// OFD positioning; a scalar whose run declares no `DeltaX` uses the advance of a + /// provider's font when the document was loaded with one, and the font size + /// otherwise. The complete result is cached and shared by cloned page handles. + /// Failed builds are not cached and may be retried on a later call. pub fn text(&self) -> crate::Result<&crate::PageText> { if let Some(text) = self.data.semantic_text.get() { return Ok(text); @@ -247,7 +261,12 @@ impl Page { if let Some(text) = self.data.semantic_text.get() { return Ok(text); } - let text = crate::semantic::build_page_text(self)?; + let advances = self + ._document + .text_advances + .as_ref() + .map(|advances| advances.0.as_ref() as &dyn crate::TextAdvanceProvider); + let text = crate::semantic::build_page_text(self, advances)?; Ok(self.data.semantic_text.get_or_init(|| text)) } @@ -339,16 +358,40 @@ impl Page { impl Document { /// Opens an OFD document from a host file path. pub fn open(path: impl AsRef, options: LoadOptions) -> Result { + Self::open_with_advances(path, options, None) + } + + /// Opens an OFD document from a host file path, resolving text advances through + /// `advances`. + pub fn open_with_advances( + path: impl AsRef, + options: LoadOptions, + advances: Option>, + ) -> Result { let path = path.as_ref(); let bytes = fs::read(path).map_err(|source| Error::Io { path: path.to_owned(), source, })?; - Self::from_bytes(bytes, options) + Self::from_bytes_with_advances(bytes, options, advances) } /// Opens an OFD document from owned bytes. pub fn from_bytes(bytes: Vec, options: LoadOptions) -> Result { + Self::from_bytes_with_advances(bytes, options, None) + } + + /// Opens an OFD document from owned bytes, resolving text advances through + /// `advances`. + /// + /// Without a provider, runs that carry no explicit `DeltaX` keep the font-size + /// footprint; with one they use the font's advances, which is what a renderer + /// draws. + pub fn from_bytes_with_advances( + bytes: Vec, + options: LoadOptions, + advances: Option>, + ) -> Result { let strictness = options.strictness; let limits = options.limits; let container = Container::from_bytes(bytes, limits.clone())?; @@ -491,6 +534,7 @@ impl Document { container, document_path, limits, + text_advances: advances.map(TextAdvances), metadata: Metadata { document_id: info.document_id, title: info.title, diff --git a/crates/rofd-core/src/lib.rs b/crates/rofd-core/src/lib.rs index 31d3b5ec..027a3440 100644 --- a/crates/rofd-core/src/lib.rs +++ b/crates/rofd-core/src/lib.rs @@ -36,8 +36,8 @@ pub use paint::{Color, LineCap, LineJoin, StrokeStyle}; pub use path_data::{PathCommand, PathData}; pub use resources::{FontResource, ImageFormat, ImageResource, ResourceIdentity, ResourceKind}; pub use semantic::{ - FindOptions, PageText, SelectionStyle, TextChar, TextCharFlags, TextGeometryPrecision, - TextMatch, TextSelection, + FindOptions, PageText, SelectionStyle, TextAdvanceProvider, TextChar, TextCharFlags, + TextGeometryPrecision, TextMatch, TextSelection, }; pub use signature::{SealPicture, SealPictureKind, StampAnnotation}; pub use text::{CharacterGlyphMap, GlyphTransform, TextCode, TextObject}; diff --git a/crates/rofd-core/src/semantic.rs b/crates/rofd-core/src/semantic.rs index 8aaf5c8c..c7ee1959 100644 --- a/crates/rofd-core/src/semantic.rs +++ b/crates/rofd-core/src/semantic.rs @@ -1,6 +1,24 @@ use std::ops::Range; -use crate::{Error, PageObject, Point, Rect, ResourceLimits, Result, TextObject, Transform}; +use crate::{Error, Page, PageObject, Point, Rect, Result, TextObject, Transform}; + +/// Supplies the horizontal advance of one scalar when a text run declares no +/// `DeltaX` for it. +/// +/// The OFD text model positions such a scalar by its font's advance, so geometry +/// that resolves fonts matches what a renderer draws; this crate parses and indexes +/// text without font access, which is why a provider is injected from outside +/// (`rofd-render` implements it over the same font stack the renderer uses). A +/// document loaded without a provider keeps the font-size footprint for those runs, +/// which stays a valid conservative approximation. +pub trait TextAdvanceProvider: Send + Sync { + /// Returns one advance in millimetres per scalar of `text`, concatenating its + /// runs in order, or `None` when the object's font cannot be resolved (the + /// builder then keeps the font-size footprint). + /// + /// Non-finite or negative values are ignored per scalar. + fn advances_mm(&self, page: &Page, text: &TextObject, size_mm: f64) -> Option>; +} /// Options for literal search in canonical page text. #[derive(Clone, Copy, Debug, Eq, PartialEq)] @@ -462,8 +480,11 @@ fn union_rectangles(mut rectangles: impl Iterator) -> Result