From b5ce41eb3239ad63ae9cccfbaad9424004158702 Mon Sep 17 00:00:00 2001 From: Velli20 Date: Sun, 11 Oct 2026 23:45:15 +0300 Subject: [PATCH] Store annotation geometry as f32 pdf-graphics types Annotation Core kept its geometry in f64 while the renderer and viewport work in f32, so every projection narrowed rectangles, points, and quads back to f32 and pdf-graphics carried f64 copies of the Rect, Point, Quad, and Polyline methods only for this crate. Annotation geometry is now read from the PDF as f32: rectangles, points, quads, polylines, line endpoints, difference rectangles, border widths and dashes, and font sizes. Opacity, border-effect intensity, action duration, and destination coordinates stay f64. The f64 geometry methods are folded into the f32 impls and the narrowing helpers are gone. Difference rectangles and the insets derived from them are Rects, and AnnotationEntry exposes its bounds, origin, size, and transform as Rect, Point, Size, and Transform instead of bare arrays. The browser contract and the JS that reads the entry size follow the new shapes. Co-Authored-By: Claude Opus 5.5 --- crates/pdf-annotation-core/src/entry.rs | 11 +- crates/pdf-annotation-core/src/free_text.rs | 28 +++- crates/pdf-annotation-core/src/kind.rs | 14 +- crates/pdf-annotation-core/src/models.rs | 8 +- crates/pdf-annotation-core/src/overlay.rs | 4 +- crates/pdf-annotation-core/src/pdf_data.rs | 42 +++--- crates/pdf-annotation-core/src/projection.rs | 41 ++--- crates/pdf-annotation-core/src/shapes.rs | 36 ++--- .../pdf-annotation-core/src/source_decode.rs | 12 +- .../src/source_decode_subtypes.rs | 33 ++-- crates/pdf-annotation-core/src/source_kind.rs | 6 +- .../src/source_properties.rs | 4 +- crates/pdf-annotation-core/src/style.rs | 30 ++-- crates/pdf-annotation-core/src/subtypes.rs | 18 +-- crates/pdf-annotation-core/src/widgets.rs | 2 +- crates/pdf-graphics/src/point.rs | 21 +-- crates/pdf-graphics/src/polyline.rs | 23 +-- crates/pdf-graphics/src/quad.rs | 10 +- crates/pdf-graphics/src/rect.rs | 142 ++++++------------ crates/pdf-graphics/src/size.rs | 6 + crates/pdf-graphics/src/transform.rs | 3 + crates/pdf-graphics/src/viewport.rs | 9 +- crates/pdf-web/js/annotation_contract.ts | 51 +++++-- crates/pdf-web/js/annotation_controls.js | 2 +- crates/pdf-web/js/annotation_layer.js | 2 +- crates/pdf-web/js/annotation_visuals.js | 2 +- crates/pdf-web/src/annotation_entry.rs | 26 +--- crates/pdf-web/src/annotation_overlay.rs | 2 +- examples/web-canvas/renderer.rs | 10 +- tools/conformance/src/worker.rs | 2 +- 30 files changed, 263 insertions(+), 337 deletions(-) diff --git a/crates/pdf-annotation-core/src/entry.rs b/crates/pdf-annotation-core/src/entry.rs index d31522fc..c5f53242 100644 --- a/crates/pdf-annotation-core/src/entry.rs +++ b/crates/pdf-annotation-core/src/entry.rs @@ -7,6 +7,7 @@ use crate::{ pdf_data::AnnotationAction, style::ResolvedStyle, }; +use pdf_graphics::{point::Point, rect::Rect, size::Size, transform::Transform}; use serde::{Deserialize, Serialize}; /// Host element family presenting a widget's field value. @@ -115,14 +116,14 @@ pub struct AnnotationEntry { /// Why the host should show a placeholder instead of `annotation.content`. pub unsupported: Option, /// Device-space interaction rectangles. - pub bounds: Vec<[f64; 4]>, + pub bounds: Vec, /// Page-space bottom-left corner of the `size` box; local coordinates are - /// `[x - origin[0], origin[1] + size[1] - y]`. - pub origin: [f64; 2], + /// `[x - origin.x, origin.y + size.height - y]`. + pub origin: Point, /// Local page-unit width and height. - pub size: [f64; 2], + pub size: Size, /// Local top-left to device affine matrix. - pub transform: [f64; 6], + pub transform: Transform, /// Preserve physical size under zoom. pub no_zoom: bool, /// Preserve orientation under rotation. diff --git a/crates/pdf-annotation-core/src/free_text.rs b/crates/pdf-annotation-core/src/free_text.rs index f682a2bd..bd75091f 100644 --- a/crates/pdf-annotation-core/src/free_text.rs +++ b/crates/pdf-annotation-core/src/free_text.rs @@ -9,7 +9,12 @@ use pdf_graphics::{color::Color, polyline::Polyline, rect::Rect}; use serde::{Deserialize, Serialize}; /// Left, top, right, and bottom text insets used when the source supplies none. -const DEFAULT_INSETS: [f64; 4] = [2.0; 4]; +const DEFAULT_INSETS: Rect = Rect { + left: 2.0, + top: 2.0, + right: 2.0, + bottom: 2.0, +}; /// Solid free-text border in page units. #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] @@ -19,7 +24,7 @@ pub struct FreeTextBorder { /// Unquantized opaque sRGB color. pub color: Color, /// Nonnegative border width. - pub width: f64, + pub width: f32, } /// Explicit native free-text style, independent of retained PDF source metadata. @@ -30,13 +35,13 @@ pub struct FreeTextStyle { /// Plain PDF font resource name; no slash or PDF delimiters. pub font_name: Vec, /// Strictly positive font size in page units. - pub font_size: f64, + pub font_size: f32, /// Unquantized opaque text sRGB color. pub text_color: Color, /// Optional solid border. pub border: Option, /// Nonnegative left, top, right, bottom text insets, in that order. - pub insets: [f64; 4], + pub insets: Rect, /// Horizontal text alignment. pub alignment: Alignment, } @@ -59,7 +64,7 @@ impl FreeTextStyle { /// /// The border is present only when the style has a border color; `insets` /// default to two page units per side. Does not validate the result. - pub fn from_resolved(style: &ResolvedStyle, insets: Option<[f64; 4]>) -> Self { + pub fn from_resolved(style: &ResolvedStyle, insets: Option) -> Self { Self { font_name: style.font_resource_name().to_vec(), font_size: style.font_size, @@ -102,7 +107,14 @@ impl FreeTextStyle { || [self.text_color.r, self.text_color.g, self.text_color.b] .iter() .any(|v| !v.is_finite() || *v < 0.0) - || self.insets.iter().any(|v| !v.is_finite() || *v < 0.0) + || [ + self.insets.left, + self.insets.top, + self.insets.right, + self.insets.bottom, + ] + .iter() + .any(|v| !v.is_finite() || *v < 0.0) || self.border.as_ref().is_some_and(|b| { !b.width.is_finite() || b.width < 0.0 @@ -123,7 +135,7 @@ impl FreeTextStyle { #[cfg_attr(feature = "typescript", ts(export_to = "annotation_contract.ts"))] pub struct FreeText { /// Page-space placement. - pub bounds: Rect, + pub bounds: Rect, /// Current Unicode text. pub text: String, /// Current text and border style. @@ -131,7 +143,7 @@ pub struct FreeText { /// Rich text retained without claiming a rich-text editor or renderer. pub rich_contents: Option>, /// Optional open two- or three-point callout, translated with the annotation. - pub callout: Option>, + pub callout: Option, /// Retained border effect, unsupported by the plain-text editor. pub border_effect: Option, } diff --git a/crates/pdf-annotation-core/src/kind.rs b/crates/pdf-annotation-core/src/kind.rs index ffe854ea..abdbf85d 100644 --- a/crates/pdf-annotation-core/src/kind.rs +++ b/crates/pdf-annotation-core/src/kind.rs @@ -16,7 +16,7 @@ use pdf_graphics::rect::Rect; #[derive(Clone, Copy, Debug, PartialEq)] pub struct SourceLayout { /// Normalized copy of the source rectangle. - pub rect: Rect, + pub rect: Rect, /// Page point `rect` was measured against: the first ink point, else its origin. pub anchor: Point, } @@ -67,7 +67,7 @@ pub enum AnnotationKind { } /// Union of every quad's bounds; markup without quads has no geometry. -fn quads_bounds(quads: &[Quad]) -> Result, ValidationError> { +fn quads_bounds(quads: &[Quad]) -> Result { quads .iter() .map(Quad::bounds) @@ -85,7 +85,7 @@ impl AnnotationKind { /// # Errors /// Returns `InvalidHighlight` for markup without quads, `InvalidInk` for ink without /// strokes, and `InvalidBounds` when the result is not a finite, positive rectangle. - pub fn bounds(&self, layout: Option<&SourceLayout>) -> Result, ValidationError> { + pub fn bounds(&self, layout: Option<&SourceLayout>) -> Result { let result = match self { Self::FreeText(v) => v.bounds, Self::Link(v) => v.bounds, @@ -122,7 +122,7 @@ impl AnnotationKind { let b = v .strokes .iter() - .filter_map(|stroke| stroke.bounds()) + .filter_map(|stroke| stroke.vertex_bounds()) .reduce(|a, b| a.union(&b)) .ok_or(ValidationError::InvalidInk)?; Rect { @@ -337,7 +337,7 @@ impl AnnotationKind { return Err(ValidationError::InvalidBounds); } if v.insets - .is_some_and(|values| values.iter().any(|value| *value < 0.0)) + .is_some_and(|r| [r.left, r.top, r.right, r.bottom].iter().any(|v| *v < 0.0)) { return Err(ValidationError::InvalidStyle { field: "shape insets", @@ -409,14 +409,14 @@ impl AnnotationKind { Self::TextNote(value) => value.anchor.translate(delta.x, delta.y), Self::Ink(value) => { for line in value.strokes.iter_mut() { - line.translate(delta.x, delta.y); + line.translate(delta); } } Self::Widget(value) => value.bounds.translate(delta.x, delta.y), Self::FreeText(v) => v.translate(delta), Self::Line(v) | Self::Polygon(v) | Self::PolyLine(v) => { v.bounds.translate(delta.x, delta.y); - v.vertices.translate(delta.x, delta.y); + v.vertices.translate(delta); } Self::Square(v) | Self::Circle(v) => v.bounds.translate(delta.x, delta.y), Self::Underline(v) | Self::Squiggly(v) | Self::StrikeOut(v) => { diff --git a/crates/pdf-annotation-core/src/models.rs b/crates/pdf-annotation-core/src/models.rs index 2e4115f5..de799314 100644 --- a/crates/pdf-annotation-core/src/models.rs +++ b/crates/pdf-annotation-core/src/models.rs @@ -44,10 +44,10 @@ pub struct Revision( ); /// Position in unrotated PDF page user space, before any viewport transform. -pub type Point = pdf_graphics::point::Point; +pub type Point = pdf_graphics::point::Point; /// Convex highlight region with four distinct corners in perimeter order. -pub type Quad = pdf_graphics::quad::Quad; +pub type Quad = pdf_graphics::quad::Quad; /// Selected regions supplied by the host's text-selection system. #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] @@ -94,9 +94,9 @@ pub struct Ink { #[serde(default)] pub style: Option, /// Nonempty open strokes, each containing at least two points. - pub strokes: Vec>, + pub strokes: Vec, /// Finite, strictly positive stroke width in page user-space units. - pub width: f64, + pub width: f32, /// Stroke color; alpha is the annotation opacity. pub color: Color, } diff --git a/crates/pdf-annotation-core/src/overlay.rs b/crates/pdf-annotation-core/src/overlay.rs index 81c1bbe3..5e8b2442 100644 --- a/crates/pdf-annotation-core/src/overlay.rs +++ b/crates/pdf-annotation-core/src/overlay.rs @@ -160,7 +160,7 @@ impl AnnotationOverlay { &mut self, mut command: AnnotationCommandRequest, viewport_revision: Option, - page_bounds: Option>, + page_bounds: Option, ) -> AnnotationLayerResult { if let Some(target) = &command.target { let annotation = self.mounted_annotation(target, viewport_revision)?; @@ -357,7 +357,7 @@ impl AnnotationOverlay { &self, annotation: &Annotation, delta: Point, - page_bounds: Option>, + page_bounds: Option, ) -> AnnotationLayerResult { let page = page_bounds.ok_or(AnnotationLayerError::InvalidInput("drag page bounds"))?; let layout = self diff --git a/crates/pdf-annotation-core/src/pdf_data.rs b/crates/pdf-annotation-core/src/pdf_data.rs index a101d675..6ed7c5af 100644 --- a/crates/pdf-annotation-core/src/pdf_data.rs +++ b/crates/pdf-annotation-core/src/pdf_data.rs @@ -58,11 +58,11 @@ impl<'de> serde::Deserialize<'de> for WidgetFieldFlags { #[cfg_attr(feature = "typescript", ts(export_to = "annotation_contract.ts"))] pub struct AnnotationBorder { /// Horizontal corner radius. - pub horizontal_radius: f64, + pub horizontal_radius: f32, /// Vertical corner radius. - pub vertical_radius: f64, + pub vertical_radius: f32, /// Border width. - pub width: f64, + pub width: f32, /// Optional validated dash pattern; `None` is a solid border. pub dash_pattern: Option, } @@ -73,11 +73,11 @@ pub struct AnnotationBorder { #[cfg_attr(feature = "typescript", ts(export_to = "annotation_contract.ts"))] pub struct BorderStyle { /// Border width. - pub width: Option, + pub width: Option, /// Border style name. pub style: Option, /// Optional dash pattern. - pub dash_pattern: Option>, + pub dash_pattern: Option>, } /// Owned BorderStyleName data retained from the source PDF. @@ -463,7 +463,7 @@ impl AnnotationAction { #[cfg_attr(feature = "typescript", ts(export_to = "annotation_contract.ts"))] pub struct InkList { /// The parsed stroke lists. - pub strokes: Vec>, + pub strokes: Vec, } /// Owned CaretAnnotation data retained from the source PDF. @@ -471,8 +471,8 @@ pub struct InkList { #[cfg_attr(feature = "typescript", derive(ts_rs::TS))] #[cfg_attr(feature = "typescript", ts(export_to = "annotation_contract.ts"))] pub struct CaretAnnotation { - /// The difference rectangle. - pub difference_rect: Option<[f64; 4]>, + /// The `/RD` difference rectangle: left, top, right, and bottom insets from `/Rect`. + pub difference_rect: Option, /// The caret style. pub style: Option, } @@ -488,8 +488,8 @@ pub struct CircleAnnotation { pub interior_color: Option, /// The border effect. pub border_effect: Option, - /// The difference rectangle. - pub difference_rect: Option<[f64; 4]>, + /// The `/RD` difference rectangle: left, top, right, and bottom insets from `/Rect`. + pub difference_rect: Option, } /// Owned FreeTextAlignment data retained from the source PDF. @@ -519,11 +519,11 @@ pub struct FreeTextAnnotation { /// The default style string. pub default_style: Option>, /// The open callout line, from its starting point to its attachment point. - pub callout_line: Option>, + pub callout_line: Option, /// The border effect. pub border_effect: Option, - /// The difference rectangle. - pub difference_rect: Option<[f64; 4]>, + /// The `/RD` difference rectangle: left, top, right, and bottom insets from `/Rect`. + pub difference_rect: Option, /// The intent. pub intent: Option>, } @@ -560,7 +560,7 @@ pub struct InkAnnotation { #[cfg_attr(feature = "typescript", ts(export_to = "annotation_contract.ts"))] pub struct LineAnnotation { /// The required line endpoints. - pub line: [f64; 4], + pub line: [f32; 4], /// The line ending styles. pub line_endings: Option<[LineEndingStyle; 2]>, /// The border style. @@ -568,9 +568,9 @@ pub struct LineAnnotation { /// The interior color. pub interior_color: Option, /// The leader line length. - pub leader_line_length: Option, + pub leader_line_length: Option, /// The leader line extension length. - pub leader_line_extension: Option, + pub leader_line_extension: Option, /// Whether the caption is positioned at the line's end. pub caption: Option, /// The intent. @@ -602,7 +602,7 @@ pub struct LinkAnnotation { #[cfg_attr(feature = "typescript", ts(export_to = "annotation_contract.ts"))] pub struct PolygonAnnotation { /// The required polygon vertices. - pub vertices: Polyline, + pub vertices: Polyline, /// The line ending styles. pub line_endings: Option<[LineEndingStyle; 2]>, /// The interior color. @@ -619,7 +619,7 @@ pub struct PolygonAnnotation { #[cfg_attr(feature = "typescript", ts(export_to = "annotation_contract.ts"))] pub struct PolyLineAnnotation { /// The required polyline vertices. - pub vertices: Polyline, + pub vertices: Polyline, /// The line ending styles. pub line_endings: Option<[LineEndingStyle; 2]>, /// The interior color. @@ -654,8 +654,8 @@ pub struct SquareAnnotation { pub interior_color: Option, /// The border effect. pub border_effect: Option, - /// The difference rectangle. - pub difference_rect: Option<[f64; 4]>, + /// The `/RD` difference rectangle: left, top, right, and bottom insets from `/Rect`. + pub difference_rect: Option, } /// Owned SquigglyAnnotation data retained from the source PDF. @@ -868,7 +868,7 @@ pub struct SourceAnnotation { /// The optional annotation rectangle from `/Rect`, in PDF page user space /// where `bottom` is the lower `y` edge. Edges keep their source values and /// order, so inverted rectangles are preserved rather than normalized. - pub rect: Option>, + pub rect: Option, /// The optional annotation contents. pub contents: Option>, /// The optional annotation name from `/NM`. diff --git a/crates/pdf-annotation-core/src/projection.rs b/crates/pdf-annotation-core/src/projection.rs index cb1cdeb8..0c6eba7d 100644 --- a/crates/pdf-annotation-core/src/projection.rs +++ b/crates/pdf-annotation-core/src/projection.rs @@ -11,11 +11,7 @@ use crate::{ pdf_data::AnnotationAction, shapes, widget_control, }; -use pdf_graphics::{ - point::Point, - rect::Rect, - viewport::{PageViewport, ViewportError}, -}; +use pdf_graphics::{point::Point, rect::Rect, size::Size, viewport::PageViewport}; /// The shared field a widget is bound to; `None` for other kinds. fn widget_field<'a>( @@ -46,28 +42,20 @@ fn unsupported_reason(annotation: &Annotation, field: Option<&Field>) -> Option< /// Device-space interaction rectangles: one per quad, else the payload bounds. fn device_regions( annotation: &Annotation, - bounds: &Rect, + bounds: &Rect, viewport: &PageViewport, -) -> AnnotationLayerResult> { +) -> AnnotationLayerResult> { let regions = match annotation.content.quads().filter(|q| !q.is_empty()) { Some(quads) => quads .iter() .map(|quad| viewport.map_quad(quad)) .collect::, _>>()?, - None => { - let rect = bounds.to_f32().ok_or(ViewportError::Bounds)?; - vec![viewport.map_rect(&rect)?.into()] - } + None => vec![viewport.map_rect(bounds)?], }; - regions - .into_iter() - .map(|region| { - if !region.is_valid() { - return Err(AnnotationLayerError::InvalidInput("device bounds")); - } - Ok([region.left, region.top, region.right, region.bottom]) - }) - .collect() + if !regions.iter().all(|region| region.is_valid()) { + return Err(AnnotationLayerError::InvalidInput("device bounds")); + } + Ok(regions) } pub(crate) fn project( @@ -102,16 +90,9 @@ pub(crate) fn project( subtype: annotation.content.subtype_name().into_owned(), unsupported: unsupported_reason(annotation, field), bounds: device_regions(annotation, &bounds, viewport)?, - origin: [bounds.left, bounds.top], - size: [bounds.width(), bounds.height()], - transform: viewport - .local_frame( - Point::new(bounds.left, bounds.bottom) - .to_f32() - .ok_or(ViewportError::Bounds)?, - )? - .to_row() - .map(f64::from), + origin: Point::new(bounds.left, bounds.top), + size: Size::new(bounds.width(), bounds.height()), + transform: viewport.local_frame(Point::new(bounds.left, bounds.bottom))?, no_zoom: annotation.metadata.no_zoom(), no_rotate: annotation.metadata.no_rotate(), draggable: annotation.can_translate(), diff --git a/crates/pdf-annotation-core/src/shapes.rs b/crates/pdf-annotation-core/src/shapes.rs index b39e7651..87f6ecd4 100644 --- a/crates/pdf-annotation-core/src/shapes.rs +++ b/crates/pdf-annotation-core/src/shapes.rs @@ -14,9 +14,9 @@ use serde::{Deserialize, Serialize}; /// Fill alpha of highlight quadrilaterals. const HIGHLIGHT_ALPHA: f32 = 0.35; /// Smallest line-ending decoration. -const MIN_DECORATION_SIZE: f64 = 3.0; +const MIN_DECORATION_SIZE: f32 = 3.0; /// Line-ending decoration size relative to the stroke width. -const DECORATION_STROKE_SCALE: f64 = 3.0; +const DECORATION_STROKE_SCALE: f32 = 3.0; pub use crate::style::ShapePaint; @@ -29,33 +29,33 @@ pub enum Shape { /// Open polyline or closed polygon; a straight segment is two open vertices. Polyline { /// Vertices in drawing order and whether the outline closes back to the first. - path: Polyline, + path: Polyline, /// Paint for the outline and interior. paint: ShapePaint, }, /// Axis-aligned ellipse inscribed in its bounds. Ellipse { /// Enclosing rectangle. - bounds: Rect, + bounds: Rect, /// Paint for the outline and interior. paint: ShapePaint, }, /// Axis-aligned rectangle. Rect { /// Rectangle edges. - bounds: Rect, + bounds: Rect, /// Paint for the outline and interior. paint: ShapePaint, }, } /// Converts a page point into local top-left units of `bounds`. -fn local(bounds: &Rect, point: Point) -> Point { +fn local(bounds: &Rect, point: Point) -> Point { Point::new(point.x - bounds.left, bounds.bottom - point.y) } /// Rectangle inset from the local box of `bounds` by half the stroke width. -fn inset_rect(bounds: &Rect, style: &ResolvedStyle) -> Rect { +fn inset_rect(bounds: &Rect, style: &ResolvedStyle) -> Rect { let inset = style.stroke_width() / 2.0; Rect { left: inset, @@ -71,11 +71,7 @@ fn opaque(color: Color) -> Color { } /// Builds the local shapes for `annotation`, or none for kinds with native presentation. -pub(crate) fn shapes( - annotation: &Annotation, - bounds: &Rect, - style: &ResolvedStyle, -) -> Vec { +pub(crate) fn shapes(annotation: &Annotation, bounds: &Rect, style: &ResolvedStyle) -> Vec { match &annotation.content { Kind::Highlight(v) => v .quads @@ -131,9 +127,9 @@ pub(crate) fn shapes( /// Draws one decoration per quad along the line `ratio` of the way from top to bottom. fn markup( quads: &[crate::models::Quad], - bounds: &Rect, + bounds: &Rect, style: &ResolvedStyle, - ratio: f64, + ratio: f32, squiggly: bool, ) -> Vec { quads @@ -155,15 +151,15 @@ fn markup( .collect() } -fn lerp(a: Point, b: Point, t: f64) -> Point { +fn lerp(a: Point, b: Point, t: f32) -> Point { Point::new(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t) } /// Path outline plus its endpoint decorations. fn path( - vertices: &Polyline, + vertices: &Polyline, endings: Option<&[LineEndingStyle; 2]>, - bounds: &Rect, + bounds: &Rect, style: &ResolvedStyle, ) -> Vec { let path = vertices.map(|p| local(bounds, p)); @@ -193,16 +189,16 @@ fn ending( let size = (style.stroke_width() * DECORATION_STROKE_SCALE).max(MIN_DECORATION_SIZE); let half = size / 2.0; let (sin, cos) = (end.y - adjacent.y).atan2(end.x - adjacent.x).sin_cos(); - let place = |[x, y]: [f64; 2]| Point::new(end.x + x * cos - y * sin, end.y + x * sin + y * cos); + let place = |[x, y]: [f32; 2]| Point::new(end.x + x * cos - y * sin, end.y + x * sin + y * cos); let filled = ShapePaint { fill: Some(opaque(style.color)), ..style.outline(false) }; - let closed = |points: &[[f64; 2]]| Shape::Polyline { + let closed = |points: &[[f32; 2]]| Shape::Polyline { path: Polyline::new(points.iter().map(|p| place(*p)).collect(), true), paint: filled.clone(), }; - let open = |points: &[[f64; 2]]| Shape::Polyline { + let open = |points: &[[f32; 2]]| Shape::Polyline { path: Polyline::new(points.iter().map(|p| place(*p)).collect(), false), paint: style.outline(false), }; diff --git a/crates/pdf-annotation-core/src/source_decode.rs b/crates/pdf-annotation-core/src/source_decode.rs index 329cadb4..5ff12b83 100644 --- a/crates/pdf-annotation-core/src/source_decode.rs +++ b/crates/pdf-annotation-core/src/source_decode.rs @@ -56,7 +56,7 @@ impl FromPdfObject for SourceAnnotation { .map(|value| { // `/Rect` is `[left, bottom, right, top]`; keep the source // edges as-is so inverted rectangles survive decoding. - value.try_array_of::(objects).map(|arr| { + value.try_array_of::(objects).map(|arr| { let [left, bottom, right, top] = arr; Rect { left, @@ -301,9 +301,9 @@ impl AnnotationBorder { }); }; - let horizontal_radius = horizontal_radius.try_number::(objects)?; - let vertical_radius = vertical_radius.try_number::(objects)?; - let width = width.try_number::(objects)?; + let horizontal_radius = horizontal_radius.try_number::(objects)?; + let vertical_radius = vertical_radius.try_number::(objects)?; + let width = width.try_number::(objects)?; // The dash array is nominally nested, but some writers flatten it into the // `/Border` array itself. @@ -379,12 +379,12 @@ impl BorderStyle { let Some(dictionary) = dictionary.optional_dictionary(key, objects)? else { return Ok(None); }; - let width = dictionary.optional_number::(b"W", objects)?; + let width = dictionary.optional_number::(b"W", objects)?; let style = dictionary .get(b"S") .map(|value| value.try_bytes(objects).map(BorderStyleName::from)) .transpose()?; - let dash_pattern = dictionary.optional_vec_of::(b"D", objects)?; + let dash_pattern = dictionary.optional_vec_of::(b"D", objects)?; Ok(Some(Self { width, diff --git a/crates/pdf-annotation-core/src/source_decode_subtypes.rs b/crates/pdf-annotation-core/src/source_decode_subtypes.rs index 9dfef65f..b1aefca9 100644 --- a/crates/pdf-annotation-core/src/source_decode_subtypes.rs +++ b/crates/pdf-annotation-core/src/source_decode_subtypes.rs @@ -9,7 +9,7 @@ use crate::pdf_data::{ StampAnnotation, StrikeOutAnnotation, TextAnnotation, UnderlineAnnotation, WidgetAnnotation, }; use crate::source_decode::{DecodeResult, color}; -use pdf_graphics::polyline::Polyline; +use pdf_graphics::{polyline::Polyline, rect::Rect}; use pdf_object_reader::{ dictionary::Dictionary, object_lookup::ObjectLookupExt, object_resolver::ObjectResolver, object_variant::ObjectVariant, @@ -141,7 +141,9 @@ impl FreeTextAnnotation { .get(b"CL") .map(|value| callout_line(value, objects)) .transpose()?; - let difference_rect = dictionary.optional_array_of::(b"RD", objects)?; + let difference_rect = dictionary + .optional_array_of::(b"RD", objects)? + .map(Rect::from); let intent = dictionary.optional_bytes(b"IT", objects)?.map(Vec::from); let border_effect = BorderEffect::from_dictionary(dictionary, b"BE", objects)?; @@ -165,12 +167,12 @@ impl LineAnnotation { ) -> DecodeResult { let line = dictionary .get_or_err(b"L")? - .try_array_of::(objects)?; + .try_array_of::(objects)?; let line_endings = line_endings(dictionary, objects)?; let border_style = BorderStyle::from_dictionary(dictionary, b"BS", objects)?; let interior_color = color(dictionary, b"IC", objects)?; - let leader_line_length = dictionary.optional_number::(b"LL", objects)?; - let leader_line_extension = dictionary.optional_number::(b"LLE", objects)?; + let leader_line_length = dictionary.optional_number::(b"LL", objects)?; + let leader_line_extension = dictionary.optional_number::(b"LLE", objects)?; let caption = dictionary.optional_boolean(b"Cap", objects)?; let intent = dictionary.optional_bytes(b"IT", objects)?.map(Vec::from); @@ -195,7 +197,9 @@ impl SquareAnnotation { let border_style = BorderStyle::from_dictionary(dictionary, b"BS", objects)?; let interior_color = color(dictionary, b"IC", objects)?; let border_effect = BorderEffect::from_dictionary(dictionary, b"BE", objects)?; - let difference_rect = dictionary.optional_array_of::(b"RD", objects)?; + let difference_rect = dictionary + .optional_array_of::(b"RD", objects)? + .map(Rect::from); Ok(Self { border_style, @@ -214,7 +218,9 @@ impl CircleAnnotation { let border_style = BorderStyle::from_dictionary(dictionary, b"BS", objects)?; let interior_color = color(dictionary, b"IC", objects)?; let border_effect = BorderEffect::from_dictionary(dictionary, b"BE", objects)?; - let difference_rect = dictionary.optional_array_of::(b"RD", objects)?; + let difference_rect = dictionary + .optional_array_of::(b"RD", objects)? + .map(Rect::from); Ok(Self { border_style, @@ -356,7 +362,9 @@ impl CaretAnnotation { dictionary: &Dictionary, objects: &dyn ObjectResolver, ) -> DecodeResult { - let difference_rect = dictionary.optional_array_of::(b"RD", objects)?; + let difference_rect = dictionary + .optional_array_of::(b"RD", objects)? + .map(Rect::from); let style = dictionary .get(b"Sy") .map(|value| value.try_bytes(objects).map(CaretSymbolStyle::from)) @@ -433,7 +441,7 @@ fn point_list( value: &ObjectVariant, objects: &dyn ObjectResolver, ) -> DecodeResult> { - let values = value.try_vec_of::(objects)?; + let values = value.try_vec_of::(objects)?; if !values.len().is_multiple_of(2) { return Err(SourceDecodeError::InvalidEntry { entry: key, @@ -450,10 +458,7 @@ fn point_list( } /// Parses a free-text callout's required two or three vertices. -fn callout_line( - value: &ObjectVariant, - objects: &dyn ObjectResolver, -) -> DecodeResult> { +fn callout_line(value: &ObjectVariant, objects: &dyn ObjectResolver) -> DecodeResult { let points = point_list(b"CL", value, objects)?; if !matches!(points.len(), 2 | 3) { return Err(SourceDecodeError::InvalidEntry { @@ -474,7 +479,7 @@ fn quad_points( let Some(value) = dictionary.get(b"QuadPoints") else { return Ok(None); }; - let values = value.try_vec_of::(objects)?; + let values = value.try_vec_of::(objects)?; if !values.len().is_multiple_of(8) { return Err(SourceDecodeError::InvalidEntry { entry: b"QuadPoints", diff --git a/crates/pdf-annotation-core/src/source_kind.rs b/crates/pdf-annotation-core/src/source_kind.rs index 01d545d7..f9ea375a 100644 --- a/crates/pdf-annotation-core/src/source_kind.rs +++ b/crates/pdf-annotation-core/src/source_kind.rs @@ -180,7 +180,7 @@ fn line( /// Polygon and polyline vertices are retained as parsed, including closure. fn path( source: &SourceAnnotation, - vertices: &Polyline, + vertices: &Polyline, endings: &Option<[LineEndingStyle; 2]>, style: ResolvedStyle, ) -> Result { @@ -196,7 +196,7 @@ fn path( fn shape( source: &SourceAnnotation, border_effect: &Option, - insets: Option<[f64; 4]>, + insets: Option, style: ResolvedStyle, ) -> Result { Ok(BoxAnnotation { @@ -252,7 +252,7 @@ fn unknown( source: &SourceAnnotation, subtype: &[u8], ) -> Result { - let bounds: Option> = source.rect.map(|_| source.bounds()).transpose()?; + let bounds: Option = source.rect.map(|_| source.bounds()).transpose()?; Ok(UnknownAnnotation { subtype: subtype.to_vec(), bounds, diff --git a/crates/pdf-annotation-core/src/source_properties.rs b/crates/pdf-annotation-core/src/source_properties.rs index f22f4688..b1b35e71 100644 --- a/crates/pdf-annotation-core/src/source_properties.rs +++ b/crates/pdf-annotation-core/src/source_properties.rs @@ -106,7 +106,7 @@ impl FreeTextAlignment { impl BorderStyle { /// Resolve retained `/W`: absent means one page unit and negative means zero. /// Returns `InvalidStyle` for a nonfinite width; does not mutate metadata or panic. - pub fn resolved_width(&self) -> Result { + pub fn resolved_width(&self) -> Result { let width = self.width.unwrap_or(1.0); if !width.is_finite() { return Err(ValidationError::InvalidStyle { @@ -127,7 +127,7 @@ impl SourceAnnotation { /// /// Returns `MissingRect` when the record has no rectangle and `InvalidBounds` /// when the normalized rectangle is nonfinite or empty. Does not panic. - pub fn bounds(&self) -> Result, ValidationError> { + pub fn bounds(&self) -> Result { let rect = self .rect .ok_or(ValidationError::MissingRect { source_id: self.id })? diff --git a/crates/pdf-annotation-core/src/style.rs b/crates/pdf-annotation-core/src/style.rs index d558b519..4f8b84c6 100644 --- a/crates/pdf-annotation-core/src/style.rs +++ b/crates/pdf-annotation-core/src/style.rs @@ -7,13 +7,13 @@ use pdf_graphics::{color::Color, rect::Rect}; use serde::{Deserialize, Serialize}; /// Stroke width used when a resolved style has no positive border width. -const DEFAULT_STROKE_WIDTH: f64 = 1.0; +const DEFAULT_STROKE_WIDTH: f32 = 1.0; /// Line height, in ems, an auto-sized single-line widget must fit within its height. -const AUTO_SIZE_LINE_FACTOR: f64 = 1.35; +const AUTO_SIZE_LINE_FACTOR: f32 = 1.35; /// Estimated average glyph advance, in ems, used to fit a push-button caption's width. -const AVERAGE_GLYPH_EM: f64 = 0.6; +const AVERAGE_GLYPH_EM: f32 = 0.6; /// Fill and stroke of one shape. #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] @@ -25,9 +25,9 @@ pub struct ShapePaint { /// Interior color whose alpha is the interior opacity; `None` leaves the interior unpainted. pub fill: Option, /// Outline width in local units. - pub width: f64, + pub width: f32, /// Dash lengths; empty means solid. - pub dash: Vec, + pub dash: Vec, /// Round caps and joins instead of the host default. pub round: bool, } @@ -59,13 +59,13 @@ pub struct ResolvedStyle { /// Optional opaque border color. pub border_color: Option, /// Nonnegative border width in page units. - pub border_width: f64, + pub border_width: f32, /// Nonnegative dash lengths; empty means solid. - pub dash: Vec, + pub dash: Vec, /// Opacity in the range zero through one. pub opacity: f64, /// Positive font size in page units. - pub font_size: f64, + pub font_size: f32, /// Host-facing font family or generic fallback. pub font_family: String, /// Horizontal text alignment. @@ -91,7 +91,7 @@ impl Default for ResolvedStyle { impl ResolvedStyle { /// Style from a compact payload color, whose alpha is the opacity, and stroke width. /// The color is made opaque; other fields keep their defaults. - pub fn from_compact(color: Color, width: f64) -> Self { + pub fn from_compact(color: Color, width: f32) -> Self { Self { color: Color::from_rgb(color.r, color.g, color.b), opacity: f64::from(color.a), @@ -119,7 +119,7 @@ impl ResolvedStyle { /// Returns the positive border width, or one page unit when no positive width is set. /// This preserves the outline fallback; it does not validate the style. - pub fn stroke_width(&self) -> f64 { + pub fn stroke_width(&self) -> f32 { if self.border_width > 0.0 { self.border_width } else { @@ -239,7 +239,7 @@ pub fn apply_default_style(bytes: &[u8], style: &mut ResolvedStyle) -> bool { match token { "Tf" => { if let [name, size] = operands.as_slice() { - let size = size.parse::().ok().filter(|size| size.is_finite()); + let size = size.parse::().ok().filter(|size| size.is_finite()); auto_size = size == Some(0.0); if let Some(size) = size.filter(|size| *size > 0.0) { style.font_size = size; @@ -262,7 +262,7 @@ pub fn apply_default_style(bytes: &[u8], style: &mut ResolvedStyle) -> bool { } operands.clear(); } - value if value.starts_with('/') || value.parse::().is_ok() => { + value if value.starts_with('/') || value.parse::().is_ok() => { if operands.len() < 4 { operands.push(value); } else { @@ -282,7 +282,7 @@ impl WidgetAnnotation { /// Only single-line text, combo boxes, and push buttons are auto-sized; a push /// button's `/MK /CA` caption must also fit the width. Returns `None` when the /// border leaves no room. Does not panic. - pub fn auto_font_size(&self, rect: &Rect, border_width: f64) -> Option { + pub fn auto_font_size(&self, rect: &Rect, border_width: f32) -> Option { let single_line = match self.field_type.as_deref() { Some(b"Tx") => self.text_mode() != TextMode::Multiline, Some(b"Ch") => self.is_combo_box(), @@ -300,7 +300,7 @@ impl WidgetAnnotation { .flatten() .and_then(|mk| mk.normal_caption.as_deref()) .map(|caption| String::from_utf8_lossy(caption).chars().count()) - .and_then(|count| count.to_f64()) + .and_then(|count| count.to_f32()) .filter(|count| *count > 0.0); if let Some(count) = caption_chars { size = size.min((rect.width() - inset) / (AVERAGE_GLYPH_EM * count)); @@ -336,7 +336,7 @@ pub fn resolve_source_style( style.dash = border .dash_pattern .as_ref() - .map(|pattern| pattern.intervals.iter().copied().map(f64::from).collect()) + .map(|pattern| pattern.intervals.clone()) .unwrap_or_default(); } let (border, interior) = match &annotation.kind { diff --git a/crates/pdf-annotation-core/src/subtypes.rs b/crates/pdf-annotation-core/src/subtypes.rs index a022110b..70271dc2 100644 --- a/crates/pdf-annotation-core/src/subtypes.rs +++ b/crates/pdf-annotation-core/src/subtypes.rs @@ -15,9 +15,9 @@ use serde::{Deserialize, Serialize}; #[cfg_attr(feature = "typescript", ts(export_to = "annotation_contract.ts"))] pub struct PathAnnotation { /// Annotation rectangle; translations move it together with all vertices. - pub bounds: Rect, + pub bounds: Rect, /// Ordered vertices; `closed` is true only for polygons and never duplicates a vertex. - pub vertices: Polyline, + pub vertices: Polyline, /// Optional endpoint decorations, preserving unknown names. pub endings: Option<[LineEndingStyle; 2]>, /// Current stroke/fill style. @@ -30,13 +30,13 @@ pub struct PathAnnotation { #[cfg_attr(feature = "typescript", ts(export_to = "annotation_contract.ts"))] pub struct BoxAnnotation { /// Page-space rectangle enclosing the shape. - pub bounds: Rect, + pub bounds: Rect, /// Current stroke/fill style. pub style: ResolvedStyle, /// Optional border effect, which may be unsupported by a host. pub border_effect: Option, /// Optional left, top, right, bottom difference rectangle. - pub insets: Option<[f64; 4]>, + pub insets: Option, } /// Underline, squiggly, or strikeout regions, in Core perimeter order. @@ -56,7 +56,7 @@ pub struct Markup { #[cfg_attr(feature = "typescript", ts(export_to = "annotation_contract.ts"))] pub struct Link { /// Fallback interaction bounds. - pub bounds: Rect, + pub bounds: Rect, /// Optional quadrilateral interaction regions in perimeter order. pub quads: Option>, /// Pointer highlight feedback metadata. @@ -86,7 +86,7 @@ impl Link { #[cfg_attr(feature = "typescript", ts(export_to = "annotation_contract.ts"))] pub struct Stamp { /// Page-space stamp rectangle. - pub bounds: Rect, + pub bounds: Rect, /// Original stamp name bytes. pub name: Vec, /// Current style. @@ -122,7 +122,7 @@ impl Stamp { #[cfg_attr(feature = "typescript", ts(export_to = "annotation_contract.ts"))] pub struct Popup { /// Popup placement. - pub bounds: Rect, + pub bounds: Rect, /// Live parent identity, distinct from retained PDF object references. pub parent: Option, /// Initial visibility requested from the host. @@ -137,7 +137,7 @@ pub struct Popup { #[cfg_attr(feature = "typescript", ts(export_to = "annotation_contract.ts"))] pub struct Caret { /// Placement rectangle. - pub bounds: Rect, + pub bounds: Rect, /// Legacy caret properties. pub properties: crate::pdf_data::CaretAnnotation, } @@ -150,5 +150,5 @@ pub struct UnknownAnnotation { /// Original subtype name bytes. pub subtype: Vec, /// Optional usable page-space bounds. - pub bounds: Option>, + pub bounds: Option, } diff --git a/crates/pdf-annotation-core/src/widgets.rs b/crates/pdf-annotation-core/src/widgets.rs index 15f339c3..321f99dc 100644 --- a/crates/pdf-annotation-core/src/widgets.rs +++ b/crates/pdf-annotation-core/src/widgets.rs @@ -42,7 +42,7 @@ pub struct Widget { pub style: Option, /// Placement on the containing annotation's page. /// Bounds, including their maximum corner, must remain finite after edits. - pub bounds: Rect, + pub bounds: Rect, /// Immutable binding; delete and create an annotation to change its field/option. pub binding: WidgetBinding, } diff --git a/crates/pdf-graphics/src/point.rs b/crates/pdf-graphics/src/point.rs index 0f141fec..dafbe1ce 100644 --- a/crates/pdf-graphics/src/point.rs +++ b/crates/pdf-graphics/src/point.rs @@ -1,12 +1,10 @@ -use num_traits::ToPrimitive; - /// A 2D point. #[derive(Debug, Clone, Copy, Default, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "typescript", derive(ts_rs::TS))] #[cfg_attr( feature = "typescript", - ts(export_to = "annotation_contract.ts", concrete(T = f64)) + ts(export_to = "annotation_contract.ts", concrete(T = f32)) )] pub struct Point { /// Horizontal coordinate. @@ -21,28 +19,15 @@ impl Point { } } -impl Point { +impl Point { /// Whether both coordinates are finite. pub fn is_finite(self) -> bool { self.x.is_finite() && self.y.is_finite() } /// Moves the point by the given displacement. - pub fn translate(&mut self, dx: f64, dy: f64) { + pub fn translate(&mut self, dx: f32, dy: f32) { self.x += dx; self.y += dy; } - - /// Narrows both coordinates to `f32`, or returns `None` unless both stay finite. - pub fn to_f32(self) -> Option { - let narrow = |value: f64| value.to_f32().filter(|value| value.is_finite()); - Some(Point::new(narrow(self.x)?, narrow(self.y)?)) - } -} - -impl From for Point { - /// Widens both coordinates losslessly. - fn from(point: Point) -> Self { - Self::new(f64::from(point.x), f64::from(point.y)) - } } diff --git a/crates/pdf-graphics/src/polyline.rs b/crates/pdf-graphics/src/polyline.rs index ec9c2195..50419d94 100644 --- a/crates/pdf-graphics/src/polyline.rs +++ b/crates/pdf-graphics/src/polyline.rs @@ -9,9 +9,9 @@ use crate::{ use num_traits::ToPrimitive; /// Distance between squiggle vertices along the baseline. -const SQUIGGLE_STEP: f64 = 2.0; +const SQUIGGLE_STEP: f32 = 2.0; /// Perpendicular squiggle displacement. -const SQUIGGLE_AMPLITUDE: f64 = 1.0; +const SQUIGGLE_AMPLITUDE: f32 = 1.0; /// One sequence of straight segments, with closure stored separately from vertices. /// @@ -21,7 +21,7 @@ const SQUIGGLE_AMPLITUDE: f64 = 1.0; #[cfg_attr(feature = "typescript", derive(ts_rs::TS))] #[cfg_attr( feature = "typescript", - ts(export_to = "annotation_contract.ts", concrete(T = f64)) + ts(export_to = "annotation_contract.ts", concrete(T = f32)) )] pub struct Polyline { /// Vertices in order, without duplication of the closing point. @@ -107,21 +107,12 @@ impl Polyline { } path } -} - -impl Polyline { - /// Moves every vertex by the given displacement without validating. - pub fn translate(&mut self, dx: f64, dy: f64) { - for point in &mut self.points { - point.translate(dx, dy); - } - } /// Axis-aligned rectangle enclosing every vertex; `None` for empty geometry. /// - /// Unlike [`Polyline::::bounds`], vertices are taken as-is: no transform is + /// Unlike [`Polyline::bounds`], vertices are taken as-is: no transform is /// applied and nonfinite coordinates propagate into the result unvalidated. - pub fn bounds(&self) -> Option> { + pub fn vertex_bounds(&self) -> Option { self.points .iter() .map(|point| Rect { @@ -139,7 +130,7 @@ impl Polyline { /// units, with alternating perpendicular offsets of -1.0 and +1.0 units. /// The endpoints are baseline anchors, not the first and last generated vertices. /// Coincident anchors produce two coincident vertices. Inputs are not validated. - pub fn squiggle(a: Point, b: Point) -> Self { + pub fn squiggle(a: Point, b: Point) -> Self { let (dx, dy) = (b.x - a.x, b.y - a.y); let length = dx.hypot(dy); let unit = if length > 0.0 { length } else { 1.0 }; @@ -147,7 +138,7 @@ impl Polyline { let count = steps.to_usize().unwrap_or(1); let points = (0..=count) .map(|i| { - let t = i.to_f64().unwrap_or(0.0) / steps; + let t = i.to_f32().unwrap_or(0.0) / steps; let shift = if i % 2 == 1 { SQUIGGLE_AMPLITUDE } else { diff --git a/crates/pdf-graphics/src/quad.rs b/crates/pdf-graphics/src/quad.rs index 2492400f..1748fe1b 100644 --- a/crates/pdf-graphics/src/quad.rs +++ b/crates/pdf-graphics/src/quad.rs @@ -6,7 +6,7 @@ use crate::{point::Point, polyline::Polyline, rect::Rect}; #[cfg_attr(feature = "typescript", derive(ts_rs::TS))] #[cfg_attr( feature = "typescript", - ts(export_to = "annotation_contract.ts", concrete(T = f64)) + ts(export_to = "annotation_contract.ts", concrete(T = f32)) )] pub struct Quad { /// Clockwise or counterclockwise perimeter; crossing edges are invalid. @@ -20,14 +20,14 @@ impl From<&Quad> for Polyline { } } -impl Quad { +impl Quad { /// Whether every corner is finite. pub fn is_finite(&self) -> bool { self.corners.iter().all(|corner| corner.is_finite()) } /// Axis-aligned rectangle enclosing all four corners, with `left`/`top` the minima. - pub fn bounds(&self) -> Rect { + pub fn bounds(&self) -> Rect { let [first, rest @ ..] = self.corners; rest.iter().fold( Rect { @@ -59,8 +59,8 @@ impl Quad { // Positive scaling of each axis preserves winding. Normalize edge differences // independently so huge offsets, subnormal extents, and extreme aspect ratios do // not overflow or underflow merely when computing a cross product. -fn turn(a: Point, b: Point, c: Point) -> f64 { - let normalize = |a: f64, b: f64, c: f64| { +fn turn(a: Point, b: Point, c: Point) -> f32 { + let normalize = |a: f32, b: f32, c: f32| { let (mut ab, mut bc) = (b - a, c - b); if !ab.is_finite() || !bc.is_finite() { ab = b * 0.5 - a * 0.5; diff --git a/crates/pdf-graphics/src/rect.rs b/crates/pdf-graphics/src/rect.rs index dbddcc66..8eb38ce5 100644 --- a/crates/pdf-graphics/src/rect.rs +++ b/crates/pdf-graphics/src/rect.rs @@ -1,5 +1,4 @@ use crate::{point::Point, transform::Transform}; -use num_traits::ToPrimitive; /// An axis-aligned rectangle whose edge coordinate type defaults to `f32`. #[derive(Debug, Default, Clone, Copy, PartialEq)] @@ -7,7 +6,7 @@ use num_traits::ToPrimitive; #[cfg_attr(feature = "typescript", derive(ts_rs::TS))] #[cfg_attr( feature = "typescript", - ts(export_to = "annotation_contract.ts", concrete(T = f64)) + ts(export_to = "annotation_contract.ts", concrete(T = f32)) )] pub struct Rect { /// The left edge of the rectangle. @@ -56,77 +55,22 @@ impl Rect { self.bottom - self.top } - /// Returns whether this rectangle has finite edges and positive dimensions. + /// Returns whether this rectangle has finite edges and finite, positive dimensions. pub fn is_valid(&self) -> bool { - self.left.is_finite() - && self.top.is_finite() - && self.right.is_finite() - && self.bottom.is_finite() + [ + self.left, + self.top, + self.right, + self.bottom, + self.width(), + self.height(), + ] + .iter() + .all(|value| value.is_finite()) && self.width() > 0.0 && self.height() > 0.0 } - /// Returns whether a point lies inside or on this rectangle's boundary. - pub fn contains(&self, point: Point) -> bool { - let rect = self.normalized(); - point.x >= rect.left - && point.x <= rect.right - && point.y >= rect.top - && point.y <= rect.bottom - } - - /// Returns whether a point lies inside or on this rectangle's boundary. - pub fn contains_point(&self, x: f32, y: f32) -> bool { - let rect = self.normalized(); - x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom - } - - /// Returns the Euclidean distance from a point to this rectangle. - pub fn distance(&self, p: Point) -> f32 { - (self.left - p.x) - .max(0.0) - .max(p.x - self.right) - .hypot((self.top - p.y).max(0.0).max(p.y - self.bottom)) - } - - /// Returns the scale transform produced by dividing this rectangle's size by another. - pub fn scale(&self, other: &Self) -> Transform { - Transform::from_scale(self.width() / other.width(), self.height() / other.height()) - } -} - -impl Rect { - /// Narrows every edge to `f32`, or returns `None` unless all of them stay finite. - pub fn to_f32(&self) -> Option { - let narrow = |value: f64| value.to_f32().filter(|value| value.is_finite()); - Some(Rect { - left: narrow(self.left)?, - top: narrow(self.top)?, - right: narrow(self.right)?, - bottom: narrow(self.bottom)?, - }) - } - - /// Orders edges so `left` and `top` are the minimum x and y coordinates. - pub fn normalized(&self) -> Self { - Self { - left: self.left.min(self.right), - top: self.top.min(self.bottom), - right: self.left.max(self.right), - bottom: self.top.max(self.bottom), - } - } - - /// Returns the horizontal extent. - pub fn width(&self) -> f64 { - self.right - self.left - } - - /// Returns the vertical extent toward increasing y. - pub fn height(&self) -> f64 { - self.bottom - self.top - } - /// Smallest rectangle containing both, assuming both are normalized. pub fn union(&self, other: &Self) -> Self { Self { @@ -138,39 +82,23 @@ impl Rect { } /// Moves all edges by the given displacement without normalizing or validating. - pub fn translate(&mut self, dx: f64, dy: f64) { + pub fn translate(&mut self, dx: f32, dy: f32) { self.left += dx; self.right += dx; self.top += dy; self.bottom += dy; } - /// Returns whether this rectangle has finite edges and finite, positive dimensions. - pub fn is_valid(&self) -> bool { - [ - self.left, - self.top, - self.right, - self.bottom, - self.width(), - self.height(), - ] - .iter() - .all(|value| value.is_finite()) - && self.width() > 0.0 - && self.height() > 0.0 - } - /// Clamps a displacement so this normalized rectangle stays inside `page` on each axis. /// Oversized rectangles are frozen on that axis; the other axis can still move. /// /// Returns `None` when either rectangle or the displacement is invalid, or when the /// displacement limits overflow. - pub fn clamp_translation(&self, delta: Point, page: &Self) -> Option> { + pub fn clamp_translation(&self, delta: Point, page: &Self) -> Option { if !self.is_valid() || !page.is_valid() || !delta.is_finite() { return None; } - let clamp = |start: f64, end: f64, page_start: f64, page_end: f64, delta: f64| { + let clamp = |start: f32, end: f32, page_start: f32, page_end: f32, delta: f32| { if end - start > page_end - page_start { return Some(0.0); } @@ -189,6 +117,34 @@ impl Rect { moved.translate(result.x, result.y); moved.is_valid().then_some(result) } + + /// Returns whether a point lies inside or on this rectangle's boundary. + pub fn contains(&self, point: Point) -> bool { + let rect = self.normalized(); + point.x >= rect.left + && point.x <= rect.right + && point.y >= rect.top + && point.y <= rect.bottom + } + + /// Returns whether a point lies inside or on this rectangle's boundary. + pub fn contains_point(&self, x: f32, y: f32) -> bool { + let rect = self.normalized(); + x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom + } + + /// Returns the Euclidean distance from a point to this rectangle. + pub fn distance(&self, p: Point) -> f32 { + (self.left - p.x) + .max(0.0) + .max(p.x - self.right) + .hypot((self.top - p.y).max(0.0).max(p.y - self.bottom)) + } + + /// Returns the scale transform produced by dividing this rectangle's size by another. + pub fn scale(&self, other: &Self) -> Transform { + Transform::from_scale(self.width() / other.width(), self.height() / other.height()) + } } impl Rect { @@ -218,18 +174,6 @@ impl Rect { } } -impl From> for Rect { - /// Widens each edge losslessly. - fn from(rect: Rect) -> Self { - Self { - left: f64::from(rect.left), - top: f64::from(rect.top), - right: f64::from(rect.right), - bottom: f64::from(rect.bottom), - } - } -} - impl From<[f32; 4]> for Rect { fn from(arr: [f32; 4]) -> Self { Rect { diff --git a/crates/pdf-graphics/src/size.rs b/crates/pdf-graphics/src/size.rs index 17051257..bad67072 100644 --- a/crates/pdf-graphics/src/size.rs +++ b/crates/pdf-graphics/src/size.rs @@ -3,6 +3,12 @@ /// The type stores the width and height as a width/height pair and is intended /// as the reusable size analogue for the crate's other geometry models. #[derive(Debug, Default, Clone, Copy, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[cfg_attr( + feature = "typescript", + ts(export_to = "annotation_contract.ts", concrete(T = f32)) +)] pub struct Size { /// The horizontal dimension of the size. pub width: T, diff --git a/crates/pdf-graphics/src/transform.rs b/crates/pdf-graphics/src/transform.rs index 29226abf..5ac44196 100644 --- a/crates/pdf-graphics/src/transform.rs +++ b/crates/pdf-graphics/src/transform.rs @@ -13,6 +13,9 @@ pub enum TransformError { /// An affine transformation matrix. #[derive(Debug, Copy, Clone, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[cfg_attr(feature = "typescript", ts(export_to = "annotation_contract.ts"))] pub struct Transform { pub sx: f32, pub kx: f32, diff --git a/crates/pdf-graphics/src/viewport.rs b/crates/pdf-graphics/src/viewport.rs index ec885f4b..7e5df3c6 100644 --- a/crates/pdf-graphics/src/viewport.rs +++ b/crates/pdf-graphics/src/viewport.rs @@ -115,13 +115,12 @@ impl PageViewport { Ok(mapped) } - /// Maps the corners of an `f64` page quad and returns their enclosing device bounds. - /// Rejects corners without a finite `f32` representation or a nonfinite result. - pub fn map_quad(&self, quad: &Quad) -> Result, ViewportError> { + /// Maps the corners of a page quad and returns their enclosing device bounds. + /// Rejects nonfinite corners or a nonfinite result. + pub fn map_quad(&self, quad: &Quad) -> Result { let mut corners = quad.corners; for corner in &mut corners { - let page = corner.to_f32().ok_or(TransformError::NonFinite)?; - *corner = self.map_page_point(page)?.into(); + *corner = self.map_page_point(*corner)?; } Ok(Quad { corners }.bounds()) } diff --git a/crates/pdf-web/js/annotation_contract.ts b/crates/pdf-web/js/annotation_contract.ts index 806a3525..ffa9b231 100644 --- a/crates/pdf-web/js/annotation_contract.ts +++ b/crates/pdf-web/js/annotation_contract.ts @@ -299,7 +299,7 @@ border_effect: BorderEffect | null, /** * Optional left, top, right, bottom difference rectangle. */ -insets: [number, number, number, number] | null, }; +insets: Rect | null, }; /** * Push button that triggers a host-owned action and never holds a value. @@ -328,9 +328,9 @@ properties: CaretAnnotation, }; */ export type CaretAnnotation = { /** - * The difference rectangle. + * The `/RD` difference rectangle: left, top, right, and bottom insets from `/Rect`. */ -difference_rect: [number, number, number, number] | null, +difference_rect: Rect | null, /** * The caret style. */ @@ -411,9 +411,9 @@ interior_color: Color | null, */ border_effect: BorderEffect | null, /** - * The difference rectangle. + * The `/RD` difference rectangle: left, top, right, and bottom insets from `/Rect`. */ -difference_rect: [number, number, number, number] | null, }; +difference_rect: Rect | null, }; /** * Unpremultiplied color with RGBA channel @@ -693,9 +693,9 @@ callout_line: Polyline | null, */ border_effect: BorderEffect | null, /** - * The difference rectangle. + * The `/RD` difference rectangle: left, top, right, and bottom insets from `/Rect`. */ -difference_rect: [number, number, number, number] | null, +difference_rect: Rect | null, /** * The intent. */ @@ -737,7 +737,7 @@ border: FreeTextBorder | null, /** * Nonnegative left, top, right, bottom text insets, in that order. */ -insets: [number, number, number, number], +insets: Rect, /** * Horizontal text alignment. */ @@ -1488,6 +1488,22 @@ dash: Array, */ round: boolean, }; +/** + * A generic two-dimensional size value with a `f32` default coordinate type. + * + * The type stores the width and height as a width/height pair and is intended + * as the reusable size analogue for the crate's other geometry models. + */ +export type Size = { +/** + * The horizontal dimension of the size. + */ +width: number, +/** + * The vertical dimension of the size. + */ +height: number, }; + /** * Native PDF annotation data retained from a page object. */ @@ -1575,9 +1591,9 @@ interior_color: Color | null, */ border_effect: BorderEffect | null, /** - * The difference rectangle. + * The `/RD` difference rectangle: left, top, right, and bottom insets from `/Rect`. */ -difference_rect: [number, number, number, number] | null, }; +difference_rect: Rect | null, }; /** * Owned SquigglyAnnotation data retained from the source PDF. @@ -1712,6 +1728,11 @@ text: string, */ color: Color, }; +/** + * An affine transformation matrix. + */ +export type Transform = { sx: number, kx: number, ky: number, sy: number, tx: number, ty: number, }; + /** * Owned UnderlineAnnotation data retained from the source PDF. */ @@ -1798,20 +1819,20 @@ unsupported: string | null, /** * Device-space interaction rectangles. */ -bounds: Array<[number, number, number, number]>, +bounds: Array, /** * Page-space bottom-left corner of the `size` box; local coordinates are - * `[x - origin[0], origin[1] + size[1] - y]`. + * `[x - origin.x, origin.y + size.height - y]`. */ -origin: [number, number], +origin: Point, /** * Local page-unit width and height. */ -size: [number, number], +size: Size, /** * Local top-left to device affine matrix. */ -transform: [number, number, number, number, number, number], +transform: Transform, /** * Preserve physical size under zoom. */ diff --git a/crates/pdf-web/js/annotation_controls.js b/crates/pdf-web/js/annotation_controls.js index 47e462aa..3975d329 100644 --- a/crates/pdf-web/js/annotation_controls.js +++ b/crates/pdf-web/js/annotation_controls.js @@ -177,7 +177,7 @@ export class FormControl extends AnnotationComponent { updateSelection(node, p) { node.multiple = p.multiple; const rowHeight = this.entry.style.font_size * LINE_SPACING; - node.size = p.list_box ? Math.max(2, Math.floor(this.entry.size[1] / rowHeight)) : 1; + node.size = p.list_box ? Math.max(2, Math.floor(this.entry.size.height / rowHeight)) : 1; for (const option of node.options) option.selected = p.selected.includes(option.index); if (!p.selected.length) node.selectedIndex = -1; if (!this.initialScroll && p.list_box) { node.scrollTop = p.top_index * rowHeight; this.initialScroll = true; } diff --git a/crates/pdf-web/js/annotation_layer.js b/crates/pdf-web/js/annotation_layer.js index 712b8777..ac90eb6c 100644 --- a/crates/pdf-web/js/annotation_layer.js +++ b/crates/pdf-web/js/annotation_layer.js @@ -76,7 +76,7 @@ export class NativeAnnotationLayer { update(component) { const { entry, node, container } = component, { style, css } = entry; component.baseTransform = css.transform; - Object.assign(container.style, { transform: css.transform, width: `${entry.size[0]}px`, height: `${entry.size[1]}px`, opacity: style.opacity, color: css.color }); + Object.assign(container.style, { transform: css.transform, width: `${entry.size.width}px`, height: `${entry.size.height}px`, opacity: style.opacity, color: css.color }); container.dataset.annotationId = String(entry.id); container.dataset.kind = entry.content.kind; container.tabIndex = entry.editable || entry.draggable ? 0 : -1; container.setAttribute('aria-label', entry.text || entry.subtype); diff --git a/crates/pdf-web/js/annotation_visuals.js b/crates/pdf-web/js/annotation_visuals.js index aab4f6d6..5d7c7f3d 100644 --- a/crates/pdf-web/js/annotation_visuals.js +++ b/crates/pdf-web/js/annotation_visuals.js @@ -20,7 +20,7 @@ const paintAttributes = paint => ({ /** Draws Core-provided local-unit shapes; no PDF geometry is interpreted here. * @param {import('./annotation_contract').WebAnnotationEntry} entry */ export function drawSvg(entry) { - const [w, h] = entry.size; + const { width: w, height: h } = entry.size; const svg = svgElement('svg', { viewBox: `0 0 ${w} ${h}`, width: '100%', height: '100%', 'aria-hidden': 'true' }); for (const shape of entry.shapes) { const paint = paintAttributes(shape.paint); diff --git a/crates/pdf-web/src/annotation_entry.rs b/crates/pdf-web/src/annotation_entry.rs index 5e7ec954..75c938ec 100644 --- a/crates/pdf-web/src/annotation_entry.rs +++ b/crates/pdf-web/src/annotation_entry.rs @@ -95,25 +95,15 @@ pub fn css_color(color: Option) -> String { /// Local-to-CSS matrix; no-zoom keeps unit scale and no-rotate drops rotation and skew. fn css_matrix(entry: &AnnotationEntry, device_to_css: &Transform) -> String { - let d = [ - device_to_css.sx, - device_to_css.ky, - device_to_css.kx, - device_to_css.sy, - device_to_css.tx, - device_to_css.ty, - ] - .map(f64::from); - let t = entry.transform; // device_to_css × transform, in CSS `matrix(a,b,c,d,e,f)` component order. - let [mut a, mut b, mut c, mut dd] = [ - d[0] * t[0] + d[2] * t[1], - d[1] * t[0] + d[3] * t[1], - d[0] * t[2] + d[2] * t[3], - d[1] * t[2] + d[3] * t[3], - ]; - let e = d[0] * t[4] + d[2] * t[5] + d[4]; - let f = d[1] * t[4] + d[3] * t[5] + d[5]; + let Transform { + sx: mut a, + ky: mut b, + kx: mut c, + sy: mut dd, + tx: e, + ty: f, + } = device_to_css.post_concatenated(&entry.transform); if entry.no_zoom { let (x, y) = (a.hypot(b), c.hypot(dd)); if x > 0.0 { diff --git a/crates/pdf-web/src/annotation_overlay.rs b/crates/pdf-web/src/annotation_overlay.rs index d3de21bd..286a5da8 100644 --- a/crates/pdf-web/src/annotation_overlay.rs +++ b/crates/pdf-web/src/annotation_overlay.rs @@ -134,7 +134,7 @@ impl WebAnnotationOverlay { &mut self, command: AnnotationCommandRequest, viewport: Option<&WebViewport>, - page_bounds: Option>, + page_bounds: Option, ) -> WebResult { Ok(self .overlay diff --git a/examples/web-canvas/renderer.rs b/examples/web-canvas/renderer.rs index bfe88bd9..49cda488 100644 --- a/examples/web-canvas/renderer.rs +++ b/examples/web-canvas/renderer.rs @@ -172,15 +172,7 @@ impl WebDocument { .and_then(|p| usize::try_from(p).ok()) .and_then(|p| self.document.pages.get(p)) .and_then(|p| p.crop_box.or(p.media_box)) - .map(|r| { - let r = r.normalized(); - pdf_graphics::rect::Rect { - left: f64::from(r.left), - top: f64::from(r.top), - right: f64::from(r.right), - bottom: f64::from(r.bottom), - } - }); + .map(|r| r.normalized()); let receipt = self .overlay .accept_event(event, viewport, bounds) diff --git a/tools/conformance/src/worker.rs b/tools/conformance/src/worker.rs index cdb83728..d96b7f2c 100644 --- a/tools/conformance/src/worker.rs +++ b/tools/conformance/src/worker.rs @@ -197,7 +197,7 @@ fn annotation_boxes(page: &pdf_document::page::PdfPage, width: u32, height: u32) .flatten() .filter(|annotation| annotation.subtype != b"Link" && annotation.subtype != b"Popup") .filter_map(|annotation| { - let rect = annotation.rect.as_ref()?.to_f32()?; + let rect = annotation.rect?; let mapped = viewport.map_rect(&rect).ok()?; let clamp = |value: f32, limit: u32| value.max(0.0).to_u32().map(|v| v.min(limit)); Some([