From 3732576fbbc28bba96bc5d15f394feec3a494890 Mon Sep 17 00:00:00 2001 From: Julien Roncaglia Date: Fri, 25 Oct 2024 16:39:55 +0200 Subject: [PATCH 1/2] Introduce text_size_info and change back where text is positioned text_size_info can give both the box where the font wants to be logically placed to form a line (But depending on the font it might draw pixels out of this box) and the pixel-bounding box where pixels were really placed. --- src/drawing/text.rs | 117 +++++++++++++++++++++++++++----------- src/morphology.rs | 4 +- src/rect.rs | 65 +++++++++++++++++++++ tests/data/truth/text.png | Bin 3079 -> 2978 bytes tests/regression.rs | 9 ++- 5 files changed, 154 insertions(+), 41 deletions(-) diff --git a/src/drawing/text.rs b/src/drawing/text.rs index 13319afb..ce9067e3 100644 --- a/src/drawing/text.rs +++ b/src/drawing/text.rs @@ -4,20 +4,35 @@ use std::f32; use crate::definitions::{Clamp, Image}; use crate::drawing::Canvas; use crate::pixelops::weighted_sum; - -use ab_glyph::{point, Font, GlyphId, OutlinedGlyph, PxScale, Rect, ScaleFont}; +use crate::rect::Rect; + +use ab_glyph::{point, Font, GlyphId, OutlinedGlyph, PxScale, ScaleFont}; + +/// The size a text will take up when rendered with the given font and scale. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct TextSizeInfo { + /// The bounding box of all the pixels of the text. Might be [`None`] if the text is empty or only contains + /// whitespace. + /// + /// As some fonts have glyphs that extend above or below the line, this is not the same as the `outline_bounds`. + /// Coordinates might be negative or exceed the [`outline_bounds`]. + pub px_bounds: Option, + + /// The logical bounding box of the text. Might be [`None`] if the text is empty. + /// + /// Can be used to position other text relative to this text. + pub outline_bounds: Option, +} fn layout_glyphs( scale: impl Into + Copy, font: &impl Font, text: &str, - mut f: impl FnMut(OutlinedGlyph, Rect), -) -> (u32, u32) { - if text.is_empty() { - return (0, 0); - } + mut f: impl FnMut(OutlinedGlyph, ab_glyph::Rect), +) -> TextSizeInfo { let font = font.as_scaled(scale); + let mut px_bounds: Option = None; let mut w = 0.0; let mut prev: Option = None; @@ -31,20 +46,50 @@ fn layout_glyphs( } prev = Some(glyph_id); let bb = g.px_bounds(); + + if let Some(current) = px_bounds { + px_bounds = Some(current.union(bb.into())) + } else { + px_bounds = Some(bb.into()); + } + f(g, bb); } } - let w = w.ceil(); - let h = font.height().ceil(); - assert!(w >= 0.0); - assert!(h >= 0.0); - (1 + w as u32, h as u32) + let outline_width = w.ceil() as u32; + let outline_height = font.height().ceil() as u32; + let outline_bounds = if outline_height > 0 && outline_width > 0 { + Some(Rect::at(0, 0).of_size(outline_width, outline_height)) + } else { + None + }; + + TextSizeInfo { + px_bounds, + outline_bounds, + } +} + +/// Get the sizing info of the given text, rendered with the given font and scale. +pub fn text_size_info( + scale: impl Into + Copy, + font: &impl Font, + text: &str, +) -> TextSizeInfo { + layout_glyphs(scale, font, text, |_, _| {}) } /// Get the width and height of the given text, rendered with the given font and scale. pub fn text_size(scale: impl Into + Copy, font: &impl Font, text: &str) -> (u32, u32) { - layout_glyphs(scale, font, text, |_, _| {}) + let info = text_size_info(scale, font, text); + + let (width, height) = info + .outline_bounds + .map(|b| (b.width(), b.height())) + .unwrap_or_else(|| (0, 0)); + + (height, width) } /// Draws colored text on an image. @@ -81,14 +126,15 @@ pub fn draw_text_mut( scale: impl Into + Copy, font: &impl Font, text: &str, -) where +) -> TextSizeInfo +where C: Canvas, ::Subpixel: Into + Clamp, { let image_width = canvas.width() as i32; let image_height = canvas.height() as i32; - layout_glyphs(scale, font, text, |g, bb| { + let info = layout_glyphs(scale, font, text, |g, bb| { let x_shift = x + bb.min.x.round() as i32; let y_shift = y + bb.min.y.round() as i32; g.draw(|gx, gy, gv| { @@ -105,16 +151,18 @@ pub fn draw_text_mut( } }) }); + + TextSizeInfo { + px_bounds: info.px_bounds.map(|b| b.translate(x, y)), + outline_bounds: info.outline_bounds.map(|b| b.translate(x, y)), + } } #[cfg(not(miri))] #[cfg(test)] mod proptests { use super::*; - use crate::{ - proptest_utils::arbitrary_image_with, - rect::{Rect, Region}, - }; + use crate::{proptest_utils::arbitrary_image_with, rect::Region}; use ab_glyph::FontRef; use image::Luma; use proptest::prelude::*; @@ -123,36 +171,37 @@ mod proptests { proptest! { #[test] - fn proptest_text_size( - img in arbitrary_image_with::>(Just(0), 0..=100, 0..=100), + fn proptest_text_size_info( + mut img in arbitrary_image_with::>(Just(0), 0..=100, 0..=100), x in 0..100, y in 0..100, scale in 0.0..100f32, - ref text in "[0-9a-zA-Z]*", + ref text in "[0-9a-zA-Z ]*", ) { let font = FontRef::try_from_slice(FONT_BYTES).unwrap(); let background = Luma([0]); let text_color = Luma([255u8]); - let img = draw_text(&img, text_color, x, y, scale, &font, text); + let draw_info = draw_text_mut(&mut img, text_color, x, y, scale, &font, text); + let size_info = text_size_info(scale, &font, text); + + let expected_draw_info = TextSizeInfo { + px_bounds: size_info.px_bounds.map(|r| r.translate(x, y)), + outline_bounds: size_info.outline_bounds.map(|r| r.translate(x, y)), + }; + assert_eq!(draw_info, expected_draw_info); - let (text_w, text_h) = text_size(scale, &font, text); if text.is_empty() { return Ok(()); } - let first_char = text.chars().next().unwrap(); - let first_x_bearing = - font.as_scaled(scale).h_side_bearing(font.glyph_id(first_char)); - let rect = if first_x_bearing < 0.0 { - let x_shift = first_x_bearing.abs().ceil() as i32; - Rect::at(x - x_shift, y).of_size(text_w + x_shift as u32, text_h) - } else { - Rect::at(x, y).of_size(text_w, text_h) + let Some(px_bounds) = draw_info.px_bounds else { + return Ok(()); }; + for (px, py, &p) in img.enumerate_pixels() { - if !rect.contains(px as i32, py as i32) { - assert_eq!(p, background, "pixel_position: {:?}, rect: {:?}", (px, py), rect); + if !px_bounds.contains(px as i32, py as i32) { + assert_eq!(p, background, "pixel_position: {:?}, rect: {:?}", (px, py), px_bounds); } } } diff --git a/src/morphology.rs b/src/morphology.rs index badb6832..03d3f92b 100644 --- a/src/morphology.rs +++ b/src/morphology.rs @@ -624,7 +624,7 @@ fn mask_reduce u8>( /// 255; /// 255; /// 255 -/// ), +/// ), /// 0, 1 /// ); /// let column_dilated = gray_image!( @@ -701,7 +701,7 @@ pub fn grayscale_dilate(image: &GrayImage, mask: &Mask) -> GrayImage { /// 255; /// 255; /// 255 -/// ), +/// ), /// 0, 1 /// ); /// let column_eroded = gray_image!( diff --git a/src/rect.rs b/src/rect.rs index 4c644036..1971086b 100644 --- a/src/rect.rs +++ b/src/rect.rs @@ -121,6 +121,64 @@ impl Rect { height: (bottom - top) as u32 + 1, }) } + + /// Returns the union of self and other, which is the smallest rectangle that contains both. + /// + /// # Examples + /// ``` + /// use imageproc::rect::Rect; + /// + /// // Unioning a rectangle with itself + /// let r = Rect::at(4, 5).of_size(6, 7); + /// assert_eq!(r.union(r), r); + /// + /// // Unioning overlapping but non-equal rectangles + /// let r = Rect::at(0, 0).of_size(5, 5); + /// let s = Rect::at(1, 4).of_size(10, 12); + /// let i = Rect::at(0, 0).of_size(11, 16); + /// assert_eq!(r.union(s), i); + /// + /// // Unioning disjoint rectangles + /// let r = Rect::at(0, 0).of_size(5, 5); + /// let s = Rect::at(10, 10).of_size(100, 12); + /// let i = Rect::at(0, 0).of_size(110, 22); + /// assert_eq!(r.union(s), i); + /// ``` + pub fn union(&self, other: Rect) -> Rect { + let left = cmp::min(self.left, other.left); + let top = cmp::min(self.top, other.top); + let right = cmp::max(self.right(), other.right()); + let bottom = cmp::max(self.bottom(), other.bottom()); + + Rect { + left, + top, + width: (right - left) as u32 + 1, + height: (bottom - top) as u32 + 1, + } + } + + /// Translate a rectangle by the given amount. + /// + /// # Examples + /// ``` + /// use imageproc::rect::Rect; + /// + /// let r = Rect::at(4, 5).of_size(6, 7); + /// assert_eq!(r.translate(0, 0), r); + /// + /// let r = Rect::at(4, 5).of_size(6, 7); + /// let expected = Rect::at(6, 8).of_size(6, 7); + /// assert_eq!(r.translate(2, 3), expected); + /// ``` + pub fn translate(&self, dx: i32, dy: i32) -> Rect { + Rect { + left: self.left + dx, + top: self.top + dy, + width: self.width, + height: self.height, + } + } } impl Region for Rect { @@ -138,6 +196,13 @@ impl Region for Rect { } } +impl From for Rect { + fn from(value: ab_glyph::Rect) -> Self { + Rect::at(value.min.x.floor() as i32, value.min.y.floor() as i32) + .of_size(value.width().ceil() as u32, value.height().ceil() as u32) + } +} + /// Position of the top left of a rectangle. /// Only used when building a [`Rect`]. #[derive(Copy, Clone, Debug, PartialEq, Eq)] diff --git a/tests/data/truth/text.png b/tests/data/truth/text.png index 03d164a47fce1a740f0805314cbcc4e6bff54b89..372749f1cb4d443ef3c36a4145920507029fbf0b 100644 GIT binary patch literal 2978 zcmeHJ{cltC8vmB!3WMcnxKb`-D_7;sF$*qpLf^a!>O>j2MS8i69=OC1;9?mCcKWi@ zf(cVMZEn<6Mb>E|)l61!vJTeubgzuft;#MoW8G=jvUS?6J#E+b({n60_a^sGxIZj; zp5)2%Bu}2_lh2dq^WL7_ujJ*f&IJJEmA|^J^)c%J45M3SkVBkmN>0L)!PL8+z$BxxEHoRtPe759?K=PdVxbjLZ@5DXR>3emH zw1aPQbHUS+&W+%k%IH_1X(71)$bZSOHenA9ismz*jQ$BaIHdXX06J6KGI?I;t;Or; z)nGB5!?+YTR>P>bnfD)Uq{$V*CQV7wo0- zcJu4IOAf}by+@yGt-ao-dHR3p7v}iZNn;mzbd2+IBe(PZkqTGZq3L24U|TuRHF-(= z3l@@?wYCDwU>z(Ex~NV1ldf*Bx+fpM7LW3WJX7#tvRSl+ehYj z+VYE;pX=u0aham+HuZYB^t=j^iKD|>hKNO{ESF_34Wlk+^LAOHB;r>&_!-MZHln@Lx#CI zO8{b+SSA+n+JWQ&o1f|?@0$3NeK_$o>dT`a1=euJ5Ifqz9m(JntAmGzVtP$0e4kD4 z%nm-zKdvuoyE*%LaDtqQ--4dA`U|jW2di;c{6=O4K6{KfEvn8)upIldnZPBCg1S6! zUYw{E%>0p1l5^WO!aU7ymjsVV!#){xU$O-RdhuVYJx zwE08%HdW^;4za^66J5}&j)KH+g^WMWa{deV^NC$>Pf6*kl$h}-IlkN4zH1`B=bIfq9koi!>9}0x9z#E33D!t;uEZNss7?A$7mymTR53tfcPv7nE{5j& zNskyLw#)kpXG4Zu?I1QBWw(2buG~ZE!~d|bmz?=g_u!_W9?Q|j%w?kbqv?)xi=xw* zg(EeG9z=%Ggup#Xmge|?ei7rsqqiTG!33E2Yvf48WxSF8{= z#_pO=^%e6+>q3U*TDPe8XIJEwlX;MX*25OH_cy!RCEn z+SDsXgxRoatggU^<$^9veQ>fE|CBw$A_vS2_dB|DEz;z23kuH$U0q2GPd-KCfOrQb zcuWFyq*w}})gVF70-?0>dL&17s(}6$ETN95dtAgzoYh)su+~-Yi^;K}YU@(M3fmxR z9W;LlU5bv{WQlc&Jb-5O$!T*A9v#YvHMU4-RMVxit|9gZORM^xM57Y8HVepNdh0$} z8oF(zx(%eq6{PR;Mvy|zXKSc*kTva~OIetY#ZiYuLRHUmi&p(9s$CZb<=kPL00pV{&S5Bh23 z@(G~Rd>!0B5l%S*HJN6-Pu?Df>;^hd>+0~4V*27xV|dV2b!OBLYlvY}c)51cxlKNH zCQ!*tG)u_g*xzNA=vi4!$jJbsH{~a?%vei#XQuO09f8U)c~Uq(FQ|Y+UyUIHrEy`+ yon>vvuc4(Z(!qFP;SDe7k_Aon|E5Q&$IyZwCbzBJx8kArF5kI(N6U+cF8mHhUy-Q* literal 3079 zcmeHJ{ZCV86n+aTPFJtfAuwX^=FFCDj_STTrM7ItIp#;%bn7;hGL0n}1XvjK`$HTe zFy_~gIkadN>D1|6#*_jr7iGvMqs6(22I;+}LMzzzmQs2zy}cdmhb8+1mh1CsZ{#@d()83AoSaRfzM`Ai^Vflq|!IvO#i&C{`dmhW~o?KB9n=1 zGbK0g8a|gW7u255eiUrKsC)-xwv29IrOlGF1Nlb`M1!+d=MJ+9nA33UvFPe%x)0jf z8LPPlSrVczgZT_w;C@E5_S%b@z&34-%Q+&`#_4zaXloW-v6iv=%w4e-Ybv@5aG9@+ z=DE?c;5$0`ggmi4x7GXc(x^uY?XHe)|CjuP)=lOIY#HMbe!-V0FLfK*LrAnNZk=|w zfRp2rde=e(aYvKOlGi4nBX%^hEoGp+yqnt3Gk@$GQylB+L9dcxT@KR&GO2^(FJKc@ zK%@?#I3>SOsCcwPV)kL<`3I>tBswl}xS_jQy}B>@=dDyGk$4uH+y!(2t&T-}BwQ+2L=>_a7;Fo4dJzy$D8&2k#g#cL~*s+;@A|iCNoa}^>A3|v6V_rge?)MR7MYZ zxKSzk6PKx;ZoFCXM~XPV_|D5%8`a}0f%Dbn8(THq+4S_ zr6n6V+G9TqTR_JdiDQqV0I|W5H9%OJTG-JPc?wd#YUZH+dNU{AvI1q91-f7spdTDn zXaM;U$6Z5p839o;QnmiJQy33wb@gN1qAn2{-!a*FTh3kV{u=(ZSe2+_15aV0HqkaI z3+)UjMUEgD9Eh|M-$V#iM<jqWWB~bLxBq^GkHZ1%(I1($+ z=eAb5CNm9J;+y@e4ZSF3MMBR%oh^-ux{dsyt|(JN-HgRg-@P_-4@~>w`ra^|e69dp z22ws$O+WY4IDD1ZBEpZS`0wtoNfJ+duh3(1SO1N?I&3*rv(aqFP!7uaq0v=khzFbb z`U-_D-U4!zG#&Hz|EMr6HB6M4(?kj?%QW;FzdVlIvK5^e^CuI@Fs&34N9cJS+Y021 z#ry-taW&s_rqEH9C2ve{3X{MU-GOgWSBc_^;dkNt z2EeZsUjV;JDvgACI&}V+kZRS%gR*EM)|q8q!|j8}!-cUOac~Ior=eF={82l)FQBso zG8hXBkBi{4d3fYv5E@sjg$O+g)MnEz7u$}nW45st)7|8L)KMXL30Uk9nq>31v~@o9 zQe1=TVR{qC&5JCd8~0+wCL$ToQ`|srA=^Aw7dA3_m(v(6gEedMCVSpc)kb<0XXDd&VjnZ<^*8)nOm4w>KpRW4Q^LPw@wDTY zC6+)qS44eaKp<0iN=UUy%%LV-5F(Ko*%y80-7&sd}Wp$$#` zLwR1!)RBaI|L@MFi%29^Z<<}hv%X~3ll{;9DE*{%`A?2z3zzSharhY Date: Sat, 26 Oct 2024 21:19:49 +0200 Subject: [PATCH 2/2] Export the new fn and type --- src/drawing/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/drawing/mod.rs b/src/drawing/mod.rs index a56aa0fa..386b00f3 100644 --- a/src/drawing/mod.rs +++ b/src/drawing/mod.rs @@ -33,7 +33,7 @@ pub use self::rect::{ }; mod text; -pub use self::text::{draw_text, draw_text_mut, text_size}; +pub use self::text::{draw_text, draw_text_mut, text_size, text_size_info, TextSizeInfo}; mod fill; pub use self::fill::{flood_fill, flood_fill_mut};