Hi, I am in the process of teaching myself Rust by rewriting a project I previously created in Python, so I apologize if this is a simple and/or already answered question.
I'm trying to create a program that is able to take in a string of text of unknown length and write it to an image file (e.g., text.png, but preferably text.bmp) with the Bookerly typeface, adjusting the font size to the largest possible for the given string's length and the image's dimensions (which are 800 by 480).
There were plenty of examples to go off in Python, so it was easy enough to use the Pillow library to pass in a TrueType file, calculate & wrap the text, and write it to the image.
I didn't have to worry about rasterizing the text, glyphs, ascender/descender heights, calculating the individual glyph placements, etc. (all of which are terms/concepts I have since learned about).
So far, the two best crates I have found that may work for what I'm trying to do are Fontdue (I have also looked into rusttype and ab_glyph, but Fontdue is a replacement for them) and COSMIC Text, but I've seemed to run into dead ends for both of them.
Fontdue has good support for doing the work of calculating font size and wrapping text for me, but it didn't seem to have good support for easily drawing the text to an image.
Cosmic provides plenty of support for manually calculating font size and wrapping text (i.e., it makes the process more hands-on but also easy), but I can't figure out how to write the text to the image. I know that in their docs they have the buffer.draw function, but I don't think that it's what I'm looking for (I can't figure out how to do it).
Any advice for either of these crates, or any alternatives, would be much appreciated
How can I go about drawing onto a buffer though? Cosmic provides a Buffer struct but I can’t get pixel coordinates for the glyphs in the text from it.
I haven't used Cosmic, but I have fiddled with fontdue a bit. fontdue::Font has a rasterize() method that gives you a rasterized character in the form of a Vec<u8> where:
Each pixel is represented as a value from 0 through 255, where 0 means "white" (or whatever your image's background color is), 255 means "black" (or whatever color you're drawing the text as), and values in between are shades of grey
The pixel at coordinate (x, y) of the rasterized character, counting from the top-left corner with y increasing downwards, is at index x + metrics.width * y of the Vec<u8>, where metrics is the other return value of rasterize().
You should be able to take it from there, I think.
I decided to go back to Fontdue, and I'm unfortunately still a bit stuck. I currently have this for loop to rasterize and print each character in a string onto an image:
for glyph in glyphs{
let (metric, bitmap) = bookerly.rasterize(glyph.parent, fontsize as f32);
for (idx, mut pxl_color) in bitmap.into_iter().enumerate(){
if pxl_color == 0 {
continue; // skip white pixels
} else {
// Invert pixel color to black
pxl_color = 255 - pxl_color;
let pxl = image::Rgb([pxl_color, pxl_color, pxl_color]);
let x = (citation_start_x as i32 + (idx % metric.width) as i32);
let y = citation_start_y as i32 + (idx / metric.height) as i32;
image.put_pixel((x + metric.xmin) as u32, (y + (fontsize as f32 - metric.bounds.height).round() as i32 - metric.ymin) as u32, pxl);
}
}
citation_start_x += metric.advance_width;
}
I found the calculations for x and y in this GitHub Gist and applied them in hopes that they'd work for my use case. It mostly does, but there are 2 glaring problems:
Certain characters have streaks of white pixels going through them. For example, printing the character 'M' produces:
Other characters I've found to have this problem are 'H', 'V', and 'W', though I haven't tried all of them.
The second problem is actually a bit visible above, which is that characters don't seem to be scaling the same ('T' is smaller than 'M' even though the same font size of 150 was used). This is what "TEST" looks like:
I assume that both problems are due to using the wrong variables and/or calculations for pixel placement onto the image via image.put_pixel(), but I'm not exactly sure what is incorrect.
Don't try to rasterize a single character at a time unless you are applying effects to each segment; this breaks ligatures like fi.
The strange behaviors are likely explained by the other values in metrics, e.g. you likely need to copy the characters with a vertical offset so that they all share a “baseline” (unless you intentionally want to vary the baseline, as mentioned above).
Hi, i recently started trying to use fontdue to draw some characters. Your question and the example code, even though they did not work perfectly, gave me the right hints to get my code going. So, many thanks for that!
I took a look at the code in the mentioned Github Gist, that got my brain in a knot. So i experimented and debugged a bit and found a much simpler way of mapping the rendered char bitmap to another bitmap:
let v_offset_towards_baseline = font_size as i32 - bounds_height_i as i32;
// draw character pixel, if found in rendered bitmap-vector:
for char_pixel_x in 0 .. width_i
{
for char_pixel_y in 0 .. height_i
{
let lightness = char_bitmap[char_pixel_x as usize + (char_pixel_y * width_i) as usize];
if lightness < 4 { continue; } // skip background pixels
image.put_pixel( (text_start_x + xmin_i + char_pixel_x) as u32
, (text_start_y + ymin_i + char_pixel_y + v_offset_towards_baseline) as u32
, pxl_white
);
}
}
The top left cross is a mark for the anchor (?, target/starting point), the yellow and red boxes mark the metrics width + height and the bounding w + h from the rendered char, the line is the baseline.