ImageSharp和字体高度

11

我有一个任务需要创建一张将被打印的图片。在图片上,我需要放置一个大写字母(大写,[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));

然而,正如您所见,我在这里硬编码了我的字体大小。高度需要与图像的高度匹配。

有没有办法在不拉伸和失去质量的情况下指定高度?我能否以像素为单位指定高度?也许有我可以安全使用的字体大小颜色。

当我将字体大小设置为我的图像像素高度时,我看到了这个: enter image description here

我不确定为什么圈出的部分有间隙。 我将左侧文本的左上角位置设置为0,0 ... 将“QWW”组的右上角点设置为图像的宽度,Y轴设置为0。 但我希望它们与大小和底部紧密连接。

2个回答

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

1
我更喜欢这个解决方案,因为它执行速度更快,可维护性更高(代码更清晰、更简洁),并且表现非常稳定,因为它不依赖于任何像素颜色操作。此外,它能够产生更好的结果。 - FlashOver
1
非常好的解决方案,我已经使用它来动态调整文本字符串的大小,以适应图像上的边界框。使用像素方法会太过繁琐。如果我没有遇到这个答案,我就只能用这种方法来完成了,所以非常感谢。 - jcvandan
@jcvandan 我非常好奇您如何使用此方法将文本适配到边界框内。您如何考虑文本换行?使用上述方法,文本换行可能会出现偏差,因为它会从一个100点字体的假设开始。 - vargonian
@vargonian 这个例子根本没有处理文本换行,这里有一个例子 https://github.com/SixLabors/Samples/blob/32557bf359066c07358572e7147e638608522503/ImageSharp/DrawWaterMarkOnImage/Program.cs#L91-L159 展示了如何处理单词换行,基本上单词换行会导致我们多次重新布局,直到找到最佳大小,而不是只缩放一次,因此它是更昂贵的操作。 - tocsoft
@tocsoft,我感激不尽,这是我怀疑的事情(需要使用试错法),但我不确定,并且我不知道这个示例存在。你为我节省了很多时间,我非常感激! - vargonian
显示剩余2条评论

12

我将你的问题分成了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);

这种方法基本上忽略了最初设置的字体大小。现在我们可以使用根据图像高度动态缩放的字体绘制文本:

initial.png

将文本放大以使用整个图像高度

根据每个字形,我们现在可能在图像的顶部/底部和文本的顶部/底部之间有间隙。字形的呈现或绘制方式严重取决于所使用的字体。我不是印刷排版的专家,但据我所知,每种字体都有自己的边距/填充,并且具有围绕基线的自定义高度。

为了使我们的字形与图像的顶部和底部对齐,我们必须进一步放大字体。为了计算这个因子,我们可以通过搜索当前绘制文本的顶部和底部像素的高度(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);
现在我们可以将文本绘制到图像的顶部和底部与图像的顶部和底部对齐:

intermediate.png

将字形移到最左边

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

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

location.Offset(-left, 0.0f);

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

final.png

此最终图像的字体大小会根据图像的大小动态缩放,并进一步放大和移动以填充整个图像的高度,并进一步移动以消除左侧的空隙。

注意

绘制文本时,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手动安装字体。


2
这可以通过使用 SixLabors.Shapes.Text 先将字形渲染为矢量图,而不执行昂贵的像素操作来更简单地完成。 - tocsoft

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接