From b35e3c66ae8c3aa34b26dc7c48bbf7d7b23ab50b Mon Sep 17 00:00:00 2001 From: Alastair Carey Date: Tue, 17 Dec 2024 20:14:13 +0000 Subject: [PATCH] Progressing #168 --- Cargo.toml | 2 +- README.md | 2 +- examples/copy_objects.rs | 2 +- examples/create_annotations.rs | 2 +- .../page/annotation/attachment_points.rs | 3 +- src/pdf/document/page/annotations.rs | 24 +-- src/pdf/document/page/object.rs | 21 +- src/pdf/document/page/paragraph.rs | 20 +- src/pdf/document/page/text.rs | 6 +- src/pdf/points.rs | 6 + src/pdf/quad_points.rs | 203 +++++++++++++++++- src/pdf/rect.rs | 9 +- 12 files changed, 254 insertions(+), 46 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index bc2d1ab..e2b60ab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pdfium-render" -version = "0.8.27" +version = "0.8.28" edition = "2018" rust-version = "1.61" publish = true diff --git a/README.md b/README.md index 75bbe61..f6d9ac9 100644 --- a/README.md +++ b/README.md @@ -300,7 +300,7 @@ will be removed in release 0.9.0. ## Version history -* 0.8.27: adds new `axum_once_cell` example; fixes a WASM-specific bug in string termination detection when copying `FPDF_WIDESTRING` buffers to Pdfium's WASM address space; deprecates the memory-unsafe WASM implementation of `FPDFBitmap_GetBuffer` in favour of `FPDFBitmap_GetBuffer_as_vec` and `FPDFBitmap_GetBuffer_as_array` (renamed from `FPDFBitmap_GetArray`); establishes upper bound on `wasm-bindgen` dependency to avoid a build failure during Github workflow automated build that was introduced in `wasm-bindgen-macros` versions 0.2.96 and later, as described at ; completes expansion of all `FPDF_*` doc comments in `PdfiumLibraryBindings` trait. Deprecated items will be removed in release 0.9.0. +* 0.8.27: adds new `examples/axum_once_cell.rs` example, thanks to an excellent contribution from ; fixes a WASM-specific bug in string termination detection when copying `FPDF_WIDESTRING` buffers to Pdfium's WASM address space; deprecates the memory-unsafe WASM implementation of `FPDFBitmap_GetBuffer` in favour of `FPDFBitmap_GetBuffer_as_vec` and `FPDFBitmap_GetBuffer_as_array` (renamed from `FPDFBitmap_GetArray`), thanks to an excellent contribution from ; establishes upper bound on `wasm-bindgen` dependency to avoid a build failure during Github workflow automated build that was introduced in `wasm-bindgen-macros` versions 0.2.96 and later, as described at ; completes expansion of all `FPDF_*` doc comments in `PdfiumLibraryBindings` trait. Deprecated items will be removed in release 0.9.0. * 0.8.26: relaxes version constraint on `chrono` dependency based on user feedback, relaxing the minimum supported Rust version to 1.61; increments the `pdfium_latest` feature to `pdfium_6721` to match new Pdfium release 6721 at ; synchronises `pdfium_future` feature against current Google mainline; adds new crate features `image_025`, `image_024`, and `image_023` to allow explicitly pinning the version of `image` that should be used by `pdfium-render`, sets `image` to `image_025`; adjusts bookmark traversal so that bookmarks are returned in a more natural order, thanks to an excellent contribution from . * 0.8.25: adds bindings for all outstanding Pdfium API functions, including `FPDFAnnot_GetFormAdditionalActionJavaScript()`, `FPDFBitmap_Create()`, `FPDFAnnot_GetFormFieldAlternateName()`, `FPDFAnnot_GetFileAttachment()`, `FPDFAnnot_AddFileAttachment()`, `FPDFAvail_Create()`, `FPDFAvail_Destroy()`, `FPDFAvail_IsDocAvail()`, `FPDFAvail_GetDocument()`, `FPDFAvail_GetFirstPageNum()`, `FPDFAvail_IsPageAvail()`, `FPDFAvail_IsFormAvail()`, `FPDFAvail_IsLinearized()`, `FPDFAnnot_GetFontColor()`, `FPDFPageObj_TransformF()`, `FPDFPageObj_GetMarkedContentID()`, `FPDF_StructElement_GetActualText()`, `FPDF_StructElement_GetObjType()`, `FPDF_StructElement_GetChildMarkedContentID()`, `FPDF_StructElement_GetAttributeAtIndex()`, `FPDF_StructElement_Attr_GetName()`, `FPDF_StructElement_Attr_GetValue()`, `FPDF_StructElement_Attr_GetType()`, `FPDF_StructElement_Attr_GetBooleanValue()`, `FPDF_StructElement_Attr_GetNumberValue()`, `FPDF_StructElement_Attr_GetStringValue()`, `FPDF_StructElement_Attr_GetBlobValue()`, `FPDF_StructElement_Attr_GetChildAtIndex()`, `FPDF_StructElement_GetMarkedContentIdCount()`, `FPDF_StructElement_GetMarkedContentIdAtIndex()`, `FPDF_ARGB()`, `FPDF_GetBValue()`, `FPDF_GetGValue()`, `FPDF_GetRValue()`, `FPDF_MovePages()`, `FPDFImageObj_GetImagePixelSize()`, `FPDFPageObj_GetRotatedBounds()`, `FPDFText_LoadCidType2Font()`, `FPDFTextObj_GetRenderedBitmap()`, `FPDF_FFLDrawSkia()`, `FPDF_NewXObjectFromPage()`, `FPDF_CloseXObject()`, `FPDF_NewFormObjectFromXObject()`, `FPDF_CopyViewerPreferences()`, `FPDF_RenderPageBitmapWithColorScheme_Start()`, `FPDF_RenderPageBitmap_Start()`, `FPDF_RenderPage_Continue()`, `FPDF_RenderPage_Close()`, `FPDF_GetDefaultTTFMap()`, `FPDF_GetDefaultTTFMapCount()`, `FPDF_GetDefaultTTFMapEntry()`, `FPDF_AddInstalledFont()`, `FPDFText_IsGenerated()`, `FPDFText_IsHyphen()`, `FPDFText_HasUnicodeMapError()`, `FPDF_InitLibraryWithConfig()`, `FPDF_SetSandBoxPolicy()`, `FPDF_SetPrintMode()`, `FPDF_DocumentHasValidCrossReferenceTable()`, `FPDF_GetTrailerEnds()`, `FPDF_GetDocUserPermissions()`, `FPDF_GetPageWidth()` `FPDF_GetPageHeight()` `FPDF_GetPageSizeByIndex()` `FPDF_RenderPageSkia()`, `FPDF_CountNamedDests()`, `FPDF_GetNamedDestByName()`, `FPDF_GetNamedDest()`, `FPDF_GetXFAPacketCount()`, `FPDF_GetXFAPacketName()`, `FPDF_GetXFAPacketContent()`, `FPDF_GetRecommendedV8Flags()`, `FPDF_GetArrayBufferAllocatorSharedInstance()`, `FPDF_BStr_Init()`, `FPDF_BStr_Set()`, `FPDF_BStr_Clear()`, `FPDF_SetPrintMode()`, and `FPDF_RenderPage()`; adds `pfdium_use_skia`,`pdfium_use_win32`, `pdfium_enable_xfa`, and `pdfium_enable_v8` crate feature flags; adjusts dependency versions in `Cargo.toml` to meet a minimum supported Rust version (MSRV) of Rust 1.60; establishes upper bound on `bindgen` dependency to avoid a build failure when compiling to WASM that was introduced in `bindgen` versions 0.70.0 and later, as described at . * 0.8.24: introduces crate feature flags for selecting Pdfium API versions to use in `PdfiumLibraryBindings`; reworked `build.rs` to output bindings for multiple sets of Pdfium header files; reworks bindings implementations to differentiate between API versions that include the `FPDFFont_*` and `FPDFText_GetTextObject()` functions added in 0.8.23, and API versions that do not; reinstates `FPDFText_GetTextRenderMode()` function removed in 0.8.23 behind feature flags; adds WASM bindings utility function `copy_string_to_pdfium()` to correctly copy the string data of an `FPDF_WIDESTRING` to Pdfium's WASM memory module, instead of just the pointer location; adds `PdfiumLibraryBindings::version()` function for reporting the currently configured API version; internally reorganizes source code layout to make the code structure clearer. diff --git a/examples/copy_objects.rs b/examples/copy_objects.rs index e66c7f6..21e5757 100644 --- a/examples/copy_objects.rs +++ b/examples/copy_objects.rs @@ -29,7 +29,7 @@ pub fn main() -> Result<(), PdfiumError> { .map(|bounds| { // Only select objects on the bottom half of the page. - bounds.top < source_page.height() / 2.0 + bounds.top() < source_page.height() / 2.0 }) .unwrap_or(false) })?; diff --git a/examples/create_annotations.rs b/examples/create_annotations.rs index 9fae3b0..1beee6a 100644 --- a/examples/create_annotations.rs +++ b/examples/create_annotations.rs @@ -102,7 +102,7 @@ pub fn main() -> Result<(), PdfiumError> { link_annotation.set_height(PdfPoints::new(50.0))?; link_annotation .attachment_points_mut() - .create_attachment_point_at_end(PdfQuadPoints::from_rect(PdfRect::new_from_values( + .create_attachment_point_at_end(PdfQuadPoints::from_rect(&PdfRect::new_from_values( 100.0, 100.0, 150.0, 150.0, )))?; diff --git a/src/pdf/document/page/annotation/attachment_points.rs b/src/pdf/document/page/annotation/attachment_points.rs index 4670c37..fac1cc2 100644 --- a/src/pdf/document/page/annotation/attachment_points.rs +++ b/src/pdf/document/page/annotation/attachment_points.rs @@ -6,7 +6,6 @@ use crate::bindgen::FPDF_ANNOTATION; use crate::bindings::PdfiumLibraryBindings; use crate::error::{PdfiumError, PdfiumInternalError}; use crate::pdf::quad_points::PdfQuadPoints; -use crate::pdf::rect::PdfRect; use std::ops::{Range, RangeInclusive}; /// The zero-based index of a single attachment point inside its containing @@ -82,7 +81,7 @@ impl<'a> PdfPageAnnotationAttachmentPoints<'a> { return Err(PdfiumError::PageAnnotationAttachmentPointIndexOutOfBounds); } - let mut result = PdfQuadPoints::from_rect(PdfRect::ZERO).as_pdfium(); + let mut result = PdfQuadPoints::ZERO.as_pdfium(); if self .bindings diff --git a/src/pdf/document/page/annotations.rs b/src/pdf/document/page/annotations.rs index 7637ae2..9615953 100644 --- a/src/pdf/document/page/annotations.rs +++ b/src/pdf/document/page/annotations.rs @@ -414,24 +414,24 @@ impl<'a> PdfPageAnnotations<'a> { let bounds = object.bounds()?; - annotation.set_position(bounds.left, bounds.bottom)?; + annotation.set_position(bounds.left(), bounds.bottom())?; annotation.set_stroke_color(color)?; const SQUIGGLY_HEIGHT: f32 = 12.0; - let annotation_top = bounds.bottom.value - 5.0; + let annotation_top = bounds.bottom().value - 5.0; let annotation_bottom = annotation_top - SQUIGGLY_HEIGHT; annotation .attachment_points_mut() .create_attachment_point_at_end(PdfQuadPoints::new_from_values( - bounds.left.value, + bounds.left().value, annotation_bottom, - bounds.right.value, + bounds.right().value, annotation_bottom, - bounds.right.value, + bounds.right().value, annotation_top, - bounds.left.value, + bounds.left().value, annotation_top, ))?; @@ -467,11 +467,11 @@ impl<'a> PdfPageAnnotations<'a> { let bounds = object.bounds()?; - annotation.set_position(bounds.left, bounds.bottom)?; + annotation.set_position(bounds.left(), bounds.bottom())?; annotation.set_stroke_color(color)?; annotation .attachment_points_mut() - .create_attachment_point_at_end(PdfQuadPoints::from_rect(bounds))?; + .create_attachment_point_at_end(bounds)?; if let Some(contents) = contents { annotation.set_width(bounds.width())?; @@ -505,11 +505,11 @@ impl<'a> PdfPageAnnotations<'a> { let bounds = object.bounds()?; - annotation.set_position(bounds.left, bounds.bottom)?; + annotation.set_position(bounds.left(), bounds.bottom())?; annotation.set_stroke_color(color)?; annotation .attachment_points_mut() - .create_attachment_point_at_end(PdfQuadPoints::from_rect(bounds))?; + .create_attachment_point_at_end(bounds)?; if let Some(contents) = contents { annotation.set_width(bounds.width())?; @@ -543,11 +543,11 @@ impl<'a> PdfPageAnnotations<'a> { let bounds = object.bounds()?; - annotation.set_position(bounds.left, bounds.bottom)?; + annotation.set_position(bounds.left(), bounds.bottom())?; annotation.set_stroke_color(color)?; annotation .attachment_points_mut() - .create_attachment_point_at_end(PdfQuadPoints::from_rect(bounds))?; + .create_attachment_point_at_end(bounds)?; if let Some(contents) = contents { annotation.set_width(bounds.width())?; diff --git a/src/pdf/document/page/object.rs b/src/pdf/document/page/object.rs index 97cd5b0..906fdb1 100644 --- a/src/pdf/document/page/object.rs +++ b/src/pdf/document/page/object.rs @@ -30,6 +30,7 @@ use crate::pdf::document::page::objects::PdfPageObjects; use crate::pdf::document::PdfDocument; use crate::pdf::matrix::{PdfMatrix, PdfMatrixValue}; use crate::pdf::points::PdfPoints; +use crate::pdf::quad_points::PdfQuadPoints; use crate::pdf::rect::PdfRect; use crate::{create_transform_getters, create_transform_setters}; use std::convert::TryInto; @@ -547,13 +548,13 @@ pub trait PdfPageObjectCommon<'a> { /// Returns `true` if this [PdfPageObject] contains transparency. fn has_transparency(&self) -> bool; - /// Returns the bounding box of this [PdfPageObject]. + /// Returns the bounding box of this [PdfPageObject] as a quadrilateral. /// /// For text objects, the bottom of the bounding box is set to the font baseline. Any characters /// in the text object that have glyph shapes that descends below the font baseline will extend /// beneath the bottom of this bounding box. To measure the distance of the maximum descent of /// any glyphs, use the [PdfPageTextObject::descent()] function. - fn bounds(&self) -> Result; + fn bounds(&self) -> Result; /// Returns the width of this [PdfPageObject]. #[inline] @@ -571,7 +572,7 @@ pub trait PdfPageObjectCommon<'a> { #[inline] fn is_inside_rect(&self, rect: &PdfRect) -> bool { self.bounds() - .map(|bounds| bounds.is_inside(rect)) + .map(|bounds| bounds.to_rect().is_inside(rect)) .unwrap_or(false) } @@ -580,7 +581,7 @@ pub trait PdfPageObjectCommon<'a> { #[inline] fn does_overlap_rect(&self, rect: &PdfRect) -> bool { self.bounds() - .map(|bounds| bounds.does_overlap(rect)) + .map(|bounds| bounds.to_rect().does_overlap(rect)) .unwrap_or(false) } @@ -765,7 +766,7 @@ where } #[inline] - fn bounds(&self) -> Result { + fn bounds(&self) -> Result { self.bounds_impl() } @@ -1045,6 +1046,16 @@ impl<'a> PdfPageObjectPrivate<'a> for PdfPageObject<'a> { self.unwrap_as_trait().get_object_handle() } + #[inline] + fn get_document_handle(&self) -> Option { + self.unwrap_as_trait().get_document_handle() + } + + #[inline] + fn set_document_handle(&mut self, document: FPDF_DOCUMENT) { + self.unwrap_as_trait_mut().set_document_handle(document); + } + #[inline] fn get_page_handle(&self) -> Option { self.unwrap_as_trait().get_page_handle() diff --git a/src/pdf/document/page/paragraph.rs b/src/pdf/document/page/paragraph.rs index 4a4dddc..7342f4a 100644 --- a/src/pdf/document/page/paragraph.rs +++ b/src/pdf/document/page/paragraph.rs @@ -5,14 +5,14 @@ use crate::bindgen::FPDF_PAGEOBJECT; use crate::error::PdfiumError; -use crate::pdf::document::PdfDocument; use crate::pdf::document::fonts::PdfFontToken; -use crate::pdf::document::page::PdfPage; -use crate::pdf::document::page::object::{PdfPageObject, PdfPageObjectCommon}; use crate::pdf::document::page::object::group::PdfPageGroupObject; use crate::pdf::document::page::object::private::internal::PdfPageObjectPrivate; use crate::pdf::document::page::object::text::PdfPageTextObject; +use crate::pdf::document::page::object::{PdfPageObject, PdfPageObjectCommon}; use crate::pdf::document::page::objects::common::PdfPageObjectsCommon; +use crate::pdf::document::page::PdfPage; +use crate::pdf::document::PdfDocument; use crate::pdf::font::PdfFont; use crate::pdf::points::PdfPoints; use itertools::Itertools; @@ -291,7 +291,7 @@ impl<'a> PdfParagraph<'a> { .map(|object| { let object_bottom = object .bounds() - .map(|bounds| bounds.bottom) + .map(|bounds| bounds.bottom()) .unwrap_or(PdfPoints::ZERO); match objects_bottom { @@ -305,7 +305,7 @@ impl<'a> PdfParagraph<'a> { let object_top = object .bounds() - .map(|bounds| bounds.top) + .map(|bounds| bounds.top()) .unwrap_or(PdfPoints::ZERO); match objects_top { @@ -324,7 +324,7 @@ impl<'a> PdfParagraph<'a> { let object_left = object .bounds() - .map(|bounds| bounds.left) + .map(|bounds| bounds.left()) .unwrap_or(PdfPoints::ZERO); match objects_left { @@ -338,7 +338,7 @@ impl<'a> PdfParagraph<'a> { let object_right = object .bounds() - .map(|bounds| bounds.right) + .map(|bounds| bounds.right()) .unwrap_or(PdfPoints::ZERO); match objects_right { @@ -472,7 +472,7 @@ impl<'a> PdfParagraph<'a> { let separator = if let Ok(bounds) = object.bounds() { if let Some(last_object_right) = last_object_right { - if last_object_right > bounds.left { + if last_object_right > bounds.left() { // The last and current objects are touching. // Assume they're part of the same word, despite being // in separate objects. @@ -507,7 +507,7 @@ impl<'a> PdfParagraph<'a> { println!( "last_object_right = {:?}, this object left = {:?}", last_object_right, - object.bounds().unwrap().left, + object.bounds().unwrap().left(), ); } else { // The styles of the two text objects are different, so they can't be merged. @@ -887,4 +887,4 @@ mod tests { Ok(()) } -} \ No newline at end of file +} diff --git a/src/pdf/document/page/text.rs b/src/pdf/document/page/text.rs index e4a5438..b410a5b 100644 --- a/src/pdf/document/page/text.rs +++ b/src/pdf/document/page/text.rs @@ -179,11 +179,11 @@ impl<'a> PdfPageText<'a> { let text_page = new_page.text()?; let tolerance_x = bounds.width() / 2.0; let tolerance_y = bounds.height() / 2.0; - let center_height = bounds.bottom + tolerance_y; + let center_height = bounds.bottom() + tolerance_y; let start_index = Self::get_char_index_near_point( *text_page.handle(), - bounds.left, + bounds.left(), tolerance_x, center_height, tolerance_y, @@ -193,7 +193,7 @@ impl<'a> PdfPageText<'a> { let end_index = Self::get_char_index_near_point( *text_page.handle(), - bounds.right, + bounds.right(), tolerance_x, center_height, tolerance_y, diff --git a/src/pdf/points.rs b/src/pdf/points.rs index a8f8e75..d66d280 100644 --- a/src/pdf/points.rs +++ b/src/pdf/points.rs @@ -95,6 +95,12 @@ impl PdfPoints { pub fn to_mm(&self) -> f32 { self.to_cm() * 10.0 } + + /// Creates a new [PdfPoints] object with the absolute value of this [PdfPoints] object. + #[inline] + pub fn abs(&self) -> PdfPoints { + PdfPoints::new(self.value.abs()) + } } impl Add for PdfPoints { diff --git a/src/pdf/quad_points.rs b/src/pdf/quad_points.rs index 999a3fc..e837055 100644 --- a/src/pdf/quad_points.rs +++ b/src/pdf/quad_points.rs @@ -1,10 +1,14 @@ //! Defines the [PdfQuadPoints] struct, a set of four coordinates expressed in [PdfPoints] //! that outline the bounds of a four-sided quadrilateral. -use crate::bindgen::FS_QUADPOINTSF; +use crate::bindgen::{FPDF_BOOL, FS_QUADPOINTSF}; +use crate::bindings::PdfiumLibraryBindings; +use crate::error::{PdfiumError, PdfiumInternalError}; +use crate::pdf::matrix::PdfMatrix; use crate::pdf::points::PdfPoints; use crate::pdf::rect::PdfRect; use std::fmt::{Display, Formatter}; +use std::hash::{Hash, Hasher}; /// A set of four coordinates expressed in [PdfPoints] that outline the bounds of a /// four-sided quadrilateral. The coordinates specify the quadrilateral's four vertices @@ -32,6 +36,9 @@ pub struct PdfQuadPoints { } impl PdfQuadPoints { + /// A [PdfQuadPoints] object with the identity value (0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0). + pub const ZERO: PdfQuadPoints = PdfQuadPoints::zero(); + #[inline] pub(crate) fn from_pdfium(points: FS_QUADPOINTSF) -> Self { PdfQuadPoints::new_from_values( @@ -39,6 +46,21 @@ impl PdfQuadPoints { ) } + #[inline] + pub(crate) fn from_pdfium_as_result( + result: FPDF_BOOL, + points: FS_QUADPOINTSF, + bindings: &dyn PdfiumLibraryBindings, + ) -> Result { + if !bindings.is_true(result) { + Err(PdfiumError::PdfiumLibraryInternalError( + PdfiumInternalError::Unknown, + )) + } else { + Ok(PdfQuadPoints::from_pdfium(points)) + } + } + /// Creates a new [PdfQuadPoints] from the given [PdfPoints] coordinate pairs. /// /// The coordinate space of a `PdfPage` has its origin (0,0) at the bottom left of the page, @@ -46,7 +68,7 @@ impl PdfQuadPoints { /// y values increasing as coordinates move vertically up. #[inline] #[allow(clippy::too_many_arguments)] - pub fn new( + pub const fn new( x1: PdfPoints, y1: PdfPoints, x2: PdfPoints, @@ -75,7 +97,7 @@ impl PdfQuadPoints { /// y values increasing as coordinates move vertically up. #[inline] #[allow(clippy::too_many_arguments)] - pub fn new_from_values( + pub const fn new_from_values( x1: f32, y1: f32, x2: f32, @@ -97,18 +119,97 @@ impl PdfQuadPoints { } } + /// Creates a new [PdfQuadPoints] object with all values set to 0.0. + /// + /// Consider using the compile-time constant value [PdfQuadPoints::ZERO] + /// rather than calling this function directly. + #[inline] + pub const fn zero() -> Self { + Self::new_from_values(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0) + } + /// Creates a new [PdfQuadPoints] from the given [PdfRect]. #[inline] - pub fn from_rect(rect: PdfRect) -> Self { + pub fn from_rect(rect: &PdfRect) -> Self { PdfQuadPoints::new( - rect.left, - rect.top, - rect.right, - rect.top, rect.left, rect.bottom, rect.right, rect.bottom, + rect.right, + rect.top, + rect.left, + rect.top, + ) + } + + /// Returns the left-most extent of this [PdfQuadPoints]. + pub fn left(&self) -> PdfPoints { + *vec![self.x1, self.x2, self.x3, self.x4] + .iter() + .min() + .unwrap() + } + + /// Returns the right-most extent of this [PdfQuadPoints]. + pub fn right(&self) -> PdfPoints { + *vec![self.x1, self.x2, self.x3, self.x4] + .iter() + .max() + .unwrap() + } + + /// Returns the bottom-most extent of this [PdfQuadPoints]. + pub fn bottom(&self) -> PdfPoints { + *vec![self.y1, self.y2, self.y3, self.y4] + .iter() + .min() + .unwrap() + } + + /// Returns the top-most extent of this [PdfQuadPoints]. + pub fn top(&self) -> PdfPoints { + *vec![self.y1, self.y2, self.y3, self.y4] + .iter() + .max() + .unwrap() + } + + /// Returns the width of this [PdfQuadPoints]. + #[inline] + pub fn width(&self) -> PdfPoints { + self.right() - self.left() + } + + /// Returns the height of this [PdfQuadPoints]. + #[inline] + pub fn height(&self) -> PdfPoints { + self.top() - self.bottom() + } + + /// Returns the result of applying the given [PdfMatrix] to each corner point + // of this [PdfQuadPoints]. + #[inline] + pub fn transform(&self, matrix: PdfMatrix) -> PdfQuadPoints { + let (x1, y1) = matrix.apply_to_points(self.x1, self.y1); + let (x2, y2) = matrix.apply_to_points(self.x2, self.y2); + let (x3, y3) = matrix.apply_to_points(self.x3, self.y3); + let (x4, y4) = matrix.apply_to_points(self.x4, self.y4); + + PdfQuadPoints::new(x1, y1, x2, y2, x3, y3, x4, y4) + } + + /// Returns the smallest [PdfRect] that can completely enclose the quadrilateral + /// outlined by this [PdfQuadPoints]. + pub fn to_rect(&self) -> PdfRect { + let xs = vec![self.x1, self.x2, self.x3, self.x4]; + let ys = vec![self.y1, self.y2, self.y3, self.y4]; + + PdfRect::new( + *ys.iter().min().unwrap(), + *xs.iter().min().unwrap(), + *ys.iter().max().unwrap(), + *xs.iter().max().unwrap(), ) } @@ -127,11 +228,45 @@ impl PdfQuadPoints { } } +// We could derive PartialEq automatically, but it's good practice to implement PartialEq +// by hand when implementing Hash. + +impl PartialEq for PdfQuadPoints { + fn eq(&self, other: &Self) -> bool { + self.x1 == other.x1 + && self.y1 == other.y1 + && self.x2 == other.x2 + && self.y2 == other.y2 + && self.x3 == other.x3 + && self.y3 == other.y3 + && self.x4 == other.x4 + && self.y4 == other.y4 + } +} + +// The f32 values inside PdfQuadPoints will never be NaN or Infinity, so these implementations +// of Eq and Hash are safe. + +impl Eq for PdfQuadPoints {} + +impl Hash for PdfQuadPoints { + fn hash(&self, state: &mut H) { + state.write_u32(self.x1.value.to_bits()); + state.write_u32(self.y1.value.to_bits()); + state.write_u32(self.x2.value.to_bits()); + state.write_u32(self.y2.value.to_bits()); + state.write_u32(self.x3.value.to_bits()); + state.write_u32(self.y3.value.to_bits()); + state.write_u32(self.x4.value.to_bits()); + state.write_u32(self.y4.value.to_bits()); + } +} + impl Display for PdfQuadPoints { #[inline] fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.write_fmt(format_args!( - "PdfQuadPoints(x1: {}, y1: {}, x2: {}, y2: {}, x3: {}, y3: {}, x4: {}, y4: {}", + "PdfQuadPoints(x1: {}, y1: {}, x2: {}, y2: {}, x3: {}, y3: {}, x4: {}, y4: {})", self.x1.value, self.y1.value, self.x2.value, @@ -143,3 +278,53 @@ impl Display for PdfQuadPoints { )) } } + +#[cfg(test)] +mod tests { + use crate::prelude::*; + + #[test] + fn test_quadpoints_extents() { + let r = PdfRect::new_from_values(50.0, 100.0, 300.0, 200.0); + + assert_eq!(r.to_quad_points().left().value, 100.0); + assert_eq!(r.to_quad_points().right().value, 200.0); + assert_eq!(r.to_quad_points().top().value, 300.0); + assert_eq!(r.to_quad_points().bottom().value, 50.0); + + assert_eq!(r.to_quad_points().width().value, 100.0); + assert_eq!(r.to_quad_points().height().value, 250.0); + } + + #[test] + fn test_quadpoints_to_rect() { + let r = PdfRect::new_from_values(100.0, 100.0, 200.0, 200.0); + assert_eq!(r.to_quad_points().to_rect(), r); + + let m = PdfMatrix::identity() + .rotate_clockwise_degrees(45.0) + .unwrap(); + let r45 = r.transform(m); + + let q = r.to_quad_points(); + let q45 = q.transform(m); + + assert_eq!(q.to_rect(), r); + assert_eq!(q45.to_rect(), r45); + + // It would be incredibly elegant to test + + // assert_eq!(q45.transform(m.invert()).to_rect(), r); + + // but sadly floating point rounding errors means the double-transformed values + // are ever-so-slightly off (by a fraction of a PdfPoint). Let's test manually + // so we can apply a comparison threshold. + + let s = q45.transform(m.invert()).to_rect(); + let threshold = PdfPoints::new(0.001); + assert!((s.top - r.top).abs() < threshold); + assert!((s.bottom - r.bottom).abs() < threshold); + assert!((s.left - r.left).abs() < threshold); + assert!((s.right - r.right).abs() < threshold); + } +} diff --git a/src/pdf/rect.rs b/src/pdf/rect.rs index df1bbb4..a35f557 100644 --- a/src/pdf/rect.rs +++ b/src/pdf/rect.rs @@ -5,6 +5,7 @@ use crate::bindings::PdfiumLibraryBindings; use crate::error::{PdfiumError, PdfiumInternalError}; use crate::pdf::matrix::PdfMatrix; use crate::pdf::points::PdfPoints; +use crate::pdf::quad_points::PdfQuadPoints; use itertools::{max, min}; use std::fmt::{Display, Formatter}; use std::hash::{Hash, Hasher}; @@ -166,6 +167,12 @@ impl PdfRect { ) } + /// Returns the [PdfQuadPoints] quadrilateral representation of this [PdfRect]. + #[inline] + pub fn to_quad_points(&self) -> PdfQuadPoints { + PdfQuadPoints::from_rect(self) + } + #[inline] pub(crate) fn as_pdfium(&self) -> FS_RECTF { FS_RECTF { @@ -207,7 +214,7 @@ impl Display for PdfRect { #[inline] fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.write_fmt(format_args!( - "PdfRect(bottom: {}, left: {}, top: {}, right: {}", + "PdfRect(bottom: {}, left: {}, top: {}, right: {})", self.bottom.value, self.left.value, self.top.value, self.right.value )) }