From 5e4009eca5b94bec141ef988fe20b281b1463a07 Mon Sep 17 00:00:00 2001 From: xiaoqidun Date: Wed, 8 Jul 2026 23:48:47 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E5=AD=97=E5=BD=A2=E7=BB=98=E5=88=B6):=20?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E6=9B=B4=E5=A4=9A=E7=9A=84=E5=AD=97=E5=BD=A2?= =?UTF-8?q?=E7=BB=98=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ofdgo_renderer.go | 134 ++++-------------------------- ofdgo_text.go | 202 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 219 insertions(+), 117 deletions(-) diff --git a/ofdgo_renderer.go b/ofdgo_renderer.go index 47f85cd..ca2e0c6 100644 --- a/ofdgo_renderer.go +++ b/ofdgo_renderer.go @@ -1018,14 +1018,17 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64 return } face := ff.Face(sizePt, fillPaint, fontStyle, canvas.FontNormal) - glyphRunes := r.textObjectGlyphRunes(fontID, obj) + glyphTransforms := r.textObjectGlyphTransforms(fontID, obj) codePos := 0 for _, tc := range obj.TextCode { var runes []rune + var glyphs []textGlyph if tc.Index != "" { runes = r.parseIndexRunes(tc.Index, fontID) + glyphs = textRuneGlyphs(runes) } else { - runes = []rune(tc.Value) + runes = textCodeRunes(tc.Value) + glyphs = textCodeGlyphs(runes, glyphTransforms, codePos) } dxs, dys := parseFloats(tc.DeltaX), parseFloats(tc.DeltaY) xs, ys := parseFloats(tc.X), parseFloats(tc.Y) @@ -1037,20 +1040,15 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64 if len(ys) > 0 { cy = ys[0] } - for i, run := range runes { - str := string(run) - if embeddedFont && tc.Index == "" && glyphRunes != nil { - if mapped, ok := glyphRunes[codePos+i]; ok { - str = string(mapped) - } - } + for i, glyph := range glyphs { + str := glyph.Text if i < len(xs) { cx = xs[i] } else if i > 0 { if dx, ok := textDelta(dxs, i-1); ok { cx += dx } else if len(dys) == 0 { - cx += face.TextWidth(str) * hScale + cx += textGlyphWidth(face, glyph) * hScale } } if i < len(ys) { @@ -1069,17 +1067,17 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64 tx, ty := ctm.Transform(cx, cy) canvasX, canvasY = tx+bx, pageH-(ty+by) } - textWidth := face.TextWidth(str) * hScale + textWidth := textGlyphWidth(face, glyph) * hScale glyphFillPaint := fillPaint if fillColorNode != nil && fillColorNode.AxialShd != nil { glyphFillPaint = parseFillPaint(fillColorNode, bx, by, pageH, canvasX, canvasY) } - advanceLimit := textGlyphAdvanceLimit(dxs, dys, xs, i, len(runes), cx) + advanceLimit := textGlyphAdvanceLimit(dxs, dys, xs, i, len(glyphs), cx) if glyphFillPaint != nil { ctx.SetFill(glyphFillPaint) drawGlyph := func(x, y float64) { - if drawAsPath { - path, width := face.ToPath(str) + if drawAsPath || glyph.GlyphID >= 0 { + path, width := textGlyphPath(face, glyph) scaleX := hScale if advanceLimit > 0 && width*scaleX > advanceLimit { scaleX = advanceLimit / width @@ -1095,12 +1093,12 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64 ctx.DrawPath(x, y, path) } } else { - if hScale != 1 { + scaled := hScale != 1 + if scaled { ctx.Push() ctx.Translate(x, y) ctx.Scale(hScale, 1) x, y = 0, 0 - defer ctx.Pop() } textFace := face if fillColorNode != nil && fillColorNode.AxialShd != nil { @@ -1108,6 +1106,9 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64 } text := canvas.NewTextLine(textFace, str, canvas.Left) ctx.DrawText(x, y, text) + if scaled { + ctx.Pop() + } } } if useTextMatrix { @@ -1144,47 +1145,6 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64 ctx.Pop() } -// textGlyphAdvanceLimit 获取显式字形推进宽度 -// 入参: dxs X方向偏移, dys Y方向偏移, xs X坐标列表, index 字形索引, count 字形数量, currentX 当前X坐标 -// 返回: float64 推进宽度 -func textGlyphAdvanceLimit(dxs, dys, xs []float64, index int, count int, currentX float64) float64 { - if index+1 >= count || len(dys) > 0 { - return 0 - } - if index+1 < len(xs) { - if advance := xs[index+1] - currentX; advance > 0 { - return advance - } - } - if advance, ok := textDelta(dxs, index); ok && advance > 0 { - return advance - } - return 0 -} - -// textCodePositioned 判断文本编码是否带显式定位 -// 入参: textCode 文本编码 -// 返回: bool 是否带显式定位 -func textCodePositioned(textCode TextCode) bool { - return strings.TrimSpace(textCode.DeltaX) != "" || - strings.TrimSpace(textCode.DeltaY) != "" || - len(parseFloats(textCode.X)) > 1 || - len(parseFloats(textCode.Y)) > 1 -} - -// textDelta 获取文本偏移量 -// 入参: deltas 偏移量数组, index 偏移量索引 -// 返回: float64 偏移量, bool 是否存在 -func textDelta(deltas []float64, index int) (float64, bool) { - if len(deltas) == 0 { - return 0, false - } - if index < len(deltas) { - return deltas[index], true - } - return deltas[len(deltas)-1], true -} - // hasTextMatrix 判断文本是否需要应用字形变换 // 入参: ctm 变换矩阵 // 返回: bool 是否需要变换 @@ -1384,66 +1344,6 @@ func (r *Renderer) textObjectFontID(text TextObject) string { return fontID } -// textObjectGlyphRunes 获取文本对象的字形映射 -// 入参: fontID 字体ID, text 文本对象 -// 返回: map[int]rune 文本位置到包装字体字符的映射 -func (r *Renderer) textObjectGlyphRunes(fontID string, text TextObject) map[int]rune { - if fontID == "" || len(text.CGTransform) == 0 { - return nil - } - result := make(map[int]rune) - for _, transform := range text.CGTransform { - glyphs := parseInts(transform.Glyphs) - count := len(glyphs) - if transform.GlyphCount > 0 && transform.GlyphCount < count { - count = transform.GlyphCount - } - if transform.CodeCount > 0 && transform.CodeCount < count { - count = transform.CodeCount - } - if count == 0 { - continue - } - if transform.CodeCount > 0 && transform.GlyphCount > 0 && transform.CodeCount != transform.GlyphCount { - continue - } - for i := 0; i < count; i++ { - if mapped, ok := r.fontGlyphRune(fontID, glyphs[i]); ok { - result[transform.CodePosition+i] = mapped - } - } - } - if len(result) == 0 { - return nil - } - return result -} - -// fontGlyphRune 获取字形ID对应的包装字体字符 -// 入参: fontID 字体ID, glyphID 字形ID或CID -// 返回: rune 包装字体字符, bool 是否存在 -func (r *Renderer) fontGlyphRune(fontID string, glyphID int) (rune, bool) { - if glyphID < 0 || glyphID > 0xFFFF { - return 0, false - } - id := uint16(glyphID) - if r.FontCIDMap != nil { - if mapping := r.FontCIDMap[fontID]; mapping != nil { - if mapped, ok := mapping[id]; ok { - return mapped, true - } - } - } - if r.FontGIDMap != nil { - if mapping := r.FontGIDMap[fontID]; mapping != nil { - if mapped, ok := mapping[id]; ok { - return mapped, true - } - } - } - return 0, false -} - // renderStamp 渲染印章 // 入参: ctx 画布上下文, s 印章对象, pageH 页面高度 func (r *Renderer) renderStamp(ctx *canvas.Context, s Stamp, pageH float64) { diff --git a/ofdgo_text.go b/ofdgo_text.go index 3ca78d9..91deaac 100644 --- a/ofdgo_text.go +++ b/ofdgo_text.go @@ -20,6 +20,7 @@ import ( "strings" "github.com/tdewolff/canvas" + "github.com/tdewolff/font" ) // parseColorWithAlpha 解析带透明度的颜色 @@ -225,6 +226,207 @@ func parseStrokePaint(strokeColor *StrokeColor, x, y, pageH, originX, originY fl return nil } +// textGlyph 绘制字形 +type textGlyph struct { + Text string + GlyphID int +} + +// textGlyphTransform 字符到字形变换 +type textGlyphTransform struct { + CodeCount int + Glyphs []textGlyph +} + +// textObjectGlyphTransforms 获取文本对象的字形变换 +// 入参: fontID 字体ID, text 文本对象 +// 返回: map[int]textGlyphTransform 文本位置到字形变换的映射 +func (r *Renderer) textObjectGlyphTransforms(fontID string, text TextObject) map[int]textGlyphTransform { + if fontID == "" || len(text.CGTransform) == 0 { + return nil + } + result := make(map[int]textGlyphTransform) + for _, transform := range text.CGTransform { + ids := parseInts(transform.Glyphs) + glyphCount := len(ids) + if transform.GlyphCount > 0 && transform.GlyphCount < glyphCount { + glyphCount = transform.GlyphCount + } + if glyphCount == 0 { + continue + } + codeCount := transform.CodeCount + if codeCount <= 0 { + codeCount = 1 + } + glyphs := make([]textGlyph, 0, glyphCount) + for i := 0; i < glyphCount; i++ { + glyphs = append(glyphs, r.textGlyphFromID(fontID, ids[i])) + } + result[transform.CodePosition] = textGlyphTransform{ + CodeCount: codeCount, + Glyphs: glyphs, + } + } + if len(result) == 0 { + return nil + } + return result +} + +// textCodeGlyphs 获取文本编码对应的绘制字形 +// 入参: runes 文本字符, transforms 字形变换, codeOffset 文本编码偏移 +// 返回: []textGlyph 绘制字形列表 +func textCodeGlyphs(runes []rune, transforms map[int]textGlyphTransform, codeOffset int) []textGlyph { + if len(transforms) == 0 { + return textRuneGlyphs(runes) + } + glyphs := make([]textGlyph, 0, len(runes)) + for i := 0; i < len(runes); { + if transform, ok := transforms[codeOffset+i]; ok && i+transform.CodeCount <= len(runes) { + glyphs = append(glyphs, transform.Glyphs...) + i += transform.CodeCount + continue + } + glyphs = append(glyphs, textGlyph{Text: string(runes[i]), GlyphID: -1}) + i++ + } + return glyphs +} + +// textRuneGlyphs 转换文本字符为绘制字形 +// 入参: runes 文本字符 +// 返回: []textGlyph 绘制字形列表 +func textRuneGlyphs(runes []rune) []textGlyph { + glyphs := make([]textGlyph, 0, len(runes)) + for _, r := range runes { + glyphs = append(glyphs, textGlyph{Text: string(r), GlyphID: -1}) + } + return glyphs +} + +// textGlyphFromID 获取字形ID对应的绘制字形 +// 入参: fontID 字体ID, glyphID 字形ID或CID +// 返回: textGlyph 绘制字形 +func (r *Renderer) textGlyphFromID(fontID string, glyphID int) textGlyph { + if mapped, ok := r.fontGlyphRune(fontID, glyphID); ok { + return textGlyph{Text: string(mapped), GlyphID: -1} + } + return textGlyph{GlyphID: glyphID} +} + +// textGlyphWidth 获取绘制字形宽度 +// 入参: face 字体, glyph 绘制字形 +// 返回: float64 字形宽度 +func textGlyphWidth(face *canvas.FontFace, glyph textGlyph) float64 { + if glyph.GlyphID >= 0 && glyph.GlyphID <= 0xFFFF { + return face.MmPerEm * float64(face.Font.GlyphAdvance(uint16(glyph.GlyphID))) + } + return face.TextWidth(glyph.Text) +} + +// textGlyphPath 获取绘制字形路径 +// 入参: face 字体, glyph 绘制字形 +// 返回: *canvas.Path 字形路径, float64 字形宽度 +func textGlyphPath(face *canvas.FontFace, glyph textGlyph) (*canvas.Path, float64) { + if glyph.GlyphID < 0 || glyph.GlyphID > 0xFFFF { + return face.ToPath(glyph.Text) + } + p := &canvas.Path{} + glyphID := uint16(glyph.GlyphID) + _ = face.Font.GlyphPath(p, glyphID, face.PPEM(canvas.DefaultResolution), 0, 0, face.MmPerEm, font.NoHinting) + if face.FauxBold != 0 { + d := face.FauxBold * face.Size + if face.Font.IsTrueType { + d = -d + } + origFastStroke := canvas.FastStroke + canvas.FastStroke = true + p = p.Offset(d, canvas.Tolerance) + canvas.FastStroke = origFastStroke + } + if face.FauxItalic != 0 { + p = p.Transform(canvas.Identity.Shear(face.FauxItalic, 0)) + } + return p, face.MmPerEm * float64(face.Font.GlyphAdvance(glyphID)) +} + +// fontGlyphRune 获取字形ID对应的包装字体字符 +// 入参: fontID 字体ID, glyphID 字形ID或CID +// 返回: rune 包装字体字符, bool 是否存在 +func (r *Renderer) fontGlyphRune(fontID string, glyphID int) (rune, bool) { + if glyphID < 0 || glyphID > 0xFFFF { + return 0, false + } + id := uint16(glyphID) + if r.FontCIDMap != nil { + if mapping := r.FontCIDMap[fontID]; mapping != nil { + if mapped, ok := mapping[id]; ok { + return mapped, true + } + } + } + if r.FontGIDMap != nil { + if mapping := r.FontGIDMap[fontID]; mapping != nil { + if mapped, ok := mapping[id]; ok { + return mapped, true + } + } + } + return 0, false +} + +// textGlyphAdvanceLimit 获取显式字形推进宽度 +// 入参: dxs X方向偏移, dys Y方向偏移, xs X坐标列表, index 字形索引, count 字形数量, currentX 当前X坐标 +// 返回: float64 推进宽度 +func textGlyphAdvanceLimit(dxs, dys, xs []float64, index int, count int, currentX float64) float64 { + if index+1 >= count || len(dys) > 0 { + return 0 + } + if index+1 < len(xs) { + if advance := xs[index+1] - currentX; advance > 0 { + return advance + } + } + if advance, ok := textDelta(dxs, index); ok && advance > 0 { + return advance + } + return 0 +} + +// textCodePositioned 判断文本编码是否带显式定位 +// 入参: textCode 文本编码 +// 返回: bool 是否带显式定位 +func textCodePositioned(textCode TextCode) bool { + return strings.TrimSpace(textCode.DeltaX) != "" || + strings.TrimSpace(textCode.DeltaY) != "" || + len(parseFloats(textCode.X)) > 1 || + len(parseFloats(textCode.Y)) > 1 +} + +// textDelta 获取文本偏移量 +// 入参: deltas 偏移量数组, index 偏移量索引 +// 返回: float64 偏移量, bool 是否存在 +func textDelta(deltas []float64, index int) (float64, bool) { + if len(deltas) == 0 { + return 0, false + } + if index < len(deltas) { + return deltas[index], true + } + return deltas[len(deltas)-1], true +} + +// textCodeRunes 获取文本编码字符 +// 入参: value 文本编码内容 +// 返回: []rune 文本字符 +func textCodeRunes(value string) []rune { + if strings.ContainsAny(value, "\r\n") { + value = strings.TrimSpace(value) + } + return []rune(value) +} + // parseAxialShdColor 解析轴向渐变颜色 // 入参: axialShd 轴向渐变节点, alpha 透明度 // 返回: color.Color 颜色对象