使用ImageSharp渲染两端对齐文本时如何实现单词基线对齐
问题背景
使用ImageSharp库渲染两端对齐文本时,将文本拆分为单词单独渲染,通过平均分配单词间距实现行首左对齐、行尾右对齐的效果,但出现垂直错位问题:不同单词包含的字符有差异(如带升部的M、带降部的j、仅含x高度字符的over),单词高度不一致,渲染后无法对齐baseline(基线)。
当前渲染效果:
原有实现代码:
public static IPathCollection RenderJustifiedText( string text, PointF startPos, float width, Font font ) { var textOptions = new TextOptions(font); var wordsAndWidths = text.Split(' ') .Select( w => new { Word = w, Width = TextMeasurer.MeasureBounds(w, textOptions).Width, } ).ToArray(); var spaceRemaining = width - wordsAndWidths.Select(ww => ww.Width).Sum(); var eachSpace = spaceRemaining / (wordsAndWidths.Length - 1); float xPos = startPos.X; List<IPathCollection> allPaths = new(); foreach (var w in wordsAndWidths) { var textPath = new PathBuilder() .AddLine( new PointF( xPos, startPos.Y ), new PointF( xPos + w.Width, startPos.Y ) ).Build(); allPaths.Add( TextBuilder.GenerateGlyphs( w.Word, textPath, textOptions ) ); xPos += w.Width + eachSpace; } return new PathCollection(allPaths.SelectMany(pathCollection => pathCollection)); }
问题原因
原有代码存在三个核心问题导致基线不对齐:
TextOptions未指定垂直对齐规则,默认采用顶部对齐模式,每个单词会以自身字形边界的顶部为基准对齐到传入的路径Y坐标,由于不同单词的字形高度、上下偏移不同,最终基线位置完全不统一。- 直接使用行起始点
startPos.Y作为路径Y坐标,startPos.Y是行内容的顶部位置,不是字体基线的位置,没有根据字体的升部参数计算正确的基线偏移。 - 未处理单行仅1个单词的边界场景,会触发除0运行时错误。
修复方案
- 初始化
TextOptions时显式指定VerticalAlignment = VerticalAlignment.Baseline,让文本渲染时以基线为对齐基准。 - 通过字体的度量参数计算像素单位下的升部高度,从行顶位置偏移得到整行共用的固定基线Y坐标,所有单词都对齐到这条基线渲染。
- 增加单单词边界判断,避免除0错误。
修复后的完整代码:
public static IPathCollection RenderJustifiedText( string text, PointF startPos, float width, Font font ) { var textOptions = new TextOptions(font) { // 配置文本垂直对齐到基线 VerticalAlignment = VerticalAlignment.Baseline }; // 计算像素单位下的字体度量值,得到行顶到基线的垂直偏移 FontMetrics fontMetrics = font.FontMetrics; float pixelsPerFontUnit = font.Size / fontMetrics.UnitsPerEm; float lineBaselineY = startPos.Y + fontMetrics.Ascender * pixelsPerFontUnit; // 预计算每个单词的宽度 var wordsAndWidths = text.Split(' ') .Select(w => new { Word = w, Width = TextMeasurer.MeasureBounds(w, textOptions).Width }) .ToArray(); // 计算单词间距,处理单单词边界 float wordSpacing = 0; if (wordsAndWidths.Length > 1) { float remainingSpace = width - wordsAndWidths.Sum(ww => ww.Width); wordSpacing = remainingSpace / (wordsAndWidths.Length - 1); } float currentX = startPos.X; List<IPathCollection> renderedGlyphs = new(); foreach (var word in wordsAndWidths) { // 所有单词使用统一的基线Y坐标构建渲染路径 IPath wordBaselinePath = new PathBuilder() .AddLine( new PointF(currentX, lineBaselineY), new PointF(currentX + word.Width, lineBaselineY) ) .Build(); renderedGlyphs.Add(TextBuilder.GenerateGlyphs(word.Word, wordBaselinePath, textOptions)); currentX += word.Width + wordSpacing; } return new PathCollection(renderedGlyphs.SelectMany(paths => paths)); }
内容的提问来源于stack exchange,提问作者Andrew Shepherd
相关产品推荐
相关产品推荐

