mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 17:27:08 +08:00
468 lines
14 KiB
Go
468 lines
14 KiB
Go
// 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
|
|
advanceX, advanceY := 1.0, 0.0
|
|
switch obj.ReadDirection {
|
|
case 90:
|
|
advanceX, advanceY = 0, 1
|
|
case 180:
|
|
advanceX = -1
|
|
case 270:
|
|
advanceX, advanceY = 0, -1
|
|
}
|
|
codePos := 0
|
|
textPos := 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 = textCodeRunes(tc.Value)
|
|
glyphs = textCodeGlyphs(runes, glyphTransforms, codePos)
|
|
}
|
|
var spans [][2]int
|
|
if textRun != nil && tc.Index == "" {
|
|
spans = textGlyphSpans(runes, glyphTransforms, codePos)
|
|
}
|
|
dxs, dys := parseFloats(tc.DeltaX), parseFloats(tc.DeltaY)
|
|
xs, ys := parseFloats(tc.X), parseFloats(tc.Y)
|
|
drawAsPath := embeddedFont || face.FauxBold > 0 || textCodePositioned(tc, xs, ys) || fillClip != nil || shadedFill || fillPattern != nil || shouldStroke
|
|
cx, cy := 0.0, 0.0
|
|
previousAdvance := 0.0
|
|
if len(xs) > 0 {
|
|
cx = xs[0]
|
|
}
|
|
if len(ys) > 0 {
|
|
cy = ys[0]
|
|
}
|
|
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)
|
|
}
|
|
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 += previousAdvance * advanceX
|
|
}
|
|
}
|
|
if i < len(ys) {
|
|
cy = ys[i]
|
|
} else if i > 0 {
|
|
if dy, ok := textDelta(dys, i-1); ok {
|
|
cy += dy
|
|
} else if len(dxs) == 0 {
|
|
cy += previousAdvance * advanceY
|
|
}
|
|
}
|
|
previousAdvance = glyphWidth * hScale
|
|
if verticalAdvance {
|
|
previousAdvance = sizeMM
|
|
}
|
|
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 = path.Stroke(strokeStyle.lineWidth, strokeStyle.lineCap, strokeStyle.lineJoin, canvas.Tolerance)
|
|
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 = underline.Stroke(sizeMM*0.05, canvas.ButtCap, canvas.MiterJoin, canvas.Tolerance)
|
|
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 = underline.Stroke(uw, canvas.ButtCap, canvas.MiterJoin, canvas.Tolerance)
|
|
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()
|
|
}
|