// Copyright 2025-2026 肖其顿 (XIAO QI DUN) // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package ofdgo import ( "strings" "github.com/tdewolff/canvas" ) const ptPerMM = 72.0 / 25.4 // renderText 渲染文本 // 入参: ctx 画布上下文, obj 文本对象, pageH 页面高度, defaults 默认绘制参数, parentCTM 父级CTM, boundaryInCTM 边界是否参与父级CTM, parentClip 父级裁剪路径 func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64, defaults *DrawParam, parentCTM *Matrix, boundaryInCTM bool, parentClip *canvas.Path) { if obj.Visible != nil && !*obj.Visible { return } shouldFill := obj.Fill == nil || *obj.Fill shouldStroke := obj.Stroke != nil && *obj.Stroke if !shouldFill && !shouldStroke { return } ctx.Push() bx, by := 0.0, 0.0 if obj.Boundary != "" { if box, err := ParseBox(obj.Boundary); err == nil { bx, by = box.X, box.Y } } localCTM := NewMatrix(obj.CTM) ctm := localCTM if parentCTM != nil { ctm = parentCTM.Multiply(ctm) } objectCTM := TranslationMatrix(bx, by).Multiply(ctm) if boundaryInCTM && parentCTM != nil { objectCTM = parentCTM.Multiply(TranslationMatrix(bx, by)).Multiply(localCTM) } clipPath := intersectClipPath(parentClip, r.buildObjectClipPath(obj.Clips, pageH, bx, by, localCTM, parentCTM, boundaryInCTM)) var dp *DrawParam if obj.DrawParam != "" { dp = r.getDrawParam(obj.DrawParam, nil) } sizeMM := obj.Size if sizeMM == 0 && dp != nil && dp.Size > 0 { sizeMM = dp.Size } if sizeMM == 0 { sizeMM = 3.5 } if obj.VScale != 0 { sizeMM *= obj.VScale } hScale := obj.HScale if hScale == 0 { hScale = 1 } useTextMatrix := hasTextMatrix(ctm) || obj.CharDirection != 0 glyphMatrix := textMatrix(ctm).Rotate(-float64(obj.CharDirection)) if scale := ctm.YScale(); scale > 0 && !useTextMatrix { sizeMM *= scale } sizePt := sizeMM * ptPerMM fillColor := colorWithAlpha(canvas.Black, obj.Alpha) var fillPaint any = fillColor var fillPattern *PatternPaint var fillColorNode *FillColor if defaults != nil { fillColorNode = defaults.FillColor } if dp != nil && dp.FillColor != nil { fillColorNode = dp.FillColor } if obj.FillColor != nil { fillColorNode = obj.FillColor } if fillColorNode != nil { fillColorNode = withFillAlpha(fillColorNode, obj.Alpha) fillColor = r.parseFillColor(fillColorNode) fillPaint = r.parseFillPaint(fillColorNode, bx, by, pageH) fillPattern = parsePatternPaint(fillColorNode) } if fillPaint == nil { fillPaint = fillColor } transformShdPaint(fillPaint, parentCTM, bx, by, pageH, boundaryInCTM) fillPaint, shadingClip, fillView := resolveShdPaint(ctx, fillPaint) fillClip := intersectClipPath(clipPath, shadingClip) var strokeStyle pathStyle strokeClip := clipPath strokeView := canvas.Identity if shouldStroke { strokeStyle = r.newPathStyle(defaults, bx, by, pageH, obj.Alpha) if dp != nil { strokeStyle.applyDrawParam(r, dp, bx, by, pageH, obj.Alpha) } if obj.StrokeColor != nil { strokeStyle.applyStrokeColor(r, obj.StrokeColor, bx, by, pageH, obj.Alpha) } if obj.LineWidth > 0 { strokeStyle.lineWidth = obj.LineWidth } strokeStyle.applyLineJoin(obj.Join, obj.MiterLimit) if strokeStyle.strokePaint == nil { strokeStyle.strokePaint = colorWithAlpha(canvas.Black, obj.Alpha) } strokeStyle.scale(ctm) transformShdPaint(strokeStyle.strokePaint, parentCTM, bx, by, pageH, boundaryInCTM) strokeStyle.strokePaint, shadingClip, strokeView = resolveShdPaint(ctx, strokeStyle.strokePaint) strokeClip = intersectClipPath(strokeClip, shadingClip) } fontStyle := canvas.FontRegular weight := obj.Weight if weight == 0 && dp != nil && dp.Weight > 0 { weight = dp.Weight } syntheticBold := weight >= 700 italic := obj.Italic if !italic && dp != nil && dp.Italic { italic = true } if italic { fontStyle |= canvas.FontItalic } fontID := r.textObjectFontID(obj) embeddedFont := false if of, ok := r.Reader.fontCache[fontID]; ok { embeddedFont = of.FontFile != "" if syntheticBold && !embeddedFont && fontNoSyntheticBold(of.FontName, of.FamilyName) { syntheticBold = false } if of.Bold { fontStyle |= canvas.FontBold } if of.Italic { fontStyle |= canvas.FontItalic } } if syntheticBold { fontStyle |= canvas.FontBold } ff := r.loadFont(fontID) var textRun *TextRun if r.pageText != nil { textRun = r.pageText.addRun(obj) } if ff == nil { if textRun != nil { box, _ := ParseBox(obj.Boundary) boundaryCTM := TranslationMatrix(box.X, box.Y) if parentCTM != nil { if boundaryInCTM { boundaryCTM = parentCTM.Multiply(boundaryCTM) } else { boundaryCTM = boundaryCTM.Multiply(*parentCTM) } } m := canvas.Matrix{{boundaryCTM.a, -boundaryCTM.c, boundaryCTM.e}, {-boundaryCTM.b, boundaryCTM.d, pageH - boundaryCTM.f}} path := canvas.Rectangle(box.W, box.H).Translate(0, -box.H).Transform(m) box = textPathBox(applyClipPath(path, clipPath), pageH) for i := range textRun.Boxes { textRun.Boxes[i] = box } } ctx.Pop() return } face := ff.Face(sizePt, fillPaint, fontStyle, canvas.FontNormal) var metrics canvas.FontMetrics if textRun != nil { metrics = face.Metrics() } glyphTransforms := r.textObjectGlyphTransforms(fontID, obj) hasUnderline := strings.Contains(obj.Decoration, "Underline") _, shadedFill := fillPaint.(canvas.Gradient) verticalAdvance := (obj.ReadDirection-obj.CharDirection)%180 != 0 codePos := 0 textPos := 0 for index, tc := range obj.TextCode { if textRun != nil && obj.textCodeLineBreak(index) { textPos++ } var runes []rune var glyphs []textGlyph if tc.Index != "" { runes = r.parseIndexRunes(tc.Index, fontID) glyphs = textRuneGlyphs(runes) } else { runes = textCodeRunes(tc.Value) glyphs = textCodeGlyphs(runes, glyphTransforms, codePos) } var spans [][2]int if textRun != nil && tc.Index == "" { spans = textGlyphSpans(runes, glyphTransforms, codePos) } positioner := NewTextPositioner(tc, obj.ReadDirection) dxs, dys, xs, ys := positioner.dxs, positioner.dys, positioner.xs, positioner.ys drawAsPath := embeddedFont || face.FauxBold > 0 || textCodePositioned(tc, xs, ys) || fillClip != nil || shadedFill || fillPattern != nil || shouldStroke for i, glyph := range glyphs { str := glyph.Text drawAsGlyphPath := drawAsPath || glyph.GlyphID >= 0 if !drawAsGlyphPath { for _, char := range str { if face.Font.GlyphIndex(char) == 0 { drawAsGlyphPath = true break } } } var glyphPath *canvas.Path var glyphWidth float64 if drawAsGlyphPath { glyphPath, glyphWidth = r.cachedTextGlyphPath(face, glyph) } else { glyphWidth = textGlyphWidth(face, glyph) } advance := glyphWidth * hScale if verticalAdvance { advance = sizeMM } position := positioner.Next(advance) cx, cy := position.X, position.Y var canvasX, canvasY float64 if boundaryInCTM && parentCTM != nil { tx, ty := localCTM.Transform(cx, cy) tx, ty = parentCTM.Transform(tx+bx, ty+by) canvasX, canvasY = tx, pageH-ty } else { tx, ty := ctm.Transform(cx, cy) canvasX, canvasY = tx+bx, pageH-(ty+by) } textWidth := glyphWidth * hScale advanceLimit := 0.0 if obj.CharDirection == 0 { advanceLimit = textGlyphAdvanceLimit(dxs, dys, xs, i, len(glyphs), cx) } if textRun != nil { if i < len(spans) { if glyphPath == nil { glyphPath, _ = r.cachedTextGlyphPath(face, glyph) } scaleX := hScale if drawAsGlyphPath && advanceLimit > 0 && glyphWidth*scaleX > advanceLimit { scaleX = advanceLimit / glyphWidth } m := canvas.Identity.Translate(canvasX, canvasY) if useTextMatrix { m = m.Mul(glyphMatrix) } path := glyphPath.Copy().Transform(m.Scale(scaleX, 1)) box := textPathBox(applyClipPath(path, clipPath), pageH) for j := spans[i][0]; j < spans[i][1]; j++ { textRun.Boxes[textPos+j] = unionTextBox(textRun.Boxes[textPos+j], box) } bounds := canvas.Rect{Y0: -metrics.Descent, X1: glyphWidth, Y1: metrics.Ascent} if !glyphPath.Empty() { bounds = bounds.Add(glyphPath.Bounds()) } textRun.addSpan(textPos+spans[i][0], textPos+spans[i][1], bounds, m.Scale(scaleX, 1), clipPath, pageH) } if r.textOnly { continue } } if shouldStroke { scaleX := hScale if advanceLimit > 0 && glyphWidth*scaleX > advanceLimit { scaleX = advanceLimit / glyphWidth } transform := canvas.Identity.Translate(canvasX, canvasY) if useTextMatrix { transform = transform.Mul(glyphMatrix) } path := glyphPath.Copy().Transform(transform.Scale(scaleX, 1)) if shouldFill && fillPattern != nil { r.renderPattern(ctx, fillPattern, pageH, applyClipPath(path.Copy(), fillClip), objectCTM) } else if shouldFill && fillPaint != nil { ctx.SetFill(fillPaint) ctx.SetStrokeColor(canvas.Transparent) drawShdPath(ctx, applyClipPath(path.Copy(), fillClip), fillView) } if len(strokeStyle.dashPattern) > 0 { path = path.Dash(strokeStyle.dashOffset, strokeStyle.dashPattern...) } path = r.strokeCanvasPath(path, strokeStyle.lineWidth, strokeStyle.lineCap, strokeStyle.lineJoin) path = applyClipPath(path, strokeClip) if strokeStyle.strokePattern != nil { r.renderPattern(ctx, strokeStyle.strokePattern, pageH, path, objectCTM) } else { ctx.SetFill(strokeStyle.strokePaint) ctx.SetStrokeColor(canvas.Transparent) drawShdPath(ctx, path, strokeView) } if hasUnderline { underline := &canvas.Path{} underline.MoveTo(0, -sizeMM*0.1) underline.LineTo(glyphWidth*scaleX, -sizeMM*0.1) underline = r.strokeCanvasPath(underline, sizeMM*0.05, canvas.ButtCap, canvas.MiterJoin) if shouldFill && fillPattern != nil { r.renderPattern(ctx, fillPattern, pageH, applyClipPath(underline.Transform(transform), clipPath), objectCTM) } else if shouldFill { ctx.SetFillColor(fillColor) ctx.DrawPath(0, 0, applyClipPath(underline.Transform(transform), clipPath)) } else if strokeStyle.strokePattern != nil { r.renderPattern(ctx, strokeStyle.strokePattern, pageH, applyClipPath(underline.Transform(transform), strokeClip), objectCTM) } else { drawShdPath(ctx, applyClipPath(underline.Transform(transform), strokeClip), strokeView) } } continue } if fillPaint != nil || fillPattern != nil { ctx.SetFill(fillPaint) ctx.SetStrokeColor(canvas.Transparent) if fillClip != nil || shadedFill || fillPattern != nil { scaleX := hScale if advanceLimit > 0 && glyphWidth*scaleX > advanceLimit { scaleX = advanceLimit / glyphWidth } textWidth = glyphWidth * scaleX textTransform := canvas.Identity.Translate(canvasX, canvasY) if useTextMatrix { textTransform = textTransform.Mul(glyphMatrix) } glyphPath = applyClipPath(glyphPath.Copy().Transform(textTransform.Scale(scaleX, 1)), fillClip) if fillPattern != nil { r.renderPattern(ctx, fillPattern, pageH, glyphPath, objectCTM) } else { drawShdPath(ctx, glyphPath, fillView) } if hasUnderline { uw := sizeMM * 0.05 off := sizeMM * 0.1 underline := &canvas.Path{} underline.MoveTo(0, -off) underline.LineTo(textWidth, -off) underline = r.strokeCanvasPath(underline, uw, canvas.ButtCap, canvas.MiterJoin) underline = applyClipPath(underline.Transform(textTransform), clipPath) if fillPattern != nil { r.renderPattern(ctx, fillPattern, pageH, underline, objectCTM) } else { ctx.SetFillColor(fillColor) ctx.SetStrokeColor(canvas.Transparent) ctx.DrawPath(0, 0, underline) } } continue } drawGlyph := func(x, y float64) { if drawAsGlyphPath { scaleX := hScale if advanceLimit > 0 && glyphWidth*scaleX > advanceLimit { scaleX = advanceLimit / glyphWidth } textWidth = glyphWidth * scaleX if face.FauxBold != 0 { if renderer, ok := ctx.Renderer.(*pdfRenderer); ok { glyphPath = renderer.glyphPath(glyphPath, ctx.View().Scale(scaleX, 1)) } } if scaleX != 1 { ctx.Push() ctx.Translate(x, y) ctx.Scale(scaleX, 1) ctx.DrawPath(0, 0, glyphPath) ctx.Pop() } else { ctx.DrawPath(x, y, glyphPath) } } else { scaled := hScale != 1 if scaled { ctx.Push() ctx.Translate(x, y) ctx.Scale(hScale, 1) x, y = 0, 0 } text := canvas.NewTextLine(face, str, canvas.Left) ctx.DrawText(x, y, text) if scaled { ctx.Pop() } } } if useTextMatrix { ctx.Push() ctx.Translate(canvasX, canvasY) ctx.ComposeView(glyphMatrix) drawGlyph(0, 0) if hasUnderline { uw := sizeMM * 0.05 ctx.SetStrokeWidth(uw) ctx.SetStrokeColor(fillColor) off := sizeMM * 0.1 ctx.MoveTo(0, -off) ctx.LineTo(textWidth, -off) ctx.Stroke() } ctx.Pop() continue } drawGlyph(canvasX, canvasY) } if hasUnderline { uw := sizeMM * 0.05 ctx.SetStrokeWidth(uw) ctx.SetStrokeColor(fillColor) off := sizeMM * 0.1 ctx.MoveTo(canvasX, canvasY-off) ctx.LineTo(canvasX+textWidth, canvasY-off) ctx.Stroke() } } codePos += len(runes) if textRun != nil { if tc.Index == "" { textPos += len(runes) } else { textPos++ } } } ctx.Pop() }