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([