Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ImageSharp and Font Height

I have a task to create an image that will be printed. On the picture, I need to put a single uppercase letter (Upper case, [A-Z]).

The printed image size can vary between 15cm height, and 30cm height (including any size in between).

The letter needs to span the full height of the printed image.

When setting the font size, I see you can get the size of the text.

using (Image<Rgba32> img = new Image<Rgba32>(imageWidth, imageHeight))
{
    img.Mutate(x => x.Fill(Rgba32.White));
    img.MetaData.HorizontalResolution = 96;
    img.MetaData.VerticalResolution = 96;
    var fo = SystemFonts.Find("Arial");
    var font = new Font(fo, 1350, FontStyle.Regular);

I can get the size of my text here:

SizeF size = TextMeasurer.Measure(group.Text, new RendererOptions(font));

However, as you can see, I hard coded the size for my font here. The height needs to be matched to the height of the image.

Is there any way to specify this, without stretching and losing quality? Is there a way I can specify the height, in pixels? Maybe there's coloration to the font size that I can use safely?

When I set the Font size to the pixel height of my Image, I am seeing this: enter image description here

I'm not sure why the circled parts have gaps. I am setting my top left position of the left hand text, to 0,0.... and the top right hand point of the 'QWW' group to the width of the image, and 0 as Y. But I'd expect them to be flush against the size, and the bottom.

like image 494
Craig Avatar asked Oct 18 '18 02:10

Craig


2 Answers

I split up your question into 3 parts:

  1. dynamic font size, rather than hard coded font size
  2. the glyph should use the full height of the image
  3. the glyph should be aligned to the left

Dynamically scale the text to fill the height of the image

After measuring the text size, calculate the factor by which the font needs to be scaled up or down to match the height of the image:

SizeF size = TextMeasurer.Measure(text, new RendererOptions(font));
float scalingFactor = finalImage.Height / size.Height;
var scaledFont = new Font(font, scalingFactor * font.Size);

This way the initially set font size is largely ignored. Now we can draw the text with the dynamically scaled font, depending on the height of the image:

initial.png

Inflate text to use the entire height of the image

Depending on each glyph, we might now have a gap in between the top/bottom side of the image and the top/bottom side of the text. How a glyph is rendered or drawn depends heavily on the font in use. I am not an expert in typography, but AFAIK every font has it's own margin/padding, and has custom heights around the baseline.

In order for our glyph to align with the top and bottom of the image, we have to further scale up the font. To calculate this factor, we can determine the top and bottom edge of the currently drawn text by searching for the height (y) of the top-most and bottom-most pixels, and scale up the font with this difference. Additionally, we need to offset the glyph by the distance from the top of the image to the top edge of the glyph:

int top = GetTopPixel(initialImage, Rgba32.White);
int bottom = GetBottomPixel(initialImage, Rgba32.White);
int offset = top + (initialImage.Height - bottom);

SizeF inflatedSize = TextMeasurer.Measure(text, new RendererOptions(scaledFont));
float inflatingFactor = (inflatedSize.Height + offset) / inflatedSize.Height;
var inflatedFont = new Font(font, inflatingFactor * scaledFont.Size);

location.Offset(0.0f, -top);

Now we can draw the text with the top and the bottom snapping to the top and bottom edges of the image:

intermediate.png

Move glyph to the very left

Lastly, depending on the glyph, the left side of the glyph might not snap with the left side of the image. Similar to the previous step, we can determine the left-most pixel of the text within the current image containing the inflated glyph, and move the text accordingly to the left to remove the gap in between:

int left = GetLeftPixel(intermediateImage, Rgba32.White);

location.Offset(-left, 0.0f);

Now we can draw the text aligning with the left side of the image:

final.png

This final image now has the font dynamically scaled depending on the size of the image, has been further scaled up and moved to fill up the entire height of the image, and has been further moved to have no gap to the left.

Note

When drawing the text, the DPI of the TextGraphicsOptions should match the DPI of the image:

var textGraphicOptions = new TextGraphicsOptions(true)
{
    HorizontalAlignment = HorizontalAlignment.Left,
    VerticalAlignment = VerticalAlignment.Top,
    DpiX = (float)finalImage.MetaData.HorizontalResolution,
    DpiY = (float)finalImage.MetaData.VerticalResolution
};

Code

private static void CreateImageFiles()
{
    Directory.CreateDirectory("output");

    string text = "J";

    Rgba32 backgroundColor = Rgba32.White;
    Rgba32 foregroundColor = Rgba32.Black;

    int imageWidth = 256;
    int imageHeight = 256;
    using (var finalImage = new Image<Rgba32>(imageWidth, imageHeight))
    {
        finalImage.Mutate(context => context.Fill(backgroundColor));
        finalImage.MetaData.HorizontalResolution = 96;
        finalImage.MetaData.VerticalResolution = 96;
        FontFamily fontFamily = SystemFonts.Find("Arial");
        var font = new Font(fontFamily, 10, FontStyle.Regular);

        var textGraphicOptions = new TextGraphicsOptions(true)
        {
            HorizontalAlignment = HorizontalAlignment.Left,
            VerticalAlignment = VerticalAlignment.Top,
            DpiX = (float)finalImage.MetaData.HorizontalResolution,
            DpiY = (float)finalImage.MetaData.VerticalResolution
        };

        SizeF size = TextMeasurer.Measure(text, new RendererOptions(font));
        float scalingFactor = finalImage.Height / size.Height;
        var scaledFont = new Font(font, scalingFactor * font.Size);

        PointF location = new PointF();
        using (Image<Rgba32> initialImage = finalImage.Clone(context => context.DrawText(textGraphicOptions, text, scaledFont, foregroundColor, location)))
        {
            initialImage.Save("output/initial.png");

            int top = GetTopPixel(initialImage, backgroundColor);
            int bottom = GetBottomPixel(initialImage, backgroundColor);
            int offset = top + (initialImage.Height - bottom);

            SizeF inflatedSize = TextMeasurer.Measure(text, new RendererOptions(scaledFont));
            float inflatingFactor = (inflatedSize.Height + offset) / inflatedSize.Height;
            var inflatedFont = new Font(font, inflatingFactor * scaledFont.Size);

            location.Offset(0.0f, -top);
            using (Image<Rgba32> intermediateImage = finalImage.Clone(context => context.DrawText(textGraphicOptions, text, inflatedFont, foregroundColor, location)))
            {
                intermediateImage.Save("output/intermediate.png");

                int left = GetLeftPixel(intermediateImage, backgroundColor);

                location.Offset(-left, 0.0f);
                finalImage.Mutate(context => context.DrawText(textGraphicOptions, text, inflatedFont, foregroundColor, location));
                finalImage.Save("output/final.png");
            }
        }
    }
}

private static int GetTopPixel(Image<Rgba32> image, Rgba32 backgroundColor)
{
    for (int y = 0; y < image.Height; y++)
    {
        for (int x = 0; x < image.Width; x++)
        {
            Rgba32 pixel = image[x, y];
            if (pixel != backgroundColor)
            {
                return y;
            }
        }
    }

    throw new InvalidOperationException("Top pixel not found.");
}

private static int GetBottomPixel(Image<Rgba32> image, Rgba32 backgroundColor)
{
    for (int y = image.Height - 1; y >= 0; y--)
    {
        for (int x = image.Width - 1; x >= 0; x--)
        {
            Rgba32 pixel = image[x, y];
            if (pixel != backgroundColor)
            {
                return y;
            }
        }
    }

    throw new InvalidOperationException("Bottom pixel not found.");
}

private static int GetLeftPixel(Image<Rgba32> image, Rgba32 backgroundColor)
{
    for (int x = 0; x < image.Width; x++)
    {
        for (int y = 0; y < image.Height; y++)
        {
            Rgba32 pixel = image[x, y];
            if (pixel != backgroundColor)
            {
                return x;
            }
        }
    }

    throw new InvalidOperationException("Left pixel not found.");
}

We don't need to save all 3 images, however we do need to create all 3 images and inflate and move the text step by step in order to fill up the entire height of the image and start at the very left of the image.

This solution works independently of the used font. Also, for a production application avoid finding a font via SystemFonts, because the font in question might not be available at the target machine. To have an stable stand-alone solution, deploy a TTF font with the application and install the font via FontCollection manually.

like image 163
FlashOver Avatar answered Nov 15 '22 21:11

FlashOver


TextMeasurer is designed for measurer text in the context of line and words not on individual characters because it doesn't look at individual glyph forms instead looks at the font as a whole to measure against line spacing etc.

Instead you will want to render the glyph directly to a vector using the nuget package SixLabors.Shapes.Text. This will allow you to accurately measure the final glyph + apply scaling and transforms to guarantee the glyph lines up with the edges of your image. It also saves you having to perform any expensive pixel level operations except the final drawing of the glyph to the image.

/// <param name="text">one or more characters to scale to fill as much of the target image size as required.</param>
/// <param name="targetSize">the size in pixels to generate the image</param>
/// <param name="outputFileName">path/filename where to save the image to</param>
private static void GenerateImage(string text, Primitives.Size targetSize, string outputFileName)
{
    FontFamily fam = SystemFonts.Find("Arial");
    Font font = new Font(fam, 100); // size doesn't matter too much as we will be scaling shortly anyway
    RendererOptions style = new RendererOptions(font, 72); // again dpi doesn't overlay matter as this code genreates a vector

    // this is the important line, where we render the glyphs to a vector instead of directly to the image
    // this allows further vector manipulation (scaling, translating) etc without the expensive pixel operations.
    IPathCollection glyphs = SixLabors.Shapes.TextBuilder.GenerateGlyphs(text, style);

    var widthScale = (targetSize.Width / glyphs.Bounds.Width);
    var heightScale = (targetSize.Height / glyphs.Bounds.Height);
    var minScale = Math.Min(widthScale, heightScale);

    // scale so that it will fit exactly in image shape once rendered
    glyphs = glyphs.Scale(minScale);

    // move the vectorised glyph so that it touchs top and left edges 
    // could be tweeked to center horizontaly & vertically here
    glyphs = glyphs.Translate(-glyphs.Bounds.Location);

    using (Image<Rgba32> img = new Image<Rgba32>(targetSize.Width, targetSize.Height))
    {
        img.Mutate(i => i.Fill(new GraphicsOptions(true), Rgba32.Black, glyphs));

        img.Save(outputFileName);
    }
}
like image 33
tocsoft Avatar answered Nov 15 '22 20:11

tocsoft