【问题标题】:ImageSharp and Font HeightImageSharp 和字体高度
【发布时间】:2019-03-22 19:26:23
【问题描述】:

我的任务是创建要打印的图像。在图片上,我需要输入一个大写字母(大写,[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。但我希望它们与尺寸和底部齐平。

【问题讨论】:

    标签: c# .net-core imagesharp


    【解决方案1】:

    我把你的问题分成三部分:

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

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

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

    根据每个字形,我们现在可能在图像的顶部/底部与文本的顶部/底部之间存在间隙。如何渲染或绘制字形在很大程度上取决于使用的字体。我不是排版专家,但 AFAIK 每种字体都有自己的边距/填充,并且在 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 手动安装该字体。

    【讨论】:

    • 这可以通过使用SixLabors.Shapes.Text首先将字形渲染为矢量而不执行昂贵的像素操作来更简单地完成。
    【解决方案2】:

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

    【讨论】:

    • 我更喜欢这个解决方案,因为它执行速度更快,具有更高的可维护性(更清晰、更简洁的代码)并且执行超级稳定,因为它独立于任何像素颜色操作。此外,它会产生更好的结果。
    • 很好的解决方案,我已经使用它来动态调整文本字符串的大小以适应图像上的边界框。使用像素方法会过于密集。如果我没有遇到这个答案,这是我知道如何做到这一点的唯一方法,非常感谢。
    • @jcvandan 我很想知道您如何使用它来将文本放入边界框内。您如何考虑文本换行?使用上面的方法,文本换行将被关闭,因为它会以 100 磅字体的假设开始。
    • @vargonian 这个例子根本不处理文本换行 这里有一个例子github.com/SixLabors/Samples/blob/… 展示了如何处理自动换行,基本上自动换行导致我们重新布局多次,直到我们找到了最佳尺寸,而不是只缩放一次,因此它的操作成本更高。
    • @tocsoft 我非常感谢你,这是我怀疑的(需要反复试验)但不确定,我不知道这个样本存在。你为我节省了很多时间,我真的很感激!
    猜你喜欢
    • 2016-01-01
    • 1970-01-01
    • 2017-05-11
    • 2013-09-05
    • 1970-01-01
    • 2012-12-28
    • 1970-01-01
    • 2017-06-10
    • 2012-01-07
    相关资源
    最近更新 更多