mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 14:31:12 +08:00
484 lines
15 KiB
Go
484 lines
15 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 (
|
|
"fmt"
|
|
"image/color"
|
|
"math"
|
|
"slices"
|
|
"strings"
|
|
|
|
"github.com/tdewolff/canvas"
|
|
)
|
|
|
|
// pathStyle 路径绘制样式
|
|
type pathStyle struct {
|
|
fillColor color.Color
|
|
strokeColor color.Color
|
|
fillPaint any
|
|
strokePaint any
|
|
fillPattern *PatternPaint
|
|
strokePattern *PatternPaint
|
|
lineWidth float64
|
|
lineCap canvas.Capper
|
|
lineJoin canvas.Joiner
|
|
lineJoinName string
|
|
miterLimit float64
|
|
dashOffset float64
|
|
dashPattern []float64
|
|
}
|
|
|
|
// newPathStyle 创建路径样式
|
|
// 入参: dp 默认绘制参数, bx 边界X坐标, by 边界Y坐标, pageH 页面高度, alpha 对象透明度
|
|
// 返回: pathStyle 路径样式
|
|
func (r *Renderer) newPathStyle(dp *DrawParam, bx, by, pageH float64, alpha *int) pathStyle {
|
|
style := pathStyle{
|
|
lineWidth: defaultPathLineWidth,
|
|
lineCap: canvas.ButtCap,
|
|
lineJoin: canvas.MiterJoiner{GapJoiner: canvas.BevelJoin, Limit: defaultMiterLimit},
|
|
miterLimit: defaultMiterLimit,
|
|
}
|
|
if dp != nil {
|
|
style.applyDrawParam(r, dp, bx, by, pageH, alpha)
|
|
}
|
|
return style
|
|
}
|
|
|
|
// applyFillColor 应用填充颜色
|
|
// 入参: r 渲染器, fill 填充颜色, bx 边界X坐标, by 边界Y坐标, pageH 页面高度, alpha 对象透明度
|
|
func (s *pathStyle) applyFillColor(r *Renderer, fill *FillColor, bx, by, pageH float64, alpha *int) {
|
|
fillColorNode := withFillAlpha(fill, alpha)
|
|
s.fillPattern = parsePatternPaint(fillColorNode)
|
|
s.fillColor = r.parseFillColor(fillColorNode)
|
|
s.fillPaint = r.parseFillPaint(fillColorNode, bx, by, pageH)
|
|
}
|
|
|
|
// applyStrokeColor 应用描边颜色
|
|
// 入参: r 渲染器, stroke 描边颜色, bx 边界X坐标, by 边界Y坐标, pageH 页面高度, alpha 对象透明度
|
|
func (s *pathStyle) applyStrokeColor(r *Renderer, stroke *StrokeColor, bx, by, pageH float64, alpha *int) {
|
|
strokeColorNode := withStrokeAlpha(stroke, alpha)
|
|
s.strokePattern = parsePatternPaint((*FillColor)(strokeColorNode))
|
|
s.strokeColor = r.parseStrokeColor(strokeColorNode)
|
|
s.strokePaint = r.parseStrokePaint(strokeColorNode, bx, by, pageH)
|
|
}
|
|
|
|
// pathLineCap 转换线帽样式
|
|
// 入参: cap 线帽名称, fallback 默认线帽
|
|
// 返回: canvas.Capper 线帽样式
|
|
func pathLineCap(cap string, fallback canvas.Capper) canvas.Capper {
|
|
switch cap {
|
|
case "Butt":
|
|
return canvas.ButtCap
|
|
case "Round":
|
|
return canvas.RoundCap
|
|
case "Square":
|
|
return canvas.SquareCap
|
|
}
|
|
return fallback
|
|
}
|
|
|
|
// applyLineJoin 应用线连接样式及尖角限制
|
|
// 入参: join 线连接名称, limit 尖角限制, explicit 是否显式指定零截断值
|
|
func (s *pathStyle) applyLineJoin(join string, limit float64, explicit bool) {
|
|
if limit > 0 || explicit {
|
|
s.miterLimit = limit
|
|
}
|
|
if s.lineJoinName == "" {
|
|
switch s.lineJoin.(type) {
|
|
case canvas.RoundJoiner:
|
|
s.lineJoinName = "Round"
|
|
case canvas.BevelJoiner:
|
|
s.lineJoinName = "Bevel"
|
|
default:
|
|
s.lineJoinName = "Miter"
|
|
}
|
|
}
|
|
switch join {
|
|
case "Miter", "Round", "Bevel":
|
|
s.lineJoinName = join
|
|
}
|
|
switch s.lineJoinName {
|
|
case "Round":
|
|
s.lineJoin = canvas.RoundJoin
|
|
case "Bevel":
|
|
s.lineJoin = canvas.BevelJoin
|
|
default:
|
|
s.lineJoin = canvas.MiterJoiner{GapJoiner: canvas.BevelJoin, Limit: s.miterLimit}
|
|
if s.miterLimit <= 1 {
|
|
s.lineJoin = canvas.BevelJoin
|
|
}
|
|
}
|
|
}
|
|
|
|
// applyDrawParam 应用绘制参数样式
|
|
// 入参: r 渲染器, dp 绘制参数, bx 边界X坐标, by 边界Y坐标, pageH 页面高度, alpha 对象透明度
|
|
func (s *pathStyle) applyDrawParam(r *Renderer, dp *DrawParam, bx, by, pageH float64, alpha *int) {
|
|
if dp.LineWidth > 0 || dp.LineWidthSet {
|
|
s.lineWidth = dp.LineWidth
|
|
}
|
|
if dp.FillColor != nil {
|
|
s.applyFillColor(r, dp.FillColor, bx, by, pageH, alpha)
|
|
}
|
|
if dp.StrokeColor != nil {
|
|
s.applyStrokeColor(r, dp.StrokeColor, bx, by, pageH, alpha)
|
|
}
|
|
if dp.Cap != "" {
|
|
s.lineCap = pathLineCap(dp.Cap, s.lineCap)
|
|
}
|
|
s.applyLineJoin(dp.Join, dp.MiterLimit, dp.MiterLimitSet)
|
|
if dp.DashPattern != "" || dp.dashPatternSet {
|
|
s.dashPattern = parseFloats(dp.DashPattern)
|
|
}
|
|
if dp.DashOffset != nil {
|
|
s.dashOffset = *dp.DashOffset
|
|
}
|
|
}
|
|
|
|
// applyPathObject 应用路径对象样式
|
|
// 入参: r 渲染器, obj 路径对象, bx 边界X坐标, by 边界Y坐标, pageH 页面高度
|
|
func (s *pathStyle) applyPathObject(r *Renderer, obj PathObject, bx, by, pageH float64) {
|
|
if obj.LineWidth > 0 || obj.LineWidthSet {
|
|
s.lineWidth = obj.LineWidth
|
|
}
|
|
if obj.FillColor != nil {
|
|
s.applyFillColor(r, obj.FillColor, bx, by, pageH, obj.Alpha)
|
|
}
|
|
if obj.StrokeColor != nil {
|
|
s.applyStrokeColor(r, obj.StrokeColor, bx, by, pageH, obj.Alpha)
|
|
}
|
|
if obj.Cap != "" {
|
|
s.lineCap = pathLineCap(obj.Cap, canvas.ButtCap)
|
|
}
|
|
s.applyLineJoin(obj.Join, obj.MiterLimit, obj.MiterLimitSet)
|
|
if obj.DashPattern != "" || obj.dashPatternSet {
|
|
s.dashPattern = parseFloats(obj.DashPattern)
|
|
}
|
|
if obj.DashOffset != nil {
|
|
s.dashOffset = *obj.DashOffset
|
|
}
|
|
}
|
|
|
|
// scale 应用路径变换缩放
|
|
// 入参: ctm 变换矩阵
|
|
func (s *pathStyle) scale(ctm Matrix) {
|
|
if scale := math.Sqrt(math.Abs(ctm.a*ctm.d - ctm.b*ctm.c)); scale > 0 {
|
|
s.lineWidth *= scale
|
|
s.dashOffset *= scale
|
|
for i := range s.dashPattern {
|
|
s.dashPattern[i] *= scale
|
|
}
|
|
}
|
|
}
|
|
|
|
// renderPath 渲染路径
|
|
// 入参: ctx 画布上下文, obj 路径对象, pageH 页面高度, defaults 默认绘制参数, parentCTM 父级CTM, boundaryInCTM 边界是否参与CTM变换, parentClip 父级裁剪路径
|
|
func (r *Renderer) renderPath(ctx *canvas.Context, obj PathObject, pageH float64, defaults *DrawParam, parentCTM *Matrix, boundaryInCTM bool, parentClip *canvas.Path) {
|
|
if r.textOnly {
|
|
return
|
|
}
|
|
if bounds, ok := ctx.Renderer.(*boundsRenderer); ok {
|
|
compiler, err := newSemanticCompiler(r)
|
|
if err != nil {
|
|
r.renderError = err
|
|
return
|
|
}
|
|
compiler.measure, compiler.contours = true, bounds.collect
|
|
err = compiler.pathObject(obj, RenderState{Defaults: defaults, Parent: parentCTM, BoundaryInCTM: boundaryInCTM, Clip: geometryFromCanvasPath(parentClip)})
|
|
if err != nil {
|
|
r.renderError = err
|
|
return
|
|
}
|
|
bounds.box = unionTextBox(bounds.box, compiler.measurement.Bounds)
|
|
bounds.contours = append(bounds.contours, compiler.measurement.Contours...)
|
|
return
|
|
}
|
|
if obj.Visible != nil && !*obj.Visible {
|
|
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)
|
|
}
|
|
style := r.newPathStyle(defaults, bx, by, pageH, obj.Alpha)
|
|
if obj.DrawParam != "" {
|
|
if dp := r.getDrawParam(obj.DrawParam, nil); dp != nil {
|
|
style.applyDrawParam(r, dp, bx, by, pageH, obj.Alpha)
|
|
}
|
|
}
|
|
style.applyPathObject(r, obj, bx, by, pageH)
|
|
style.scale(ctm)
|
|
style.lineWidth = r.deviceLineWidth(style.lineWidth)
|
|
objectCTM := TranslationMatrix(bx, by).Multiply(ctm)
|
|
if boundaryInCTM && parentCTM != nil {
|
|
objectCTM = parentCTM.Multiply(TranslationMatrix(bx, by)).Multiply(localCTM)
|
|
}
|
|
transformShdPaint(style.fillPaint, parentCTM, bx, by, pageH, boundaryInCTM)
|
|
transformShdPaint(style.strokePaint, parentCTM, bx, by, pageH, boundaryInCTM)
|
|
pathCTM := ctm
|
|
if boundaryInCTM {
|
|
pathCTM = localCTM
|
|
}
|
|
p := r.buildPath(obj, pageH, pathCTM, false)
|
|
if boundaryInCTM && parentCTM != nil {
|
|
p = p.Transform(canvas.Matrix{
|
|
{parentCTM.a, -parentCTM.c, parentCTM.c*pageH + parentCTM.e},
|
|
{-parentCTM.b, parentCTM.d, pageH*(1-parentCTM.d) - parentCTM.f},
|
|
})
|
|
}
|
|
clipPath := r.intersectCachedClipPath(parentClip, r.buildObjectClipPath(obj.Clips, pageH, obj.Boundary, localCTM, parentCTM, boundaryInCTM))
|
|
shouldFill := false
|
|
if obj.Fill != nil {
|
|
shouldFill = *obj.Fill
|
|
}
|
|
if style.fillPaint == nil {
|
|
style.fillPaint = style.fillColor
|
|
}
|
|
if shouldFill {
|
|
fillPaint, shadingClip, fillView := resolveShdPaint(ctx, style.fillPaint)
|
|
fillClip := r.intersectCachedClipPath(clipPath, shadingClip)
|
|
paths := canvas.Paths(p.Copy().Split())
|
|
for _, path := range paths {
|
|
path.Close()
|
|
}
|
|
fp := paths.Merge()
|
|
fillRule := canvas.NonZero
|
|
if fillClip != nil && !fp.Bounds().Overlaps(fillClip.Bounds()) {
|
|
fp = &canvas.Path{}
|
|
}
|
|
if obj.Rule == "Even-Odd" {
|
|
fillRule = canvas.EvenOdd
|
|
if bounds, ok := rectangularPath(fillClip); ok && bounds.Contains(fp.Bounds()) {
|
|
fillClip = nil
|
|
}
|
|
if fillClip != nil || style.fillPattern != nil {
|
|
fp = r.settleCanvasPath(fp, fillRule)
|
|
}
|
|
}
|
|
if fillClip != nil {
|
|
fp = applyClipPath(fp, fillClip)
|
|
}
|
|
if style.fillPattern != nil {
|
|
r.renderPattern(ctx, style.fillPattern, pageH, fp, objectCTM)
|
|
} else if style.fillPaint != nil {
|
|
ctx.SetFillRule(fillRule)
|
|
ctx.SetFill(fillPaint)
|
|
ctx.SetStrokeColor(canvas.Transparent)
|
|
r.drawShdPath(ctx, fp, fillView)
|
|
}
|
|
}
|
|
shouldStroke := true
|
|
if obj.Stroke != nil {
|
|
shouldStroke = *obj.Stroke
|
|
}
|
|
if shouldStroke {
|
|
ctx.SetFillRule(canvas.NonZero)
|
|
if style.strokePaint == nil {
|
|
style.strokePaint = style.strokeColor
|
|
}
|
|
if style.strokePaint == nil {
|
|
style.strokePaint = colorWithAlpha(canvas.Black, obj.Alpha)
|
|
}
|
|
strokePaint, shadingClip, strokeView := resolveShdPaint(ctx, style.strokePaint)
|
|
ctx.SetFillColor(canvas.Transparent)
|
|
ctx.SetStroke(strokePaint)
|
|
ctx.SetStrokeWidth(style.lineWidth)
|
|
ctx.SetStrokeCapper(style.lineCap)
|
|
ctx.SetStrokeJoiner(style.lineJoin)
|
|
sp := p
|
|
zeroDash := slices.Contains(style.dashPattern, 0)
|
|
if len(style.dashPattern) > 0 && !zeroDash {
|
|
sp = sp.Dash(style.dashOffset, style.dashPattern...)
|
|
ctx.SetDashes(0)
|
|
}
|
|
strokeClip := r.intersectCachedClipPath(clipPath, shadingClip)
|
|
_, repeat := strokePaint.(*repeatAxialGradient)
|
|
if strokeClip != nil || strokeView != canvas.Identity || repeat || style.strokePattern != nil || !canvasNativeStroke(r.backends.Geometry) || zeroDash {
|
|
sp = sp.Copy()
|
|
fill := style.strokePattern == nil && canvasFillClip(strokeClip)
|
|
if zeroDash {
|
|
sp = r.strokeDashedCanvasPath(sp, style.lineWidth, style.lineCap, style.lineJoin, style.dashOffset, style.dashPattern, fill)
|
|
} else {
|
|
sp = r.strokeDashedCanvasPath(sp, style.lineWidth, style.lineCap, style.lineJoin, 0, nil, fill)
|
|
}
|
|
if fill {
|
|
sp = applyFillClipPath(sp, strokeClip)
|
|
} else {
|
|
sp = applyClipPath(sp, strokeClip)
|
|
}
|
|
if style.strokePattern != nil {
|
|
r.renderPattern(ctx, style.strokePattern, pageH, sp, objectCTM)
|
|
} else {
|
|
ctx.SetFill(strokePaint)
|
|
ctx.SetStrokeColor(canvas.Transparent)
|
|
r.drawShdPath(ctx, sp, strokeView)
|
|
}
|
|
} else {
|
|
ctx.DrawPath(0, 0, sp)
|
|
}
|
|
}
|
|
ctx.Pop()
|
|
}
|
|
|
|
// renderPattern 渲染图案填充
|
|
// 入参: ctx 画布上下文, pattern 底纹画刷, pageH 页面高度, clip 绘制区域, objectCTM 对象变换矩阵
|
|
func (r *Renderer) renderPattern(ctx *canvas.Context, pattern *PatternPaint, pageH float64, clip *canvas.Path, objectCTM Matrix) {
|
|
if bounds, ok := ctx.Renderer.(*boundsRenderer); ok {
|
|
if pattern.Alpha == nil || *pattern.Alpha > 0 {
|
|
bounds.add(clip)
|
|
}
|
|
return
|
|
}
|
|
if r.pageText != nil {
|
|
text := r.pageText
|
|
r.pageText = nil
|
|
defer func() { r.pageText = text }()
|
|
}
|
|
if clip == nil || clip.Empty() || pattern.CellContent.empty() {
|
|
return
|
|
}
|
|
if r.patternReferences[pattern.Pattern] {
|
|
r.renderError = fmt.Errorf("cyclic pattern paint")
|
|
return
|
|
}
|
|
if r.patternReferences == nil {
|
|
r.patternReferences = make(map[*Pattern]bool)
|
|
}
|
|
r.patternReferences[pattern.Pattern] = true
|
|
parentPage := r.patternPage
|
|
defer func() {
|
|
delete(r.patternReferences, pattern.Pattern)
|
|
r.patternPage = parentPage
|
|
}()
|
|
xStep, yStep := pattern.XStep, pattern.YStep
|
|
if xStep < pattern.Width {
|
|
xStep = pattern.Width
|
|
}
|
|
if yStep < pattern.Height {
|
|
yStep = pattern.Height
|
|
}
|
|
if xStep <= 0 || yStep <= 0 {
|
|
return
|
|
}
|
|
patternCTM := NewMatrix(pattern.CTM)
|
|
if pattern.RelativeTo != "Page" {
|
|
patternCTM = objectCTM.Multiply(patternCTM)
|
|
} else if parentPage != nil {
|
|
patternCTM = parentPage.Multiply(patternCTM)
|
|
}
|
|
invCTM, ok := patternCTM.Invert()
|
|
if !ok {
|
|
return
|
|
}
|
|
bounds := clip.Bounds()
|
|
points := [][2]float64{
|
|
{bounds.X0, pageH - bounds.Y0},
|
|
{bounds.X1, pageH - bounds.Y0},
|
|
{bounds.X1, pageH - bounds.Y1},
|
|
{bounds.X0, pageH - bounds.Y1},
|
|
}
|
|
minX, maxX := 0.0, 0.0
|
|
minY, maxY := 0.0, 0.0
|
|
for i, point := range points {
|
|
x, y := invCTM.Transform(point[0], point[1])
|
|
if i == 0 {
|
|
minX, maxX = x, x
|
|
minY, maxY = y, y
|
|
continue
|
|
}
|
|
minX = math.Min(minX, x)
|
|
maxX = math.Max(maxX, x)
|
|
minY = math.Min(minY, y)
|
|
maxY = math.Max(maxY, y)
|
|
}
|
|
startX := int(math.Floor(minX/xStep)) - 1
|
|
endX := int(math.Ceil(maxX/xStep)) + 1
|
|
startY := int(math.Floor(minY/yStep)) - 1
|
|
endY := int(math.Ceil(maxY/yStep)) + 1
|
|
defaults := &DrawParam{FillColor: &pattern.Color, StrokeColor: (*StrokeColor)(&pattern.Color)}
|
|
for ix := startX; ix <= endX; ix++ {
|
|
for iy := startY; iy <= endY; iy++ {
|
|
tileCTM := patternCTM.Multiply(TranslationMatrix(float64(ix)*xStep, float64(iy)*yStep))
|
|
if ix%2 != 0 && (pattern.ReflectMethod == "Column" || pattern.ReflectMethod == "RowAndColumn") {
|
|
tileCTM = tileCTM.Multiply(Matrix{a: -1, d: 1, e: pattern.Width})
|
|
}
|
|
if iy%2 != 0 && (pattern.ReflectMethod == "Row" || pattern.ReflectMethod == "RowAndColumn") {
|
|
tileCTM = tileCTM.Multiply(Matrix{a: 1, d: -1, f: pattern.Height})
|
|
}
|
|
r.patternPage = &tileCTM
|
|
for obj := range pattern.CellContent.objects() {
|
|
obj = mergeGraphicObjectAlpha(obj, pattern.Alpha)
|
|
r.renderObject(ctx, &obj, pageH, defaults, &tileCTM, true, clip)
|
|
if r.renderError != nil {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// buildPath 解析路径并返回Canvas Path
|
|
// 入参: obj 路径对象, pageH 页面高度, ctm 变换矩阵, boundaryInCTM 边界是否参与CTM变换
|
|
// 返回: *canvas.Path 路径对象
|
|
func (r *Renderer) buildPath(obj PathObject, pageH float64, ctm Matrix, boundaryInCTM bool) *canvas.Path {
|
|
p, err := r.parseCanvasPath(obj.AbbreviatedData)
|
|
if err != nil {
|
|
r.renderError = err
|
|
return &canvas.Path{}
|
|
}
|
|
box, _ := ParseBox(obj.Boundary)
|
|
if boundaryInCTM {
|
|
ctm = ctm.Multiply(TranslationMatrix(box.X, box.Y))
|
|
} else {
|
|
ctm = TranslationMatrix(box.X, box.Y).Multiply(ctm)
|
|
}
|
|
if canvas.Equal(ctm.a*ctm.d-ctm.b*ctm.c, 0) {
|
|
p = p.ReplaceArcs()
|
|
}
|
|
return p.Transform(canvas.Matrix{{ctm.a, ctm.c, ctm.e}, {-ctm.b, -ctm.d, pageH - ctm.f}})
|
|
}
|
|
|
|
// parseCanvasPath 复用局部路径解析,返回独立副本且不缓存错误或超预算结果
|
|
// 入参: data 紧缩路径
|
|
// 返回: *canvas.Path 局部路径, error 解析错误
|
|
func (r *Renderer) parseCanvasPath(data string) (*canvas.Path, error) {
|
|
key := canvasPathKey{data: data, epsilon: canvas.Epsilon}
|
|
cache := r.canvasParsedPaths()
|
|
if path, ok := cache.get(key); ok {
|
|
return path.Copy(), nil
|
|
}
|
|
geometry, err := ParseGeometryPath(data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
path, err := canvasObjectPath(geometry)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if cost := len(data) + len(path.Data())*8 + 256; cost <= cache.limit {
|
|
key.data = strings.Clone(data)
|
|
cache.put(key, path.Copy(), cost)
|
|
}
|
|
return path, nil
|
|
}
|