Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix text_size #689

Merged
merged 13 commits into from
Oct 19, 2024
85 changes: 71 additions & 14 deletions src/drawing/text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<GlyphId> = None;

let mut w = 0.0;
let mut prev: Option<GlyphId> = 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<PxScale> + Copy, font: &impl Font, text: &str) -> (u32, u32) {
layout_glyphs(scale, font, text, |_, _| {})
}
Expand Down Expand Up @@ -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<C>(
canvas: &mut C,
Expand All @@ -84,11 +89,11 @@ pub fn draw_text_mut<C>(
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;
Expand All @@ -101,3 +106,55 @@ pub fn draw_text_mut<C>(
})
});
}

#[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::<Luma<u8>>(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);
}
}
}
}
}
30 changes: 18 additions & 12 deletions tests/regression.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -808,19 +810,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);
}
}
}
Loading