ImageSharp 和字体高度

ImageSharp and Font Height

我有一项任务是创建要打印的图像。在图片上,我需要放一个大写字母(Upper case, [A-Z])。

打印的图像尺寸可以在 15 厘米高和 30 厘米高之间变化(包括介于两者之间的任何尺寸)。

字母需要跨越打印图像的整个高度。

在设置字体大小的时候,我看到你可以得到文字的大小。

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);

我可以在这里得到我的文字的大小:

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

但是,如您所见,我在这里对字体大小进行了硬编码。高度需要和图片的高度相匹配。

有没有什么方法可以在不拉伸和降低质量的情况下指定它?有没有一种方法可以指定高度(以像素为单位)?也许我可以安全使用的字体大小有颜色?

当我将字体大小设置为图像的像素高度时,我看到了:

不知道圈出的部分为什么会有空隙。我将左侧文本的左上角位置设置为 0,0.... 并将 'QWW' 组的右上角点设置为图像的宽度,并将 0 设置为 Y。但是我' d 希望它们与尺寸和底部齐平。

我将你的问题分为 3 个部分:

  1. 动态字体大小,而不是硬编码字体大小
  2. 字形应使用图像的全高
  3. 字形应左对齐

动态缩放文本以填充图像的高度

测量文本大小后,计算字体需要放大或缩小以匹配图像高度的系数:

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

这样初始设置的字体大小在很大程度上被忽略了。现在我们可以根据图像的高度使用动态缩放的字体绘制文本:

膨胀文本以使用图像的整个高度

根据每个字形,我们现在可能在图像的 top/bottom 侧和文本的 top/bottom 侧之间有一个间隙。字形的呈现或绘制方式在很大程度上取决于所使用的字体。我不是排版专家,但 AFAIK 每种字体都有自己的 margin/padding,并且在 baseline.

周围有自定义高度

为了让我们的字形与图像的顶部和底部对齐,我们必须进一步放大字体。为了计算这个因子,我们可以通过搜索最顶部和最底部像素的高度(y)来确定当前绘制文本的顶部和底部边缘,并按比例放大具有这种差异的字体。此外,我们需要将字形偏移图像顶部到字形顶部边缘的距离:

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);

现在我们可以绘制顶部和底部对齐图像顶部和底部边缘的文本:

将字形移到最左边

最后,根据字形,字形的左侧可能不会与图像的左侧对齐。与上一步类似,我们可以确定包含膨胀字形的当前图像中文本最左边的像素,并将文本相应地向左移动以消除中间的空隙:

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

location.Offset(-left, 0.0f);

现在我们可以绘制与图像左侧对齐的文本:

这张最终图片的字体现在根据图片的大小动态缩放,进一步放大并移动以填满图片的整个高度,并且进一步移动到与离开了。

备注

绘制文字时,TextGraphicsOptionsDPI应与图像的DPI匹配:

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

代码

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.");
}

我们不需要保存所有 3 个图像,但是我们需要创建所有 3 个图像并逐步膨胀和移动文本以填满图像的整个高度并从最开始图片左侧。

此解决方案独立于所使用的字体。此外,对于生产应用程序,请避免通过 SystemFonts 查找字体,因为目标机器上可能没有相关字体。要获得稳定的独立解决方案,请使用应用程序部署 TTF 字体并通过 FontCollection 手动安装字体。

TextMeasurer 专为行上下文中的测量文本而设计,而不是单个字符上的单词,因为它不查看单个字形形式,而是查看整个字体以衡量行间距等.

相反,您需要使用 nuget 包 SixLabors.Shapes.Text 将字形直接渲染为矢量。这将使您能够准确测量最终字形 + 应用缩放和变换以保证字形与图像边缘对齐。它还使您不必执行任何昂贵的像素级操作,除了将字形最终绘制到图像上。

/// <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);
    }
}