From e4d93e2d604b56a8d4d2dc1ac95afc2af268df52 Mon Sep 17 00:00:00 2001 From: cospectrum Date: Sat, 19 Oct 2024 23:10:57 +0300 Subject: [PATCH] fix `text_size` (#689) --- src/drawing/text.rs | 85 +++++++++++++++++++++++++++++++++++++-------- tests/regression.rs | 30 +++++++++------- 2 files changed, 89 insertions(+), 26 deletions(-) diff --git a/src/drawing/text.rs b/src/drawing/text.rs index cf126df8..13319afb 100644 --- a/src/drawing/text.rs +++ b/src/drawing/text.rs @@ -13,32 +13,36 @@ fn layout_glyphs( text: &str, mut f: impl FnMut(OutlinedGlyph, Rect), ) -> (u32, u32) { - let (mut w, mut h) = (0f32, 0f32); - + if text.is_empty() { + return (0, 0); + } let font = font.as_scaled(scale); - let mut last: Option = None; + + let mut w = 0.0; + let mut prev: Option = None; for c in text.chars() { let glyph_id = font.glyph_id(c); let glyph = glyph_id.with_scale_and_position(scale, point(w, font.ascent())); w += font.h_advance(glyph_id); if let Some(g) = font.outline_glyph(glyph) { - if let Some(last) = last { - w += font.kern(glyph_id, last); + if let Some(prev) = prev { + w += font.kern(glyph_id, prev); } - last = Some(glyph_id); + prev = Some(glyph_id); let bb = g.px_bounds(); - h = h.max(bb.height()); f(g, bb); } } - (w as u32, h as u32) + let w = w.ceil(); + let h = font.height().ceil(); + assert!(w >= 0.0); + assert!(h >= 0.0); + (1 + w as u32, h as u32) } /// Get the width and height of the given text, rendered with the given font and scale. -/// -/// Note that this function *does not* support newlines, you must do this manually. pub fn text_size(scale: impl Into + Copy, font: &impl Font, text: &str) -> (u32, u32) { layout_glyphs(scale, font, text, |_, _| {}) } @@ -67,6 +71,7 @@ where draw_text_mut(&mut out, color, x, y, scale, font, text); out } + #[doc=generate_mut_doc_comment!("draw_text")] pub fn draw_text_mut( canvas: &mut C, @@ -84,11 +89,11 @@ pub fn draw_text_mut( let image_height = canvas.height() as i32; layout_glyphs(scale, font, text, |g, bb| { - let bbx = x + bb.min.x.round() as i32; - let bby = y + bb.min.y.round() as i32; + 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| { - let image_x = gx as i32 + bbx; - let image_y = gy as i32 + bby; + let image_x = gx as i32 + x_shift; + let image_y = gy as i32 + y_shift; if (0..image_width).contains(&image_x) && (0..image_height).contains(&image_y) { let image_x = image_x as u32; @@ -101,3 +106,55 @@ pub fn draw_text_mut( }) }); } + +#[cfg(not(miri))] +#[cfg(test)] +mod proptests { + use super::*; + use crate::{ + proptest_utils::arbitrary_image_with, + rect::{Rect, Region}, + }; + use ab_glyph::FontRef; + use image::Luma; + use proptest::prelude::*; + + const FONT_BYTES: &[u8] = include_bytes!("../../tests/data/fonts/DejaVuSans.ttf"); + + proptest! { + #[test] + fn proptest_text_size( + 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]*", + ) { + 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 (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) + }; + 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); + } + } + } + } +} diff --git a/tests/regression.rs b/tests/regression.rs index 0710de04..68a969d5 100644 --- a/tests/regression.rs +++ b/tests/regression.rs @@ -23,9 +23,11 @@ use image::{ use imageproc::contrast::ThresholdType; use imageproc::definitions::Image; +use imageproc::drawing::text_size; use imageproc::filter::bilateral::GaussianEuclideanColorDistance; use imageproc::filter::bilateral_filter; use imageproc::kernel::{self}; +use imageproc::rect::{Rect, Region}; use imageproc::{ definitions::{Clamp, HasBlack, HasWhite}, edges::canny, @@ -826,19 +828,23 @@ fn test_bilateral_filter() { #[test] fn test_draw_text() { - let mut image = GrayImage::from_pixel(300, 300, Luma::black()); let font_bytes = include_bytes!("data/fonts/DejaVuSans.ttf"); let font = ab_glyph::FontRef::try_from_slice(font_bytes).unwrap(); - imageproc::drawing::draw_text_mut( - &mut image, - Luma::white(), - 50, - 100, - 30.0f32, - &font, - "Hello world!", - ); - - compare_to_truth_image(&image, "text.png"); + let background = Luma::black(); + let mut img = GrayImage::from_pixel(300, 300, background); + + let text = "Hello world!"; + let scale = 30.0; + let (x, y) = (50, 100); + imageproc::drawing::draw_text_mut(&mut img, Luma::white(), x, y, scale, &font, text); + compare_to_truth_image(&img, "text.png"); + + let (text_w, text_h) = text_size(scale, &font, text); + let rect = Rect::at(x, y).of_size(text_w, text_h); + for (px, py, &p) in img.enumerate_pixels() { + if !rect.contains(px as i32, py as i32) { + assert_eq!(p, background); + } + } }