feat(后端结构): 完善多后端解耦,清理遗留代码
build.yaml / build (push) Successful in 40s

This commit is contained in:
xiaoqidun committed 2026-09-23 17:20:25 +08:00
1 parent 77ffbe6a72
commit c15547f0d2
27 files changed
+1162 -461

No files matched your search

+53
View File
@@ -57,6 +57,13 @@ type resolvedFontResult struct {
err error err error
} }
// resetFontCache 重置字体解析及后端私有缓存,不预先创建任何绘图库对象
func (r *Renderer) resetFontCache() {
r.preparedFonts = make(map[string]*PreparedFont)
r.resolvedFonts = make(map[resolvedFontKey]resolvedFontResult)
r.backendStates = make(map[any]any)
}
// ResolveFont 使用配置的字体后端解析资源,保留精确匹配与回退的区别 // ResolveFont 使用配置的字体后端解析资源,保留精确匹配与回退的区别
// 入参: id 字体资源标识, exact 是否要求精确匹配 // 入参: id 字体资源标识, exact 是否要求精确匹配
// 返回: ResolvedFont 字体与来源, error 解析错误 // 返回: ResolvedFont 字体与来源, error 解析错误
@@ -73,3 +80,49 @@ func (r *Renderer) ResolveFont(id string, exact bool) (ResolvedFont, error) {
r.resolvedFonts[key] = resolvedFontResult{font: resolved, err: err} r.resolvedFonts[key] = resolvedFontResult{font: resolved, err: err}
return resolved, err return resolved, err
} }
// PreparedFont 保存绘制用字体及GID、CID到包装字符的映射,所有数据只读
// Data可与源编码不同,原始资源仍由ResolveFont和Reader保留
type PreparedFont struct {
ResolvedFont
Glyphs map[uint16]rune
CIDs map[uint16]rune
}
// PrepareFont 统一处理内嵌字体包装和索引映射,供各编译器复用
// 入参: id 字体资源标识
// 返回: *PreparedFont 绘制字体, error 字体解析错误
func (r *Renderer) PrepareFont(id string) (*PreparedFont, error) {
if cached := r.preparedFonts[id]; cached != nil {
return cached, nil
}
resolved, err := r.ResolveFont(id, false)
if err != nil {
return nil, err
}
prepared := &PreparedFont{ResolvedFont: resolved}
definition := r.Reader.fontCache[id]
if definition != nil && definition.FontFile != "" {
prepared.CIDs = getCFFCIDRuneMap(resolved.Data)
if _, data, mapping, _, err := FixFontDataAggressive(resolved.Data, true, true); err == nil {
prepared.Data = data
if mapping != nil {
prepared.Glyphs = make(map[uint16]rune)
for character, glyph := range mapping {
if character == packedGlyphRune(glyph) {
prepared.Glyphs[glyph] = character
}
}
for character, glyph := range mapping {
if _, ok := prepared.Glyphs[glyph]; !ok {
prepared.Glyphs[glyph] = character
}
}
}
}
}
if definition != nil {
r.preparedFonts[id] = prepared
}
return prepared, nil
}
+6 -10
View File
@@ -23,7 +23,6 @@ import (
_ "image/gif" _ "image/gif"
"math" "math"
canvasimage "github.com/tdewolff/canvas/image"
_ "github.com/xiaoqidun/jbig2" _ "github.com/xiaoqidun/jbig2"
_ "golang.org/x/image/bmp" _ "golang.org/x/image/bmp"
"golang.org/x/image/draw" "golang.org/x/image/draw"
@@ -34,15 +33,12 @@ import (
// 入参: data 图片数据 // 入参: data 图片数据
// 返回: image.Image 图片对象, string 图片格式, error 错误信息 // 返回: image.Image 图片对象, string 图片格式, error 错误信息
func decodeImageData(data []byte) (image.Image, string, error) { func decodeImageData(data []byte) (image.Image, string, error) {
if isJPEGData(data) { if isJPEGData(data) || isPNGData(data) {
if img, err := canvasimage.NewJPEGImage(bytes.NewReader(data)); err == nil { img, err := newEncodedImage(data)
return img, "jpeg", nil if err != nil {
} return nil, "", err
}
if isPNGData(data) {
if img, err := canvasimage.NewPNGImage(bytes.NewReader(data)); err == nil {
return img, "png", nil
} }
return img, img.format, nil
} }
img, format, err := image.Decode(bytes.NewReader(data)) img, format, err := image.Decode(bytes.NewReader(data))
if err == nil { if err == nil {
@@ -412,7 +408,7 @@ func imageWithTransparentEdge(img image.Image) (image.Image, int) {
if w == 0 || h == 0 { if w == 0 || h == 0 {
return img, 0 return img, 0
} }
if src, ok := img.(*canvasimage.Image); ok && src.Mimetype == "image/jpeg" && src.Mask == nil { if src, ok := img.(*EncodedImage); ok && src.MIME() == "image/jpeg" {
return img, 0 return img, 0
} }
source := imagePixelSource(img) source := imagePixelSource(img)
+159
View File
@@ -0,0 +1,159 @@
// 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 (
"bytes"
"fmt"
"image"
"image/color"
_ "image/jpeg"
_ "image/png"
"io"
"sync"
)
// EncodedImage 保留PNG或JPEG原始编码,首次读取像素时解码
// Bytes返回只读数据,Image返回共享只读像素,解码错误由Image返回
type EncodedImage struct {
data []byte
format string
config image.Config
once sync.Once
decoded image.Image
err error
}
// NewEncodedImage 读取PNG或JPEG头信息,不解码像素,复制输入数据
// 入参: data 原始编码
// 返回: *EncodedImage 惰性图片, error 头信息或格式错误
func NewEncodedImage(data []byte) (*EncodedImage, error) {
return newEncodedImage(bytes.Clone(data))
}
// newEncodedImage 接管编码数据并读取头信息
// 入参: data 原始编码
// 返回: *EncodedImage 惰性图片, error 头信息或格式错误
func newEncodedImage(data []byte) (*EncodedImage, error) {
config, format, err := image.DecodeConfig(bytes.NewReader(data))
if err != nil {
return nil, err
}
if format != "jpeg" && format != "png" {
return nil, fmt.Errorf("unsupported encoded image %q", format)
}
return &EncodedImage{data: data, format: format, config: config}, nil
}
// Bytes 返回只读原始编码,调用方不得修改
// 返回: []byte 原始编码
func (img *EncodedImage) Bytes() []byte { return img.data }
// MIME 返回图片媒体类型
// 返回: string 媒体类型
func (img *EncodedImage) MIME() string { return "image/" + img.format }
// ColorModel 返回头信息中的颜色模型,不解码像素
// 返回: color.Model 颜色模型
func (img *EncodedImage) ColorModel() color.Model { return img.config.ColorModel }
// Bounds 返回图片范围,不解码像素
// 返回: image.Rectangle 图片范围
func (img *EncodedImage) Bounds() image.Rectangle {
return image.Rect(0, 0, img.config.Width, img.config.Height)
}
// Image 首次解码图片并缓存结果或错误,支持并发读取
// 返回: image.Image 只读像素, error 解码错误
func (img *EncodedImage) Image() (image.Image, error) {
img.once.Do(func() { img.decoded, _, img.err = image.Decode(bytes.NewReader(img.data)) })
return img.decoded, img.err
}
// At 返回像素颜色,解码失败时返回透明色,绘制前应通过Image检查错误
// 入参: x 横坐标, y 纵坐标
// 返回: color.Color 像素颜色
func (img *EncodedImage) At(x, y int) color.Color {
decoded, err := img.Image()
if err != nil {
return color.Transparent
}
return decoded.At(x, y)
}
// ImageResource 按资源标识读取只读图片,PNG和JPEG保留原始编码并惰性解码
// 应先读取源页面以加载页面资源,返回EncodedImage时通过Image检查像素解码错误
// 入参: id 图片资源标识
// 返回: image.Image 图片资源, error 读取错误
func (r *Renderer) ImageResource(id string) (image.Image, error) {
path, ok := r.Reader.ResMap[id]
if !ok {
return nil, fmt.Errorf("image resource %q not found", id)
}
return r.cachedImageResource(path)
}
// cachedImageResource 复用原始图片资源,不依赖输出后端
// 入参: resPath 图片资源路径
// 返回: image.Image 图片对象, error 读取错误
func (r *Renderer) cachedImageResource(resPath string) (image.Image, error) {
resPath = cleanPackagePath(r.Reader.ResPath(resPath))
img, ok := r.imageCache[resPath]
if !ok {
var err error
img, err = r.readImageResource(resPath)
if err != nil {
return nil, err
}
if r.imageCache == nil {
r.imageCache = make(map[string]image.Image)
}
r.imageCache[resPath] = img
}
return img, nil
}
// decodeImageResource 读取或复用图片资源,按输出需要解码像素
// 入参: resPath 图片资源路径
// 返回: image.Image 图片对象, error 读取或解码错误
func (r *Renderer) decodeImageResource(resPath string) (image.Image, error) {
img, err := r.cachedImageResource(resPath)
if err != nil {
return nil, err
}
if r.decodeImages {
if source, ok := img.(*EncodedImage); ok {
return source.Image()
}
}
return img, nil
}
// readImageResource 读取图片资源并保留可直接嵌入的原始编码
// 入参: resPath 图片资源路径
// 返回: image.Image 图片对象, error 读取或解码错误
func (r *Renderer) readImageResource(resPath string) (image.Image, error) {
rc, err := r.Reader.openFile(resPath)
if err != nil {
return nil, err
}
defer rc.Close()
data, err := io.ReadAll(rc)
if err != nil {
return nil, err
}
img, _, err := decodeImageData(data)
return img, err
}
+11 -12
View File
@@ -191,21 +191,21 @@ func (r *Renderer) parseStrokeColor(strokeColor *StrokeColor) color.Color {
return r.parseFillColor((*FillColor)(strokeColor)) return r.parseFillColor((*FillColor)(strokeColor))
} }
// patternPaint 底纹画刷 // PatternPaint 底纹画刷
type patternPaint struct { type PatternPaint struct {
*Pattern *Pattern
color FillColor Color FillColor
alpha *int Alpha *int
} }
// parsePatternPaint 解析底纹画刷 // parsePatternPaint 解析底纹画刷
// 入参: fill 填充颜色节点 // 入参: fill 填充颜色节点
// 返回: *patternPaint 底纹画刷 // 返回: *PatternPaint 底纹画刷
func parsePatternPaint(fill *FillColor) *patternPaint { func parsePatternPaint(fill *FillColor) *PatternPaint {
if fill.Pattern == nil { if fill.Pattern == nil {
return nil return nil
} }
return &patternPaint{Pattern: fill.Pattern, color: FillColor{Value: fill.Value, Index: fill.Index, ColorSpace: fill.ColorSpace}, alpha: fill.Alpha} return &PatternPaint{Pattern: fill.Pattern, Color: FillColor{Value: fill.Value, Index: fill.Index, ColorSpace: fill.ColorSpace}, Alpha: fill.Alpha}
} }
// parseShdColor 解析渐变颜色 // parseShdColor 解析渐变颜色
@@ -219,12 +219,11 @@ func (r *Renderer) parseShdColor(segments []ShdSegment, alpha *int) color.Color
return r.parseColorWithAlpha(c.Value, c.Index, c.ColorSpace, mergeAlpha(c.Alpha, alpha)) return r.parseColorWithAlpha(c.Value, c.Index, c.ColorSpace, mergeAlpha(c.Alpha, alpha))
} }
// GradientStops 解析OFD渐变分段的位置与透明度,返回独立数据并保留原分段顺序
// GradientStops 解析OFD渐变分段的位置与透明度,返回独立数据并保留原分段顺序 // GradientStops 解析OFD渐变分段的位置与透明度,返回独立数据并保留原分段顺序
// 入参: segments 渐变分段, alpha 透明度 // 入参: segments 渐变分段, alpha 透明度
// 返回: []RasterStop 后端无关的渐变分段 // 返回: []ColorStop 后端无关的渐变分段
func (r *Renderer) GradientStops(segments []ShdSegment, alpha *int) []RasterStop { func (r *Renderer) GradientStops(segments []ShdSegment, alpha *int) []ColorStop {
var gradient []RasterStop var gradient []ColorStop
position, step := 0.0, 0.0 position, step := 0.0, 0.0
for i, segment := range segments { for i, segment := range segments {
if !segment.positionMissing { if !segment.positionMissing {
@@ -246,7 +245,7 @@ func (r *Renderer) GradientStops(segments []ShdSegment, alpha *int) []RasterStop
offset := position offset := position
position += step position += step
segmentAlpha := mergeAlpha(segment.Color.Alpha, alpha) segmentAlpha := mergeAlpha(segment.Color.Alpha, alpha)
gradient = append(gradient, RasterStop{Offset: offset, Color: r.ResolveColor(segment.Color.Value, segment.Color.Index, segment.Color.ColorSpace, segmentAlpha)}) gradient = append(gradient, ColorStop{Offset: offset, Color: r.ResolveColor(segment.Color.Value, segment.Color.Index, segment.Color.ColorSpace, segmentAlpha)})
} }
if len(gradient) == 0 { if len(gradient) == 0 {
return nil return nil
+87
View File
@@ -0,0 +1,87 @@
// 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 "image/color"
// ColorStop 保存预乘RGBA颜色节点及渐变区间位置
type ColorStop struct {
Offset float64
Color color.RGBA
}
// PaintKind 区分无画刷、纯色、轴向渐变、径向渐变和图案
type PaintKind uint8
const (
PaintNone PaintKind = iota
PaintSolid
PaintLinear
PaintRadial
PaintPattern
)
// Paint 保存后端无关的颜色、渐变和图案,不修改源节点
type Paint struct {
Kind PaintKind
Color color.RGBA
Gradient *Shading
Pattern *PatternPaint
}
// Shading 保存OFD对象局部坐标中的渐变,单位为毫米,纵轴向下
// Extend、MapType和MapUnit保留标准的延伸及重复语义
type Shading struct {
Start, End Point
StartRadius, EndRadius float64
Stops []ColorStop
Extend, MapType string
MapUnit, Eccentricity, Angle float64
}
// ResolvePaint 解析颜色、透明度、渐变分段和图案,不依赖绘图库
// 入参: fill 填充或转换后的描边节点
// 返回: Paint 只读画刷
func (r *Renderer) ResolvePaint(fill *FillColor) Paint {
if fill == nil {
return Paint{}
}
if fill.Pattern != nil {
return Paint{Kind: PaintPattern, Pattern: parsePatternPaint(fill)}
}
base := r.parseFillColor(fill)
if base == nil {
return Paint{}
}
paint := Paint{Kind: PaintSolid, Color: colorToRGBA(base)}
if node := fill.AxialShd; node != nil {
start, end := parseFloats(node.StartPoint), parseFloats(node.EndPoint)
stops := r.GradientStops(node.Segment, fill.Alpha)
if len(start) >= 2 && len(end) >= 2 && len(stops) != 0 && (!geometryEqual(start[0], end[0]) || !geometryEqual(start[1], end[1])) {
paint.Kind = PaintLinear
paint.Gradient = &Shading{Start: Point{X: start[0], Y: start[1]}, End: Point{X: end[0], Y: end[1]}, Stops: stops, Extend: node.Extend, MapType: node.MapType, MapUnit: node.MapUnit}
return paint
}
}
if node := fill.RadialShd; node != nil && node.EndRadius > 0 {
start, end := parseFloats(node.StartPoint), parseFloats(node.EndPoint)
stops := r.GradientStops(node.Segment, fill.Alpha)
if len(start) >= 2 && len(end) >= 2 && len(stops) != 0 {
paint.Kind = PaintRadial
paint.Gradient = &Shading{Start: Point{X: start[0], Y: start[1]}, End: Point{X: end[0], Y: end[1]}, StartRadius: node.StartRadius, EndRadius: node.EndRadius, Stops: stops, Extend: node.Extend, MapType: node.MapType, MapUnit: node.MapUnit, Eccentricity: node.Eccentricity, Angle: node.Angle}
}
}
return paint
}
+1 -7
View File
@@ -79,18 +79,12 @@ const (
RasterRadial RasterRadial
) )
// RasterStop 按Offset升序排列的预乘RGBA颜色节点
type RasterStop struct {
Offset float64
Color color.RGBA
}
// RasterGradient 局部坐标下的线性或双圆径向渐变,区间外延续边界颜色 // RasterGradient 局部坐标下的线性或双圆径向渐变,区间外延续边界颜色
type RasterGradient struct { type RasterGradient struct {
Kind RasterGradientKind Kind RasterGradientKind
Start, End RasterPoint Start, End RasterPoint
R0, R1 float64 R0, R1 float64
Stops []RasterStop Stops []ColorStop
} }
// PixelSize 检查页面尺寸并计算目标像素大小 // PixelSize 检查页面尺寸并计算目标像素大小
+157
View File
@@ -0,0 +1,157 @@
// 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
// PageScene 保存后端无关的页面语义与绘制顺序,不提前展开文字或重新编码图片
// 场景借用源页面及资源的只读引用,编辑文档后应重新构建
type PageScene struct {
Source *PageContent
Box Box
commands []sceneCommand
}
// sceneCommand 保存叶子对象、签章或嵌套分组事件
type sceneCommand struct {
kind sceneCommandKind
object *GraphicObject
state RenderState
stamp Stamp
annotation string
count int
}
// sceneCommandKind 区分场景中的绘制和分组事件
type sceneCommandKind uint8
const (
sceneObject sceneCommandKind = iota
sceneStamp
sceneBeginObject
sceneBeginAnnotation
sceneEnd
sceneSkip
)
// CompileScene 展开页面结构,保留原文、字形索引、动作和对象标识
// 不提前绘制文字或图片,裁剪轮廓按几何后端需求解析
// 入参: page 源页面
// 返回: *PageScene 只读语义场景, error 遍历或几何错误
func (r *Renderer) CompileScene(page *PageContent) (*PageScene, error) {
box, err := r.GetPageBox(page)
if err != nil {
return nil, err
}
scene := &PageScene{Source: page, Box: box}
if err := r.WalkPage(page, (*sceneRecorder)(scene)); err != nil {
return nil, err
}
return scene, nil
}
// Walk 重放语义场景,访问器仍可自行处理文字和原始图片
// 入参: visitor 页面访问器
// 返回: error 访问器错误
func (s *PageScene) Walk(visitor PageVisitor) error {
groups, grouped := visitor.(PageGroups)
var stack []bool
for _, command := range s.commands {
switch command.kind {
case sceneObject:
if err := visitor.DrawObject(command.object, command.state); err != nil {
closeSceneGroups(groups, stack)
return err
}
case sceneStamp:
if err := visitor.DrawStamp(command.stamp); err != nil {
closeSceneGroups(groups, stack)
return err
}
case sceneBeginObject:
if grouped {
stack = append(stack, groups.BeginObject(command.object))
}
case sceneBeginAnnotation:
if grouped {
stack = append(stack, groups.BeginAnnotation(command.annotation))
}
case sceneEnd:
if grouped {
if stack[len(stack)-1] {
groups.EndObject()
}
stack = stack[:len(stack)-1]
}
case sceneSkip:
if grouped {
groups.SkipObjects(command.count)
}
}
}
return nil
}
// closeSceneGroups 在访问器失败时配对结束已建立的分组
// 入参: groups 分组访问器, stack 分组状态
func closeSceneGroups(groups PageGroups, stack []bool) {
for i := len(stack) - 1; i >= 0; i-- {
if stack[i] {
groups.EndObject()
}
}
}
// sceneRecorder 收集公共遍历事件,不解释绘制外观
type sceneRecorder PageScene
// DrawObject 保留源对象及继承状态
// 入参: object 源对象, state 继承状态
// 返回: error 固定为空
func (s *sceneRecorder) DrawObject(object *GraphicObject, state RenderState) error {
s.commands = append(s.commands, sceneCommand{kind: sceneObject, object: object, state: state})
return nil
}
// DrawStamp 保留签章外观
// 入参: stamp 签章
// 返回: error 固定为空
func (s *sceneRecorder) DrawStamp(stamp Stamp) error {
s.commands = append(s.commands, sceneCommand{kind: sceneStamp, stamp: stamp})
return nil
}
// BeginObject 记录对象分组
// 入参: object 源对象
// 返回: bool 固定为true
func (s *sceneRecorder) BeginObject(object *GraphicObject) bool {
s.commands = append(s.commands, sceneCommand{kind: sceneBeginObject, object: object})
return true
}
// BeginAnnotation 记录注解分组
// 入参: id 注解标识
// 返回: bool 固定为true
func (s *sceneRecorder) BeginAnnotation(id string) bool {
s.commands = append(s.commands, sceneCommand{kind: sceneBeginAnnotation, annotation: id})
return true
}
// EndObject 记录分组结束
func (s *sceneRecorder) EndObject() { s.commands = append(s.commands, sceneCommand{kind: sceneEnd}) }
// SkipObjects 记录不可见成员数量,保持可编辑输出的对象编号
// 入参: count 对象数量
func (s *sceneRecorder) SkipObjects(count int) {
s.commands = append(s.commands, sceneCommand{kind: sceneSkip, count: count})
}
+273
View File
@@ -0,0 +1,273 @@
// 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"
// RenderState 保存对象继承的样式、父变换和页面坐标裁剪,所有引用只读
// Clip为nil表示未裁剪,空路径表示完全裁去,单位为毫米
type RenderState struct {
Defaults *DrawParam
Parent *Matrix
BoundaryInCTM bool
Clip *GeometryPath
}
// PageVisitor 消费按标准图层顺序展开的对象与印章,源对象和状态只读
// 保留引用时文档也须保持不变,坐标与属性继续遵循OFD语义
// 复合图元的变换、透明度和引用由公共遍历处理,具体对象外观由访问器解释
type PageVisitor interface {
DrawObject(object *GraphicObject, state RenderState) error
DrawStamp(stamp Stamp) error
}
// PageGroups 提供可选的对象及注解分组,返回true时配对调用EndObject
// SkipObjects保持不可见复合图元的成员编号,供可编辑矢量输出使用
type PageGroups interface {
BeginObject(object *GraphicObject) bool
BeginAnnotation(id string) bool
EndObject()
SkipObjects(count int)
}
// WalkPage 按模板、图层、注解和印章顺序访问页面,不依赖任何绘图库
// 页面背景由调用方处理,文字裁剪可通过几何后端加载字体,图片由访问器按需加载
// 入参: page 页面内容, visitor 页面访问器
// 返回: error 遍历或访问器错误
func (r *Renderer) WalkPage(page *PageContent, visitor PageVisitor) error {
for order := range 3 {
if r.Reader.doc != nil {
for _, ref := range page.Template {
kind := ref.ZOrder
if kind == "" {
kind = "Background"
}
if layerOrder(kind) != order {
continue
}
template := r.loadTemplate(ref.TemplateID)
if template == nil {
continue
}
for templateOrder := range 3 {
if err := r.walkLayers(template.Content.Layer, templateOrder, visitor); err != nil {
return err
}
}
}
}
if err := r.walkLayers(page.Content.Layer, order, visitor); err != nil {
return err
}
}
if r.RenderAnnotations {
for _, annotation := range r.Reader.Annots[page.ID] {
if annotation.Visible != nil && !*annotation.Visible {
continue
}
if err := r.walkAnnotation(annotation, visitor); err != nil {
return err
}
}
}
for _, stamp := range r.Reader.Stamps[page.ID] {
if err := visitor.DrawStamp(stamp); err != nil {
return err
}
}
return nil
}
// walkAnnotation 访问注解外观并保持独立分组和定位
// 入参: annotation 注解, visitor 页面访问器
// 返回: error 遍历或访问器错误
func (r *Renderer) walkAnnotation(annotation Annotation, visitor PageVisitor) error {
if groups, ok := visitor.(PageGroups); ok && groups.BeginAnnotation(annotation.ID) {
defer groups.EndObject()
}
box, _ := ParseBox(annotation.Appearance.Boundary)
matrix := TranslationMatrix(box.X, box.Y)
state := RenderState{Parent: &matrix}
for i := range annotation.Appearance.Objects {
if err := r.WalkObject(&annotation.Appearance.Objects[i], state, visitor); err != nil {
return err
}
}
return nil
}
// walkLayers 按原顺序访问指定层级,不重新排列同类图层
// 入参: layers 图层列表, order 层级顺序, visitor 页面访问器
// 返回: error 遍历或访问器错误
func (r *Renderer) walkLayers(layers []Layer, order int, visitor PageVisitor) error {
for _, layer := range layers {
if layerOrder(layer.Type) != order {
continue
}
state := RenderState{Defaults: r.drawParamDefaults(layer.DrawParam, nil)}
if len(layer.Objects) != 0 {
for i := range layer.Objects {
if err := r.WalkObject(&layer.Objects[i], state, visitor); err != nil {
return err
}
}
continue
}
for _, object := range layer.TextObject {
if err := r.WalkObject(&GraphicObject{Type: "TextObject", TextObject: object}, state, visitor); err != nil {
return err
}
}
for _, object := range layer.PathObject {
if err := r.WalkObject(&GraphicObject{Type: "PathObject", PathObject: object}, state, visitor); err != nil {
return err
}
}
for _, object := range layer.ImageObject {
if err := r.WalkObject(&GraphicObject{Type: "ImageObject", ImageObject: object}, state, visitor); err != nil {
return err
}
}
for _, object := range layer.CompositeGraphicUnit {
if err := r.WalkObject(&GraphicObject{Type: "CompositeGraphicUnit", CompositeGraphicUnit: object}, state, visitor); err != nil {
return err
}
}
}
return nil
}
// WalkObject 访问单个对象或展开复合图元,保留父级裁剪和源对象标识
// 入参: object 源对象, state 继承状态, visitor 页面访问器
// 返回: error 遍历或访问器错误
func (r *Renderer) WalkObject(object *GraphicObject, state RenderState, visitor PageVisitor) error {
return r.walkObject(object, state, visitor, nil)
}
// walkObject 访问对象并传递当前资源引用链
// 入参: object 源对象, state 继承状态, visitor 页面访问器, references 当前资源引用链
// 返回: error 遍历或访问器错误
func (r *Renderer) walkObject(object *GraphicObject, state RenderState, visitor PageVisitor, references map[string]bool) error {
if groups, ok := visitor.(PageGroups); ok && groups.BeginObject(object) {
defer groups.EndObject()
}
switch object.Type {
case "TextObject", "PathObject", "ImageObject":
return visitor.DrawObject(object, state)
case "CompositeGraphicUnit", "CompositeObject":
return r.walkComposite(object.CompositeGraphicUnit, state, visitor, references)
default:
return fmt.Errorf("unsupported graphic object %q", object.Type)
}
}
// walkComposite 展开复合图元的资源、成员与透明度,不修改共享资源
// 入参: object 复合图元, state 继承状态, visitor 页面访问器, references 当前资源引用链
// 返回: error 几何或访问器错误
func (r *Renderer) walkComposite(object CompositeGraphicUnit, state RenderState, visitor PageVisitor, references map[string]bool) error {
if object.Visible != nil && !*object.Visible {
if groups, ok := visitor.(PageGroups); ok {
count, ref, seen := len(object.Objects), object.ResourceID, make(map[string]bool)
for ref != "" && !seen[ref] {
seen[ref] = true
unit := r.CompositeGraphicUnits[ref]
if unit == nil {
break
}
count, ref = count+len(unit.Objects), unit.ResourceID
}
groups.SkipObjects(count)
}
return nil
}
box, _ := ParseBox(object.Boundary)
boundary := TranslationMatrix(box.X, box.Y)
if state.Parent != nil {
boundary = state.Parent.Multiply(boundary)
}
matrix := boundary.Multiply(NewMatrix(object.CTM))
clips, clipMatrix := object.Clips, matrix
if clips != nil && clips.TransFlag != nil && !*clips.TransFlag {
copy := *clips
copy.TransFlag = nil
clips, clipMatrix = &copy, boundary
}
if clips != nil {
geometry, err := r.Geometry()
if err != nil {
return err
}
state.Clip, err = geometry.Clip(r, clips, clipMatrix, state.Clip)
if err != nil {
return err
}
}
state.Defaults = r.drawParamDefaults(object.DrawParam, state.Defaults)
state.Parent = &matrix
if object.ResourceID != "" {
if references[object.ResourceID] {
return fmt.Errorf("cyclic composite resource %q", object.ResourceID)
}
if ref, ok := r.CompositeGraphicUnits[object.ResourceID]; ok {
if references == nil {
references = make(map[string]bool)
}
references[object.ResourceID] = true
copy := *ref
copy.Alpha = mergeAlpha(copy.Alpha, object.Alpha)
referenceState := state
referenceState.BoundaryInCTM = true
err := r.walkComposite(copy, referenceState, visitor, references)
delete(references, object.ResourceID)
if err != nil {
return err
}
}
}
if len(object.Objects) != 0 {
for _, member := range object.Objects {
member = mergeGraphicObjectAlpha(member, object.Alpha)
if err := r.walkObject(&member, state, visitor, references); err != nil {
return err
}
}
return nil
}
for _, member := range object.ImageObject {
member.Alpha = mergeAlpha(member.Alpha, object.Alpha)
if err := visitor.DrawObject(&GraphicObject{Type: "ImageObject", ImageObject: member}, state); err != nil {
return err
}
}
for _, member := range object.PathObject {
member.Alpha = mergeAlpha(member.Alpha, object.Alpha)
if err := visitor.DrawObject(&GraphicObject{Type: "PathObject", PathObject: member}, state); err != nil {
return err
}
}
for _, member := range object.TextObject {
member.Alpha = mergeAlpha(member.Alpha, object.Alpha)
if err := visitor.DrawObject(&GraphicObject{Type: "TextObject", TextObject: member}, state); err != nil {
return err
}
}
for _, member := range object.CompositeGraphicUnit {
member.Alpha = mergeAlpha(member.Alpha, object.Alpha)
if err := r.walkComposite(member, state, visitor, references); err != nil {
return err
}
}
return nil
}
+2 -1
View File
@@ -25,7 +25,6 @@ import (
// OnExportProgress可选,同步回报文档导出的已处理页数及总页数,返回错误则停止,页数完成不代表写入成功 // OnExportProgress可选,同步回报文档导出的已处理页数及总页数,返回错误则停止,页数完成不代表写入成功
// TransparentBackground关闭页面白底,供嵌套图案和印章保持透明背景 // TransparentBackground关闭页面白底,供嵌套图案和印章保持透明背景
type Renderer struct { type Renderer struct {
canvasFontState
Reader *Reader Reader *Reader
DPI float64 DPI float64
RenderAnnotations bool RenderAnnotations bool
@@ -41,7 +40,9 @@ type Renderer struct {
pageText *PageText pageText *PageText
textOnly bool textOnly bool
backends RenderBackends backends RenderBackends
backendStates map[any]any
resolvedFonts map[resolvedFontKey]resolvedFontResult resolvedFonts map[resolvedFontKey]resolvedFontResult
preparedFonts map[string]*PreparedFont
renderError error renderError error
TransparentBackground bool TransparentBackground bool
} }
+30 -6
View File
@@ -68,15 +68,39 @@ func (r *clipRenderer) RenderImage(img image.Image, m canvas.Matrix) {}
// 入参: clips 裁剪对象, pageH 页面高度, bx 边界X坐标, by 边界Y坐标, objectCTM 对象CTM, parentCTM 父级CTM, boundaryInCTM 边界是否参与父级CTM // 入参: clips 裁剪对象, pageH 页面高度, bx 边界X坐标, by 边界Y坐标, objectCTM 对象CTM, parentCTM 父级CTM, boundaryInCTM 边界是否参与父级CTM
// 返回: *canvas.Path 路径对象 // 返回: *canvas.Path 路径对象
func (r *Renderer) buildObjectClipPath(clips *Clips, pageH float64, bx, by float64, objectCTM Matrix, parentCTM *Matrix, boundaryInCTM bool) *canvas.Path { func (r *Renderer) buildObjectClipPath(clips *Clips, pageH float64, bx, by float64, objectCTM Matrix, parentCTM *Matrix, boundaryInCTM bool) *canvas.Path {
if clips == nil {
return nil
}
if !boundaryInCTM && parentCTM != nil { if !boundaryInCTM && parentCTM != nil {
objectCTM = parentCTM.Multiply(objectCTM) objectCTM = parentCTM.Multiply(objectCTM)
} }
p := r.buildClipPath(clips, pageH, bx, by, objectCTM) if clips.TransFlag != nil && !*clips.TransFlag {
if p != nil && boundaryInCTM && parentCTM != nil { copy := *clips
p = p.Transform(canvas.Matrix{ copy.TransFlag = nil
{parentCTM.a, -parentCTM.c, parentCTM.c*pageH + parentCTM.e}, clips = &copy
{-parentCTM.b, parentCTM.d, pageH*(1-parentCTM.d) - parentCTM.f}, objectCTM = NewMatrix("")
}) }
matrix := TranslationMatrix(bx, by).Multiply(objectCTM)
if boundaryInCTM && parentCTM != nil {
matrix = parentCTM.Multiply(matrix)
}
geometry, err := r.Geometry()
if err != nil {
r.renderError = err
return nil
}
path, err := geometry.Clip(r, clips, matrix, nil)
if err != nil {
r.renderError = err
return nil
}
p, err := geometryToCanvasPath(path)
if err != nil {
r.renderError = err
return nil
}
if p != nil {
p = p.Translate(0, pageH)
} }
return p return p
} }
+15 -5
View File
@@ -22,7 +22,7 @@ import (
"github.com/tdewolff/canvas" "github.com/tdewolff/canvas"
) )
// CompilePage 使用默认字体和几何解释器生成独立绘制数据 // CompilePage 使用Canvas页面解释器和配置的字体、几何后端生成独立绘制数据
// 入参: r 渲染器, page 页面内容 // 入参: r 渲染器, page 页面内容
// 返回: *RasterPage 绘制页面, error 编译错误 // 返回: *RasterPage 绘制页面, error 编译错误
func (CanvasBackend) CompilePage(r *Renderer, page *PageContent) (*RasterPage, error) { func (CanvasBackend) CompilePage(r *Renderer, page *PageContent) (*RasterPage, error) {
@@ -34,7 +34,11 @@ func (CanvasBackend) CompilePage(r *Renderer, page *PageContent) (*RasterPage, e
if _, _, err := result.PixelSize(); err != nil { if _, _, err := result.PixelSize(); err != nil {
return nil, err return nil, err
} }
compiler := &canvasPageCompiler{page: result} geometry, err := r.Geometry()
if err != nil {
return nil, err
}
compiler := &canvasPageCompiler{page: result, geometry: geometry}
if err := r.renderCanvasContext(canvas.NewContext(compiler), page); err != nil { if err := r.renderCanvasContext(canvas.NewContext(compiler), page); err != nil {
return nil, err return nil, err
} }
@@ -47,6 +51,7 @@ func (CanvasBackend) CompilePage(r *Renderer, page *PageContent) (*RasterPage, e
// canvasPageCompiler 将已解释的页面绘制转换为自有数据,不保存第三方绘图对象 // canvasPageCompiler 将已解释的页面绘制转换为自有数据,不保存第三方绘图对象
type canvasPageCompiler struct { type canvasPageCompiler struct {
page *RasterPage page *RasterPage
geometry GeometryBackend
err error err error
} }
@@ -69,7 +74,12 @@ func (c *canvasPageCompiler) RenderPath(path *canvas.Path, style canvas.Style, m
offset, dashes := canvas.ScaleDash(style.StrokeWidth, style.DashOffset, style.Dashes) offset, dashes := canvas.ScaleDash(style.StrokeWidth, style.DashOffset, style.Dashes)
stroke = stroke.Dash(offset, dashes...) stroke = stroke.Dash(offset, dashes...)
} }
stroke = stroke.Stroke(style.StrokeWidth, style.StrokeCapper, style.StrokeJoiner, canvas.PixelTolerance/(c.page.DPI/25.4)) var err error
stroke, err = canvasStrokePath(c.geometry, stroke, canvasStrokeOptions(style.StrokeWidth, style.StrokeCapper, style.StrokeJoiner, canvas.PixelTolerance/(c.page.DPI/25.4)))
if err != nil {
c.err = err
return
}
c.fill(stroke, style.Stroke, style.FillRule, m) c.fill(stroke, style.Stroke, style.FillRule, m)
} }
} }
@@ -100,9 +110,9 @@ func (c *canvasPageCompiler) fill(path *canvas.Path, paint canvas.Paint, rule ca
c.err = fmt.Errorf("unsupported raster gradient %T", paint.Gradient) c.err = fmt.Errorf("unsupported raster gradient %T", paint.Gradient)
return return
} }
g.Stops = make([]RasterStop, len(stops)) g.Stops = make([]ColorStop, len(stops))
for i, stop := range stops { for i, stop := range stops {
g.Stops[i] = RasterStop{Offset: stop.Offset, Color: stop.Color} g.Stops[i] = ColorStop{Offset: stop.Offset, Color: stop.Color}
} }
cmd.Paint.Gradient = g cmd.Paint.Gradient = g
} }
+6 -37
View File
@@ -54,7 +54,7 @@ func (CanvasBackend) ResolveFont(r *Renderer, id string, exact bool) (ResolvedFo
if source, family := r.fontSourceMatch(id, definition, exact); family != nil { if source, family := r.fontSourceMatch(id, definition, exact); family != nil {
sfnt := family.Face(12, style).Font.SFNT sfnt := family.Face(12, style).Font.SFNT
face := fontFaceInfo(sfnt.Tables["name"], source.face) face := fontFaceInfo(sfnt.Tables["name"], source.face)
r.fontSourceUsed[id] = source r.canvasState().fontSourceUsed[id] = source
return ResolvedFont{Data: fontSFNTData(sfnt), Source: source.name, Face: &face, Exact: source.exact}, nil return ResolvedFont{Data: fontSFNTData(sfnt), Source: source.name, Face: &face, Exact: source.exact}, nil
} }
if !exact { if !exact {
@@ -66,11 +66,11 @@ func (CanvasBackend) ResolveFont(r *Renderer, id string, exact bool) (ResolvedFo
// canvasFallbackFont 按需加载默认阅读字体,不用于精确编辑匹配 // canvasFallbackFont 按需加载默认阅读字体,不用于精确编辑匹配
// 返回: ResolvedFont 默认字体,未安装时数据为空 // 返回: ResolvedFont 默认字体,未安装时数据为空
func (r *Renderer) canvasFallbackFont() ResolvedFont { func (r *Renderer) canvasFallbackFont() ResolvedFont {
if r.fontFamily == nil { if r.canvasState().fontFamily == nil {
r.initCanvasFonts() r.initCanvasFonts()
} }
if r.defaultFontLoaded { if r.canvasState().defaultFontLoaded {
return ResolvedFont{Data: fontSFNTData(r.fontFamily.Face(12, canvas.FontRegular).Font.SFNT)} return ResolvedFont{Data: fontSFNTData(r.canvasState().fontFamily.Face(12, canvas.FontRegular).Font.SFNT)}
} }
return ResolvedFont{} return ResolvedFont{}
} }
@@ -97,39 +97,8 @@ func (f canvasFontMetrics) Write() []byte {
return fontSFNTData(f.SFNT) return fontSFNTData(f.SFNT)
} }
// canvasFontState 保存默认适配器的字体与字形缓存,不向后端接口暴露绘图库类型
type canvasFontState struct {
fontFamily *canvas.FontFamily
defaultFontLoaded bool
fontMap map[string]*canvas.FontFamily
fontGIDMap map[string]map[uint16]rune
fontCIDMap map[string]map[uint16]rune
fontCache map[fontCacheKey]*canvas.FontFamily
svgFontCache map[*canvas.Font]SVGFont
fontSourceCache map[string][]fontSource
fontSourceUsed map[string]fontSource
fontDirCandidates map[string][]fontFileCandidate
fontFSCandidates map[int][]fontFileCandidate
textGlyphPathCache map[textGlyphPathCacheKey]textGlyphPathCacheValue
}
// resetFontCache 重置字体匹配、加载和字形缓存
func (r *Renderer) resetFontCache() {
r.resolvedFonts = make(map[resolvedFontKey]resolvedFontResult)
r.fontMap = make(map[string]*canvas.FontFamily)
r.fontGIDMap = make(map[string]map[uint16]rune)
r.fontCIDMap = make(map[string]map[uint16]rune)
r.fontCache = make(map[fontCacheKey]*canvas.FontFamily)
r.svgFontCache = make(map[*canvas.Font]SVGFont)
r.fontSourceCache = make(map[string][]fontSource)
r.fontSourceUsed = make(map[string]fontSource)
r.fontDirCandidates = make(map[string][]fontFileCandidate)
r.fontFSCandidates = make(map[int][]fontFileCandidate)
r.textGlyphPathCache = make(map[textGlyphPathCacheKey]textGlyphPathCacheValue)
}
// initCanvasFonts 初始化Canvas默认字体 // initCanvasFonts 初始化Canvas默认字体
func (r *Renderer) initCanvasFonts() { func (r *Renderer) initCanvasFonts() {
r.fontFamily = canvas.NewFontFamily("default") r.canvasState().fontFamily = canvas.NewFontFamily("default")
r.defaultFontLoaded = r.loadDefaultFonts() r.canvasState().defaultFontLoaded = r.loadDefaultFonts()
} }
+1 -1
View File
@@ -22,7 +22,7 @@ import "github.com/tdewolff/canvas"
// 返回: bool 是否加载成功 // 返回: bool 是否加载成功
func (r *Renderer) loadDefaultFonts() bool { func (r *Renderer) loadDefaultFonts() bool {
for _, name := range fontDefaultSystemNames() { for _, name := range fontDefaultSystemNames() {
if err := r.fontFamily.LoadSystemFont(name, canvas.FontRegular); err == nil { if err := r.canvasState().fontFamily.LoadSystemFont(name, canvas.FontRegular); err == nil {
return true return true
} }
} }
+15 -37
View File
@@ -64,10 +64,10 @@ type fontCacheKey struct {
// 入参: fontID 字体ID // 入参: fontID 字体ID
// 返回: *canvas.FontFamily 字体族 // 返回: *canvas.FontFamily 字体族
func (r *Renderer) loadFont(fontID string) *canvas.FontFamily { func (r *Renderer) loadFont(fontID string) *canvas.FontFamily {
if ff, ok := r.fontMap[fontID]; ok { if ff, ok := r.canvasState().fontMap[fontID]; ok {
return ff return ff
} }
resolved, err := r.ResolveFont(fontID, false) resolved, err := r.PrepareFont(fontID)
if err != nil { if err != nil {
r.renderError = err r.renderError = err
return nil return nil
@@ -82,34 +82,12 @@ func (r *Renderer) loadFont(fontID string) *canvas.FontFamily {
fontData := resolved.Data fontData := resolved.Data
fontStyle := canvasFontStyle(of) fontStyle := canvasFontStyle(of)
ff := canvas.NewFontFamily(of.FontName) ff := canvas.NewFontFamily(of.FontName)
if of.FontFile != "" {
if cidMap := getCFFCIDRuneMap(fontData); len(cidMap) > 0 {
r.fontCIDMap[fontID] = cidMap
}
if _, fixedData, mapping, _, err := FixFontDataAggressive(fontData, true, true); err == nil {
fontData = fixedData
if mapping != nil {
inv := make(map[uint16]rune)
for k, v := range mapping {
if k == packedGlyphRune(v) {
inv[v] = k
}
}
for k, v := range mapping {
if _, ok := inv[v]; !ok {
inv[v] = k
}
}
r.fontGIDMap[fontID] = inv
}
}
}
if err := ff.LoadFont(fontData, 0, fontStyle); err != nil { if err := ff.LoadFont(fontData, 0, fontStyle); err != nil {
r.renderError = err r.renderError = err
return nil return nil
} }
if r.Reader.fontCache[fontID] != nil { if r.Reader.fontCache[fontID] != nil {
r.fontMap[fontID] = ff r.canvasState().fontMap[fontID] = ff
} }
return ff return ff
} }
@@ -132,7 +110,7 @@ func canvasFontStyle(font *Font) canvas.FontStyle {
// 入参: fontID 字体ID, font OFD字体定义, style Canvas字体样式 // 入参: fontID 字体ID, font OFD字体定义, style Canvas字体样式
// 返回: []fontSource 字体来源列表 // 返回: []fontSource 字体来源列表
func (r *Renderer) fontSources(fontID string, font *Font, style canvas.FontStyle) []fontSource { func (r *Renderer) fontSources(fontID string, font *Font, style canvas.FontStyle) []fontSource {
if sources, ok := r.fontSourceCache[fontID]; ok { if sources, ok := r.canvasState().fontSourceCache[fontID]; ok {
return sources return sources
} }
bold := style&canvas.FontBold != 0 bold := style&canvas.FontBold != 0
@@ -181,7 +159,7 @@ func (r *Renderer) fontSources(fontID string, font *Font, style canvas.FontStyle
sources = appendFontSource(sources, seen, fontSource{kind: fontSourceFS, index: index, name: name}) sources = appendFontSource(sources, seen, fontSource{kind: fontSourceFS, index: index, name: name})
} }
} }
r.fontSourceCache[fontID] = sources r.canvasState().fontSourceCache[fontID] = sources
return sources return sources
} }
@@ -212,8 +190,8 @@ func appendFontSource(sources []fontSource, seen map[fontSourceKey]bool, source
// 入参: fontID 字体ID, font OFD字体定义, exact 是否禁止无关回退 // 入参: fontID 字体ID, font OFD字体定义, exact 是否禁止无关回退
// 返回: fontSource 字体来源, *canvas.FontFamily 已加载的字体族,未匹配时为空 // 返回: fontSource 字体来源, *canvas.FontFamily 已加载的字体族,未匹配时为空
func (r *Renderer) fontSourceMatch(fontID string, font *Font, exact bool) (fontSource, *canvas.FontFamily) { func (r *Renderer) fontSourceMatch(fontID string, font *Font, exact bool) (fontSource, *canvas.FontFamily) {
if source, ok := r.fontSourceUsed[fontID]; ok && (!exact || source.exact) && r.fontMap[fontID] != nil { if source, ok := r.canvasState().fontSourceUsed[fontID]; ok && (!exact || source.exact) && r.canvasState().fontMap[fontID] != nil {
return source, r.fontMap[fontID] return source, r.canvasState().fontMap[fontID]
} }
style := canvasFontStyle(font) style := canvasFontStyle(font)
for _, source := range r.fontSources(fontID, font, style) { for _, source := range r.fontSources(fontID, font, style) {
@@ -221,7 +199,7 @@ func (r *Renderer) fontSourceMatch(fontID string, font *Font, exact bool) (fontS
continue continue
} }
key := fontCacheKey{fontSourceKey: fontSourceKey{kind: source.kind, index: source.index, name: source.name, face: source.face}, style: style} key := fontCacheKey{fontSourceKey: fontSourceKey{kind: source.kind, index: source.index, name: source.name, face: source.face}, style: style}
if cached, ok := r.fontCache[key]; ok { if cached, ok := r.canvasState().fontCache[key]; ok {
if cached != nil { if cached != nil {
return source, cached return source, cached
} }
@@ -240,7 +218,7 @@ func (r *Renderer) fontSourceMatch(fontID string, font *Font, exact bool) (fontS
// 返回: *canvas.FontFamily 字体族 // 返回: *canvas.FontFamily 字体族
func (r *Renderer) loadFontSource(family *canvas.FontFamily, source fontSource, style canvas.FontStyle) *canvas.FontFamily { func (r *Renderer) loadFontSource(family *canvas.FontFamily, source fontSource, style canvas.FontStyle) *canvas.FontFamily {
key := fontCacheKey{fontSourceKey: fontSourceKey{kind: source.kind, index: source.index, name: source.name, face: source.face}, style: style} key := fontCacheKey{fontSourceKey: fontSourceKey{kind: source.kind, index: source.index, name: source.name, face: source.face}, style: style}
if cached, ok := r.fontCache[key]; ok { if cached, ok := r.canvasState().fontCache[key]; ok {
return cached return cached
} }
var err error var err error
@@ -262,10 +240,10 @@ func (r *Renderer) loadFontSource(family *canvas.FontFamily, source fontSource,
} }
} }
if err != nil { if err != nil {
r.fontCache[key] = nil r.canvasState().fontCache[key] = nil
return nil return nil
} }
r.fontCache[key] = family r.canvasState().fontCache[key] = family
return family return family
} }
@@ -273,7 +251,7 @@ func (r *Renderer) loadFontSource(family *canvas.FontFamily, source fontSource,
// 入参: dir 字体目录, names 字体名称, bold 是否粗体, italic 是否斜体 // 入参: dir 字体目录, names 字体名称, bold 是否粗体, italic 是否斜体
// 返回: []fontFileMatch 字体匹配列表 // 返回: []fontFileMatch 字体匹配列表
func (r *Renderer) matchFontFiles(dir string, names []string, bold, italic bool) []fontFileMatch { func (r *Renderer) matchFontFiles(dir string, names []string, bold, italic bool) []fontFileMatch {
candidates, ok := r.fontDirCandidates[dir] candidates, ok := r.canvasState().fontDirCandidates[dir]
if !ok { if !ok {
files, _ := filepath.Glob(filepath.Join(dir, "*")) files, _ := filepath.Glob(filepath.Join(dir, "*"))
candidates = fontFileCandidates(files, filepath.Base) candidates = fontFileCandidates(files, filepath.Base)
@@ -285,7 +263,7 @@ func (r *Renderer) matchFontFiles(dir string, names []string, bold, italic bool)
candidates = appendFontFileNames(candidates, i, fontFileNames(file)) candidates = appendFontFileNames(candidates, i, fontFileNames(file))
file.Close() file.Close()
} }
r.fontDirCandidates[dir] = candidates r.canvasState().fontDirCandidates[dir] = candidates
} }
return fontFileMatches(candidates, names, bold, italic) return fontFileMatches(candidates, names, bold, italic)
} }
@@ -298,7 +276,7 @@ func (r *Renderer) matchFontFS(index int, names []string, bold, italic bool) []f
if fonts, ok := fsys.(*FontFS); ok { if fonts, ok := fsys.(*FontFS); ok {
return fonts.matchStyle(names, bold, italic) return fonts.matchStyle(names, bold, italic)
} }
candidates, ok := r.fontFSCandidates[index] candidates, ok := r.canvasState().fontFSCandidates[index]
if !ok { if !ok {
files, _ := fs.Glob(fsys, "*") files, _ := fs.Glob(fsys, "*")
candidates = fontFileCandidates(files, path.Base) candidates = fontFileCandidates(files, path.Base)
@@ -315,7 +293,7 @@ func (r *Renderer) matchFontFS(index int, names []string, bold, italic bool) []f
candidates = appendFontFileNames(candidates, i, fontFileNames(reader)) candidates = appendFontFileNames(candidates, i, fontFileNames(reader))
file.Close() file.Close()
} }
r.fontFSCandidates[index] = candidates r.canvasState().fontFSCandidates[index] = candidates
} }
return fontFileMatches(candidates, names, bold, italic) return fontFileMatches(candidates, names, bold, italic)
} }
+50
View File
@@ -21,6 +21,56 @@ import (
"github.com/tdewolff/canvas" "github.com/tdewolff/canvas"
) )
// canvasStrokeOptions 转换Canvas描边样式,虚线由调用方传入
// 入参: width 描边宽度, cap 线帽, join 连接, tolerance 展开误差
// 返回: StrokeOptions 公共描边样式
func canvasStrokeOptions(width float64, cap canvas.Capper, join canvas.Joiner, tolerance float64) StrokeOptions {
options := StrokeOptions{Width: width, Cap: "Butt", Join: "Miter", MiterLimit: defaultMiterLimit, Tolerance: tolerance}
switch cap.(type) {
case canvas.RoundCapper:
options.Cap = "Round"
case canvas.SquareCapper:
options.Cap = "Square"
}
switch value := join.(type) {
case canvas.RoundJoiner:
options.Join = "Round"
case canvas.BevelJoiner:
options.Join = "Bevel"
case canvas.MiterJoiner:
options.MiterLimit = value.Limit
}
return options
}
// canvasStrokePath 通过配置的几何后端生成描边,不修改源路径
// 入参: geometry 几何后端, path Canvas路径, options 描边样式
// 返回: *canvas.Path 描边轮廓, error 几何错误
func canvasStrokePath(geometry GeometryBackend, path *canvas.Path, options StrokeOptions) (*canvas.Path, error) {
outline, err := geometry.Stroke(*geometryFromCanvasPath(path), options)
if err != nil {
return nil, err
}
return geometryToCanvasPath(&outline)
}
// strokeCanvasPath 将文字和复杂画刷的描边交给配置的几何后端
// 入参: path Canvas路径, width 描边宽度, cap 线帽, join 连接
// 返回: *canvas.Path 描边轮廓
func (r *Renderer) strokeCanvasPath(path *canvas.Path, width float64, cap canvas.Capper, join canvas.Joiner) *canvas.Path {
geometry, err := r.Geometry()
if err != nil {
r.renderError = err
return &canvas.Path{}
}
result, err := canvasStrokePath(geometry, path, canvasStrokeOptions(width, cap, join, canvas.Tolerance))
if err != nil {
r.renderError = err
return &canvas.Path{}
}
return result
}
// Path 解析对象的标准紧缩路径并转换到页面坐标 // Path 解析对象的标准紧缩路径并转换到页面坐标
// 入参: object 路径对象 // 入参: object 路径对象
// 返回: GeometryPath 页面路径, error 无效路径错误 // 返回: GeometryPath 页面路径, error 无效路径错误
+2 -2
View File
@@ -92,11 +92,11 @@ func (r *Renderer) cachedTextGlyphPath(face *canvas.FontFace, glyph textGlyph) (
direction: face.Direction, direction: face.Direction,
glyph: glyph, glyph: glyph,
} }
if cached, ok := r.textGlyphPathCache[key]; ok { if cached, ok := r.canvasState().textGlyphPathCache[key]; ok {
return cached.path, cached.width return cached.path, cached.width
} }
path, width := textGlyphPath(face, glyph) path, width := textGlyphPath(face, glyph)
r.textGlyphPathCache[key] = textGlyphPathCacheValue{path: path, width: width} r.canvasState().textGlyphPathCache[key] = textGlyphPathCacheValue{path: path, width: width}
return path, width return path, width
} }
+26 -62
View File
@@ -15,10 +15,9 @@
package ofdgo package ofdgo
import ( import (
"bufio" "bytes"
"fmt" "fmt"
"image" "image"
"io"
"math" "math"
"github.com/tdewolff/canvas" "github.com/tdewolff/canvas"
@@ -90,12 +89,35 @@ func (r *Renderer) renderImage(ctx *canvas.Context, obj ImageObject, pageH float
m[0][2] -= m[0][0]*p + m[0][1]*p m[0][2] -= m[0][0]*p + m[0][1]*p
m[1][2] -= m[1][0]*p + m[1][1]*p m[1][2] -= m[1][0]*p + m[1][1]*p
} }
ctx.RenderImage(img, ctx.CoordSystemView().Mul(ctx.View()).Mul(m)) ctx.RenderImage(r.canvasEncodedImage(img), ctx.CoordSystemView().Mul(ctx.View()).Mul(m))
if obj.Border != nil { if obj.Border != nil {
r.renderImageBorder(ctx, obj, box, pageH, parentCTM, boundaryInCTM, clipPath) r.renderImageBorder(ctx, obj, box, pageH, parentCTM, boundaryInCTM, clipPath)
} }
} }
// canvasEncodedImage 在Canvas输出边界保留原始编码,不重新压缩图片
// 入参: img 库图片
// 返回: image.Image Canvas可直接嵌入的图片
func (r *Renderer) canvasEncodedImage(img image.Image) image.Image {
source, ok := img.(*EncodedImage)
if !ok {
return img
}
state := r.canvasState()
if encoded, ok := state.images[source]; ok {
return encoded
}
var encoded *canvasimage.Image
if source.format == "jpeg" {
encoded, _ = canvasimage.NewJPEGImage(bytes.NewReader(source.Bytes()))
} else {
encoded, _ = canvasimage.NewPNGImage(bytes.NewReader(source.Bytes()))
}
encoded.Bytes = source.Bytes()
state.images[source] = encoded
return encoded
}
// renderImageBorder 渲染图像边框 // renderImageBorder 渲染图像边框
// 入参: ctx 画布上下文, obj 图片对象, box 图像边界, pageH 页面高度, parentCTM 父级CTM, boundaryInCTM 边界是否参与父级CTM, clipPath 裁剪路径 // 入参: ctx 画布上下文, obj 图片对象, box 图像边界, pageH 页面高度, parentCTM 父级CTM, boundaryInCTM 边界是否参与父级CTM, clipPath 裁剪路径
func (r *Renderer) renderImageBorder(ctx *canvas.Context, obj ImageObject, box Box, pageH float64, parentCTM *Matrix, boundaryInCTM bool, clipPath *canvas.Path) { func (r *Renderer) renderImageBorder(ctx *canvas.Context, obj ImageObject, box Box, pageH float64, parentCTM *Matrix, boundaryInCTM bool, clipPath *canvas.Path) {
@@ -166,7 +188,7 @@ func (r *Renderer) imageWithClip(img image.Image, clipPath *canvas.Path, m canva
bounds, w, h = resized.Bounds(), nw, nh bounds, w, h = resized.Bounds(), nw, nh
} }
clip := clipPath.Copy().Transform(m.Inv()) clip := clipPath.Copy().Transform(m.Inv())
compiler := &canvasPageCompiler{page: &RasterPage{Width: float64(w), Height: float64(h), DPI: 25.4}} compiler := &canvasPageCompiler{page: &RasterPage{Width: float64(w), Height: float64(h), DPI: 25.4}, geometry: r.backends.Geometry}
ctx := canvas.NewContext(compiler) ctx := canvas.NewContext(compiler)
ctx.SetFillColor(canvas.White) ctx.SetFillColor(canvas.White)
ctx.SetStrokeColor(canvas.Transparent) ctx.SetStrokeColor(canvas.Transparent)
@@ -216,61 +238,3 @@ func (r *Renderer) imageWithClip(img image.Image, clipPath *canvas.Path, m canva
} }
return out return out
} }
// decodeImageResource 解码图片资源
// 入参: resPath 图片资源路径
// 返回: image.Image 图片对象, error 错误信息
func (r *Renderer) decodeImageResource(resPath string) (image.Image, error) {
resPath = cleanPackagePath(r.Reader.ResPath(resPath))
img, ok := r.imageCache[resPath]
if !ok {
var err error
img, err = r.readImageResource(resPath)
if err != nil {
return nil, err
}
if r.imageCache == nil {
r.imageCache = make(map[string]image.Image)
}
r.imageCache[resPath] = img
}
if r.decodeImages {
if source, ok := img.(*canvasimage.Image); ok {
return source.Image()
}
}
return img, nil
}
// readImageResource 读取图片资源并保留可直接嵌入的原始编码
// 入参: resPath 图片资源路径
// 返回: image.Image 图片对象, error 错误信息
func (r *Renderer) readImageResource(resPath string) (image.Image, error) {
rc, err := r.Reader.openFile(resPath)
if err != nil {
return nil, err
}
defer rc.Close()
reader := bufio.NewReaderSize(rc, 8)
header, _ := reader.Peek(8)
if isJPEGData(header) {
img, err := canvasimage.NewJPEGImage(reader)
if err != nil {
return nil, err
}
return img, nil
}
if isPNGData(header) {
img, err := canvasimage.NewPNGImage(reader)
if err != nil {
return nil, err
}
return img, nil
}
data, err := io.ReadAll(reader)
if err != nil {
return nil, err
}
img, _, err := decodeImageData(data)
return img, err
}
+83 -141
View File
@@ -16,159 +16,101 @@ package ofdgo
import "github.com/tdewolff/canvas" import "github.com/tdewolff/canvas"
// renderAnnotations 渲染页面注释外观 // canvasPageVisitor 将公共页面遍历结果交给Canvas解释器
// 入参: ctx 画布上下文, pageID 页面ID, pageH 页面高度 type canvasPageVisitor struct {
func (r *Renderer) renderAnnotations(ctx *canvas.Context, pageID string, pageH float64) { renderer *Renderer
for _, annot := range r.Reader.Annots[pageID] { context *canvas.Context
if annot.Visible != nil && !*annot.Visible { height float64
continue }
// DrawObject 绘制叶子对象,转换页面坐标裁剪
// 入参: object 源对象, state 继承状态
// 返回: error 绘制错误
func (v *canvasPageVisitor) DrawObject(object *GraphicObject, state RenderState) error {
clip, err := geometryToCanvasPath(state.Clip)
if err != nil {
return err
} }
groups, grouped := ctx.Renderer.(interface { if clip != nil {
clip = clip.Translate(0, v.height)
}
switch object.Type {
case "TextObject":
v.renderer.renderText(v.context, object.TextObject, v.height, state.Defaults, state.Parent, state.BoundaryInCTM, clip)
case "PathObject":
v.renderer.renderPath(v.context, object.PathObject, v.height, state.Defaults, state.Parent, state.BoundaryInCTM, clip)
case "ImageObject":
v.renderer.renderImage(v.context, object.ImageObject, v.height, state.Parent, state.BoundaryInCTM, clip)
}
return v.renderer.renderError
}
// DrawStamp 绘制签章外观
// 入参: stamp 签章
// 返回: error 绘制错误
func (v *canvasPageVisitor) DrawStamp(stamp Stamp) error {
v.renderer.renderStamp(v.context, stamp, v.height)
return v.renderer.renderError
}
// BeginObject 开始可编辑对象分组
// 入参: object 源对象
// 返回: bool 是否建立分组
func (v *canvasPageVisitor) BeginObject(object *GraphicObject) bool {
groups, ok := v.context.Renderer.(interface {
beginObject(*GraphicObject) bool
endObject()
})
return ok && groups.beginObject(object)
}
// BeginAnnotation 开始注解分组
// 入参: id 注解标识
// 返回: bool 是否建立分组
func (v *canvasPageVisitor) BeginAnnotation(id string) bool {
groups, ok := v.context.Renderer.(interface {
beginAnnotation(string) bool beginAnnotation(string) bool
endObject() endObject()
}) })
grouped = grouped && groups.beginAnnotation(annot.ID) return ok && groups.beginAnnotation(id)
box, _ := ParseBox(annot.Appearance.Boundary)
ctm := Matrix{a: 1, d: 1, e: box.X, f: box.Y}
for i := range annot.Appearance.Objects {
r.renderObject(ctx, &annot.Appearance.Objects[i], pageH, nil, &ctm, false, nil)
}
if grouped {
groups.endObject()
}
}
} }
// renderTemplate 渲染模板 // EndObject 结束对象或注解分组
// 入参: ctx 画布上下文, templateID 模板ID, pageH 页面高度 func (v *canvasPageVisitor) EndObject() {
func (r *Renderer) renderTemplate(ctx *canvas.Context, templateID string, pageH float64) { v.context.Renderer.(interface{ endObject() }).endObject()
tplContent := r.loadTemplate(templateID)
if tplContent == nil {
return
}
for order := range 3 {
r.renderLayers(ctx, tplContent.Content.Layer, pageH, order)
}
} }
// renderLayers 按原顺序渲染指定类型的图层 // SkipObjects 保留不可见对象的编号
// 入参: ctx 画布上下文, layers 图层列表, pageH 页面高度, order 图层类型顺序 // 入参: count 对象数量
func (r *Renderer) renderLayers(ctx *canvas.Context, layers []Layer, pageH float64, order int) { func (v *canvasPageVisitor) SkipObjects(count int) {
for _, layer := range layers { if groups, ok := v.context.Renderer.(interface{ skipObjects(int) }); ok {
if layerOrder(layer.Type) == order {
r.renderLayer(ctx, layer, pageH, nil, nil)
}
}
}
// renderLayer 渲染图层
// 入参: ctx 画布上下文, layer 图层对象, pageH 页面高度, defaults 默认绘制参数, parentCTM 父级CTM
func (r *Renderer) renderLayer(ctx *canvas.Context, layer Layer, pageH float64, defaults *DrawParam, parentCTM *Matrix) {
defaults = r.drawParamDefaults(layer.DrawParam, defaults)
if len(layer.Objects) > 0 {
for i := range layer.Objects {
r.renderObject(ctx, &layer.Objects[i], pageH, defaults, parentCTM, false, nil)
}
return
}
for _, textObj := range layer.TextObject {
r.renderText(ctx, textObj, pageH, defaults, parentCTM, false, nil)
}
for _, pathObj := range layer.PathObject {
r.renderPath(ctx, pathObj, pageH, defaults, parentCTM, false, nil)
}
for _, imgObj := range layer.ImageObject {
r.renderImage(ctx, imgObj, pageH, parentCTM, false, nil)
}
for _, cgu := range layer.CompositeGraphicUnit {
r.renderCompositeGraphicUnit(ctx, cgu, pageH, defaults, parentCTM, false, nil)
}
}
// renderCompositeGraphicUnit 渲染复合图元
// 入参: ctx 画布上下文, cgu 复合图元对象, pageH 页面高度, defaults 默认绘制参数, parentCTM 父级CTM, boundaryInCTM 边界是否参与CTM变换, parentClip 父级裁剪路径
func (r *Renderer) renderCompositeGraphicUnit(ctx *canvas.Context, cgu CompositeGraphicUnit, pageH float64, defaults *DrawParam, parentCTM *Matrix, boundaryInCTM bool, parentClip *canvas.Path) {
if cgu.Visible != nil && !*cgu.Visible {
if groups, ok := ctx.Renderer.(interface{ skipObjects(int) }); ok {
count, ref, seen := len(cgu.Objects), cgu.ResourceID, make(map[string]bool)
for ref != "" && !seen[ref] {
seen[ref] = true
unit := r.CompositeGraphicUnits[ref]
if unit == nil {
break
}
count, ref = count+len(unit.Objects), unit.ResourceID
}
groups.skipObjects(count) groups.skipObjects(count)
} }
return
}
ctx.Push()
box, _ := ParseBox(cgu.Boundary)
boundaryCTM := TranslationMatrix(box.X, box.Y)
if parentCTM != nil {
boundaryCTM = parentCTM.Multiply(boundaryCTM)
}
currentCTM := boundaryCTM.Multiply(NewMatrix(cgu.CTM))
clips, clipCTM := cgu.Clips, currentCTM
if clips != nil && clips.TransFlag != nil && !*clips.TransFlag {
clipCopy := *clips
clipCopy.TransFlag = nil
clips, clipCTM = &clipCopy, boundaryCTM
}
clipPath := intersectClipPath(parentClip, r.buildClipPath(clips, pageH, 0, 0, clipCTM))
defaults = r.drawParamDefaults(cgu.DrawParam, defaults)
if cgu.ResourceID != "" {
if ref, ok := r.CompositeGraphicUnits[cgu.ResourceID]; ok {
refCopy := *ref
refCopy.Alpha = mergeAlpha(refCopy.Alpha, cgu.Alpha)
r.renderCompositeGraphicUnit(ctx, refCopy, pageH, defaults, &currentCTM, true, clipPath)
}
}
if len(cgu.Objects) > 0 {
for _, obj := range cgu.Objects {
obj = mergeGraphicObjectAlpha(obj, cgu.Alpha)
r.renderObject(ctx, &obj, pageH, defaults, &currentCTM, boundaryInCTM, clipPath)
}
ctx.Pop()
return
}
for _, imgObj := range cgu.ImageObject {
imgObj.Alpha = mergeAlpha(imgObj.Alpha, cgu.Alpha)
r.renderImage(ctx, imgObj, pageH, &currentCTM, boundaryInCTM, clipPath)
}
for _, pathObj := range cgu.PathObject {
pathObj.Alpha = mergeAlpha(pathObj.Alpha, cgu.Alpha)
r.renderPath(ctx, pathObj, pageH, defaults, &currentCTM, boundaryInCTM, clipPath)
}
for _, textObj := range cgu.TextObject {
textObj.Alpha = mergeAlpha(textObj.Alpha, cgu.Alpha)
r.renderText(ctx, textObj, pageH, defaults, &currentCTM, boundaryInCTM, clipPath)
}
for _, subCgu := range cgu.CompositeGraphicUnit {
subCgu.Alpha = mergeAlpha(subCgu.Alpha, cgu.Alpha)
r.renderCompositeGraphicUnit(ctx, subCgu, pageH, defaults, &currentCTM, boundaryInCTM, clipPath)
}
ctx.Pop()
} }
// renderObject 渲染图形对象 // canvasRenderState 将Canvas裁剪转换为公共页面状态
// 入参: ctx 画布上下文, obj 图形对象, pageH 页面高度, defaults 默认绘制参数, parentCTM 父级CTM, boundaryInCTM 边界是否参与CTM变换, parentClip 父级裁剪路径 // 入参: pageH 页面高度, defaults 默认样式, parentCTM 父变换, boundaryInCTM 边界是否参与变换, parentClip 父裁剪
func (r *Renderer) renderObject(ctx *canvas.Context, obj *GraphicObject, pageH float64, defaults *DrawParam, parentCTM *Matrix, boundaryInCTM bool, parentClip *canvas.Path) { // 返回: RenderState 公共页面状态
if groups, ok := ctx.Renderer.(interface { func canvasRenderState(pageH float64, defaults *DrawParam, parentCTM *Matrix, boundaryInCTM bool, parentClip *canvas.Path) RenderState {
beginObject(*GraphicObject) bool var clip *GeometryPath
endObject() if parentClip != nil {
}); ok && groups.beginObject(obj) { clip = geometryFromCanvasPath(parentClip.Copy().Translate(0, -pageH))
defer groups.endObject()
} }
switch obj.Type { return RenderState{Defaults: defaults, Parent: parentCTM, BoundaryInCTM: boundaryInCTM, Clip: clip}
case "TextObject": }
r.renderText(ctx, obj.TextObject, pageH, defaults, parentCTM, boundaryInCTM, parentClip)
case "PathObject": // renderCompositeGraphicUnit 通过公共遍历绘制复合图元
r.renderPath(ctx, obj.PathObject, pageH, defaults, parentCTM, boundaryInCTM, parentClip) // 入参: ctx 画布上下文, cgu 复合图元, pageH 页面高度, defaults 默认样式, parentCTM 父变换, boundaryInCTM 边界是否参与变换, parentClip 父裁剪
case "ImageObject": func (r *Renderer) renderCompositeGraphicUnit(ctx *canvas.Context, cgu CompositeGraphicUnit, pageH float64, defaults *DrawParam, parentCTM *Matrix, boundaryInCTM bool, parentClip *canvas.Path) {
r.renderImage(ctx, obj.ImageObject, pageH, parentCTM, boundaryInCTM, parentClip) if err := r.walkComposite(cgu, canvasRenderState(pageH, defaults, parentCTM, boundaryInCTM, parentClip), &canvasPageVisitor{r, ctx, pageH}, nil); err != nil {
case "CompositeGraphicUnit", "CompositeObject": r.renderError = err
r.renderCompositeGraphicUnit(ctx, obj.CompositeGraphicUnit, pageH, defaults, parentCTM, boundaryInCTM, parentClip) }
}
// renderObject 通过公共遍历绘制图形对象
// 入参: ctx 画布上下文, obj 图形对象, pageH 页面高度, defaults 默认样式, parentCTM 父变换, boundaryInCTM 边界是否参与变换, parentClip 父裁剪
func (r *Renderer) renderObject(ctx *canvas.Context, obj *GraphicObject, pageH float64, defaults *DrawParam, parentCTM *Matrix, boundaryInCTM bool, parentClip *canvas.Path) {
if err := r.WalkObject(obj, canvasRenderState(pageH, defaults, parentCTM, boundaryInCTM, parentClip), &canvasPageVisitor{r, ctx, pageH}); err != nil {
r.renderError = err
} }
} }
+2 -21
View File
@@ -63,27 +63,8 @@ func (r *Renderer) renderCanvasPageToContext(ctx *canvas.Context, page *PageCont
ctx.SetFillColor(canvas.White) ctx.SetFillColor(canvas.White)
ctx.DrawPath(0, 0, canvas.Rectangle(box.W, box.H)) ctx.DrawPath(0, 0, canvas.Rectangle(box.W, box.H))
} }
for order := range 3 { if err := r.WalkPage(page, &canvasPageVisitor{r, ctx, pageH}); err != nil {
if r.Reader.doc != nil { return err
for _, tplRef := range page.Template {
kind := tplRef.ZOrder
if kind == "" {
kind = "Background"
}
if layerOrder(kind) == order {
r.renderTemplate(ctx, tplRef.TemplateID, pageH)
}
}
}
r.renderLayers(ctx, page.Content.Layer, pageH, order)
}
if r.RenderAnnotations {
r.renderAnnotations(ctx, page.ID, pageH)
}
if stamps, ok := r.Reader.Stamps[page.ID]; ok {
for _, stamp := range stamps {
r.renderStamp(ctx, stamp, pageH)
}
} }
if r.renderError != nil { if r.renderError != nil {
return r.renderError return r.renderError
+29 -63
View File
@@ -15,27 +15,24 @@
package ofdgo package ofdgo
import ( import (
"github.com/tdewolff/canvas"
"math" "math"
"github.com/tdewolff/canvas"
) )
// parseFillPaint 解析填充画刷 // parseFillPaint 将公共画刷转换为Canvas对象
// 入参: fillColor 填充颜色节点, x X坐标, y Y坐标, pageH 页面高度 // 入参: fillColor 填充颜色节点, x X坐标, y Y坐标, pageH 页面高度
// 返回: any 填充画刷 // 返回: any 填充画刷
func (r *Renderer) parseFillPaint(fillColor *FillColor, x, y, pageH float64) any { func (r *Renderer) parseFillPaint(fillColor *FillColor, x, y, pageH float64) any {
if fillColor == nil { paint := r.ResolvePaint(fillColor)
switch paint.Kind {
case PaintNone, PaintPattern:
return nil return nil
case PaintLinear, PaintRadial:
return canvasShading(paint, x, y, pageH)
default:
return paint.Color
} }
if fillColor.Pattern != nil {
return nil
}
if gradient := r.parseAxialShdGradient(fillColor.AxialShd, fillColor.Alpha, x, y, pageH); gradient != nil {
return newShdPaint(gradient, fillColor.AxialShd.Extend, fillColor.AxialShd.MapType, fillColor.AxialShd.MapUnit)
}
if paint := r.parseRadialShdPaint(fillColor.RadialShd, fillColor.Alpha, x, y, pageH); paint != nil {
return paint
}
return r.parseFillColor(fillColor)
} }
// parseStrokePaint 解析勾边画刷 // parseStrokePaint 解析勾边画刷
@@ -45,39 +42,34 @@ func (r *Renderer) parseStrokePaint(strokeColor *StrokeColor, x, y, pageH float6
return r.parseFillPaint((*FillColor)(strokeColor), x, y, pageH) return r.parseFillPaint((*FillColor)(strokeColor), x, y, pageH)
} }
// parseShdSegments 将通用渐变分段转换为Canvas画刷 // canvasGradient 将公共颜色节点转换为Canvas渐变
// 入参: segments 渐变分段, alpha 透明度 // 入参: stops 渐变分段
// 返回: canvas.Grad 渐变分段 // 返回: canvas.Grad 渐变分段
func (r *Renderer) parseShdSegments(segments []ShdSegment, alpha *int) canvas.Grad { func canvasGradient(stops []ColorStop) canvas.Grad {
var gradient canvas.Grad var gradient canvas.Grad
for _, stop := range r.GradientStops(segments, alpha) { for _, stop := range stops {
gradient.Add(stop.Offset, stop.Color) gradient.Add(stop.Offset, stop.Color)
} }
return gradient return gradient
} }
// parseAxialShdGradient 解析轴向渐变 // canvasShading 转换坐标与椭圆渐变变换,不重新解析OFD样式
// 入参: axialShd 轴向渐变节点, alpha 透明度, x X坐标, y Y坐标, pageH 页面高度 // 入参: paint 公共渐变画刷, x X坐标, y Y坐标, pageH 页面高度
// 返回: canvas.Gradient 渐变对象 // 返回: *shdPaint Canvas渐变画刷
func (r *Renderer) parseAxialShdGradient(axialShd *AxialShd, alpha *int, x, y, pageH float64) canvas.Gradient { func canvasShading(paint Paint, x, y, pageH float64) *shdPaint {
if axialShd == nil { source := paint.Gradient
return nil gradient := canvasGradient(source.Stops)
start := canvas.Point{X: x + source.Start.X, Y: pageH - y - source.Start.Y}
end := canvas.Point{X: x + source.End.X, Y: pageH - y - source.End.Y}
if paint.Kind == PaintLinear {
return newShdPaint(gradient.ToLinear(start, end), source.Extend, source.MapType, source.MapUnit)
} }
start := parseFloats(axialShd.StartPoint) result := newShdPaint(gradient.ToRadial(start, source.StartRadius, end, source.EndRadius), source.Extend, source.MapType, source.MapUnit)
end := parseFloats(axialShd.EndPoint) if e := source.Eccentricity; 0 < e && e < 1 {
if len(start) < 2 || len(end) < 2 { result.view = canvas.Identity.Translate(start.X, start.Y).Rotate(-source.Angle).Scale(1, math.Sqrt(1-e*e)).Translate(-start.X, -start.Y)
return nil result.gradient = gradient.ToRadial(start, source.StartRadius, result.view.Inv().Dot(end), source.EndRadius)
} }
gradient := r.parseShdSegments(axialShd.Segment, alpha) return result
if gradient == nil {
return nil
}
startPoint := canvas.Point{X: x + start[0], Y: pageH - (y + start[1])}
endPoint := canvas.Point{X: x + end[0], Y: pageH - (y + end[1])}
if startPoint.Equals(endPoint) {
return nil
}
return gradient.ToLinear(startPoint, endPoint)
} }
// axialShdClip 获取轴向渐变的延伸裁剪区域 // axialShdClip 获取轴向渐变的延伸裁剪区域
@@ -101,29 +93,3 @@ func axialShdClip(gradient *canvas.LinearGradient, extend int, bounds canvas.Rec
} }
return area.ToPath().Transform(axis) return area.ToPath().Transform(axis)
} }
// parseRadialShdPaint 解析径向渐变画刷
// 入参: radialShd 径向渐变节点, alpha 透明度, x X坐标, y Y坐标, pageH 页面高度
// 返回: *shdPaint 渐变画刷
func (r *Renderer) parseRadialShdPaint(radialShd *RadialShd, alpha *int, x, y, pageH float64) *shdPaint {
if radialShd == nil || radialShd.EndRadius <= 0 {
return nil
}
start := parseFloats(radialShd.StartPoint)
end := parseFloats(radialShd.EndPoint)
if len(start) < 2 || len(end) < 2 {
return nil
}
gradient := r.parseShdSegments(radialShd.Segment, alpha)
if gradient == nil {
return nil
}
startPoint := canvas.Point{X: x + start[0], Y: pageH - (y + start[1])}
endPoint := canvas.Point{X: x + end[0], Y: pageH - (y + end[1])}
paint := newShdPaint(gradient.ToRadial(startPoint, radialShd.StartRadius, endPoint, radialShd.EndRadius), radialShd.Extend, radialShd.MapType, radialShd.MapUnit)
if e := radialShd.Eccentricity; 0 < e && e < 1 {
paint.view = canvas.Identity.Translate(startPoint.X, startPoint.Y).Rotate(-radialShd.Angle).Scale(1, math.Sqrt(1-e*e)).Translate(-startPoint.X, -startPoint.Y)
paint.gradient = gradient.ToRadial(startPoint, radialShd.StartRadius, paint.view.Inv().Dot(endPoint), radialShd.EndRadius)
}
return paint
}
+9 -8
View File
@@ -29,8 +29,8 @@ type pathStyle struct {
strokeColor color.Color strokeColor color.Color
fillPaint any fillPaint any
strokePaint any strokePaint any
fillPattern *patternPaint fillPattern *PatternPaint
strokePattern *patternPaint strokePattern *PatternPaint
lineWidth float64 lineWidth float64
lineCap canvas.Capper lineCap canvas.Capper
lineJoin canvas.Joiner lineJoin canvas.Joiner
@@ -280,9 +280,10 @@ func (r *Renderer) renderPath(ctx *canvas.Context, obj PathObject, pageH float64
} }
strokeClip := intersectClipPath(clipPath, shadingClip) strokeClip := intersectClipPath(clipPath, shadingClip)
_, repeat := strokePaint.(*repeatAxialGradient) _, repeat := strokePaint.(*repeatAxialGradient)
if strokeClip != nil || strokeView != canvas.Identity || repeat || style.strokePattern != nil { _, nativeGeometry := r.backends.Geometry.(CanvasBackend)
if strokeClip != nil || strokeView != canvas.Identity || repeat || style.strokePattern != nil || !nativeGeometry {
sp = sp.Copy() sp = sp.Copy()
sp = sp.Stroke(style.lineWidth, style.lineCap, style.lineJoin, canvas.Tolerance) sp = r.strokeCanvasPath(sp, style.lineWidth, style.lineCap, style.lineJoin)
sp = applyClipPath(sp, strokeClip) sp = applyClipPath(sp, strokeClip)
if style.strokePattern != nil { if style.strokePattern != nil {
r.renderPattern(ctx, style.strokePattern, pageH, sp, objectCTM) r.renderPattern(ctx, style.strokePattern, pageH, sp, objectCTM)
@@ -300,9 +301,9 @@ func (r *Renderer) renderPath(ctx *canvas.Context, obj PathObject, pageH float64
// renderPattern 渲染图案填充 // renderPattern 渲染图案填充
// 入参: ctx 画布上下文, pattern 底纹画刷, pageH 页面高度, clip 绘制区域, objectCTM 对象变换矩阵 // 入参: ctx 画布上下文, pattern 底纹画刷, pageH 页面高度, clip 绘制区域, objectCTM 对象变换矩阵
func (r *Renderer) renderPattern(ctx *canvas.Context, pattern *patternPaint, pageH float64, clip *canvas.Path, objectCTM Matrix) { func (r *Renderer) renderPattern(ctx *canvas.Context, pattern *PatternPaint, pageH float64, clip *canvas.Path, objectCTM Matrix) {
if bounds, ok := ctx.Renderer.(*boundsRenderer); ok { if bounds, ok := ctx.Renderer.(*boundsRenderer); ok {
if pattern.alpha == nil || *pattern.alpha > 0 { if pattern.Alpha == nil || *pattern.Alpha > 0 {
bounds.add(clip) bounds.add(clip)
} }
return return
@@ -358,7 +359,7 @@ func (r *Renderer) renderPattern(ctx *canvas.Context, pattern *patternPaint, pag
endX := int(math.Ceil(maxX/xStep)) + 1 endX := int(math.Ceil(maxX/xStep)) + 1
startY := int(math.Floor(minY/yStep)) - 1 startY := int(math.Floor(minY/yStep)) - 1
endY := int(math.Ceil(maxY/yStep)) + 1 endY := int(math.Ceil(maxY/yStep)) + 1
defaults := &DrawParam{FillColor: &pattern.color, StrokeColor: (*StrokeColor)(&pattern.color)} defaults := &DrawParam{FillColor: &pattern.Color, StrokeColor: (*StrokeColor)(&pattern.Color)}
for ix := startX; ix <= endX; ix++ { for ix := startX; ix <= endX; ix++ {
for iy := startY; iy <= endY; iy++ { for iy := startY; iy <= endY; iy++ {
tileCTM := patternCTM.Multiply(TranslationMatrix(float64(ix)*xStep, float64(iy)*yStep)) tileCTM := patternCTM.Multiply(TranslationMatrix(float64(ix)*xStep, float64(iy)*yStep))
@@ -369,7 +370,7 @@ func (r *Renderer) renderPattern(ctx *canvas.Context, pattern *patternPaint, pag
tileCTM = tileCTM.Multiply(Matrix{a: 1, d: -1, f: pattern.Height}) tileCTM = tileCTM.Multiply(Matrix{a: 1, d: -1, f: pattern.Height})
} }
for _, obj := range pattern.CellContent.Objects { for _, obj := range pattern.CellContent.Objects {
obj = mergeGraphicObjectAlpha(obj, pattern.alpha) obj = mergeGraphicObjectAlpha(obj, pattern.Alpha)
r.renderObject(ctx, &obj, pageH, defaults, &tileCTM, true, clip) r.renderObject(ctx, &obj, pageH, defaults, &tileCTM, true, clip)
} }
} }
+1 -1
View File
@@ -120,6 +120,6 @@ func (r *Renderer) renderStampImage(ctx *canvas.Context, img image.Image, s Stam
ctx.Push() ctx.Push()
ctx.Translate(box.X, pageH-(box.Y+box.H)) ctx.Translate(box.X, pageH-(box.Y+box.H))
ctx.Scale(box.W/float64(img.Bounds().Dx()), box.H/float64(img.Bounds().Dy())) ctx.Scale(box.W/float64(img.Bounds().Dx()), box.H/float64(img.Bounds().Dy()))
ctx.DrawImage(0, 0, img, canvas.DPMM(1.0)) ctx.DrawImage(0, 0, r.canvasEncodedImage(img), canvas.DPMM(1.0))
ctx.Pop() ctx.Pop()
} }
+58
View File
@@ -0,0 +1,58 @@
// 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 (
"image"
"github.com/tdewolff/canvas"
)
// canvasBackendState 保存Canvas字体、字形和编码图片适配缓存
type canvasBackendState struct {
images map[*EncodedImage]image.Image
fontFamily *canvas.FontFamily
defaultFontLoaded bool
fontMap map[string]*canvas.FontFamily
fontCache map[fontCacheKey]*canvas.FontFamily
svgFontCache map[*canvas.Font]SVGFont
fontSourceCache map[string][]fontSource
fontSourceUsed map[string]fontSource
fontDirCandidates map[string][]fontFileCandidate
fontFSCandidates map[int][]fontFileCandidate
textGlyphPathCache map[textGlyphPathCacheKey]textGlyphPathCacheValue
}
// canvasState 获取当前渲染器的Canvas私有缓存,按需创建且不向其他后端暴露
// 返回: *canvasBackendState Canvas缓存
func (r *Renderer) canvasState() *canvasBackendState {
key := CanvasBackend{}
if state, ok := r.backendStates[key]; ok {
return state.(*canvasBackendState)
}
state := &canvasBackendState{
images: make(map[*EncodedImage]image.Image),
fontMap: make(map[string]*canvas.FontFamily),
fontCache: make(map[fontCacheKey]*canvas.FontFamily),
svgFontCache: make(map[*canvas.Font]SVGFont),
fontSourceCache: make(map[string][]fontSource),
fontSourceUsed: make(map[string]fontSource),
fontDirCandidates: make(map[string][]fontFileCandidate),
fontFSCandidates: make(map[int][]fontFileCandidate),
textGlyphPathCache: make(map[textGlyphPathCacheKey]textGlyphPathCacheValue),
}
r.backendStates[key] = state
return state
}
+2 -2
View File
@@ -186,7 +186,7 @@ func (s *svgResourceRenderer) RenderText(text *canvas.Text, m canvas.Matrix) {
return return
} }
font := text.MostCommonFontFace().Font font := text.MostCommonFontFace().Font
resource, ok := s.renderer.svgFontCache[font] resource, ok := s.renderer.canvasState().svgFontCache[font]
if !ok { if !ok {
data := fontSFNTData(font.SFNT) data := fontSFNTData(font.SFNT)
resource = SVGFont{ resource = SVGFont{
@@ -198,7 +198,7 @@ func (s *svgResourceRenderer) RenderText(text *canvas.Text, m canvas.Matrix) {
if font.Style().Italic() { if font.Style().Italic() {
resource.Style = "italic" resource.Style = "italic"
} }
s.renderer.svgFontCache[font] = resource s.renderer.canvasState().svgFontCache[font] = resource
} }
if !s.seen[resource.Name] { if !s.seen[resource.Name] {
s.seen[resource.Name] = true s.seen[resource.Name] = true
+10 -43
View File
@@ -76,7 +76,7 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64
sizePt := sizeMM * ptPerMM sizePt := sizeMM * ptPerMM
fillColor := colorWithAlpha(canvas.Black, obj.Alpha) fillColor := colorWithAlpha(canvas.Black, obj.Alpha)
var fillPaint any = fillColor var fillPaint any = fillColor
var fillPattern *patternPaint var fillPattern *PatternPaint
var fillColorNode *FillColor var fillColorNode *FillColor
if defaults != nil { if defaults != nil {
fillColorNode = defaults.FillColor fillColorNode = defaults.FillColor
@@ -187,15 +187,6 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64
hasUnderline := strings.Contains(obj.Decoration, "Underline") hasUnderline := strings.Contains(obj.Decoration, "Underline")
_, shadedFill := fillPaint.(canvas.Gradient) _, shadedFill := fillPaint.(canvas.Gradient)
verticalAdvance := (obj.ReadDirection-obj.CharDirection)%180 != 0 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 codePos := 0
textPos := 0 textPos := 0
for index, tc := range obj.TextCode { for index, tc := range obj.TextCode {
@@ -215,17 +206,9 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64
if textRun != nil && tc.Index == "" { if textRun != nil && tc.Index == "" {
spans = textGlyphSpans(runes, glyphTransforms, codePos) spans = textGlyphSpans(runes, glyphTransforms, codePos)
} }
dxs, dys := parseFloats(tc.DeltaX), parseFloats(tc.DeltaY) positioner := NewTextPositioner(tc, obj.ReadDirection)
xs, ys := parseFloats(tc.X), parseFloats(tc.Y) 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 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 { for i, glyph := range glyphs {
str := glyph.Text str := glyph.Text
drawAsGlyphPath := drawAsPath || glyph.GlyphID >= 0 drawAsGlyphPath := drawAsPath || glyph.GlyphID >= 0
@@ -244,28 +227,12 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64
} else { } else {
glyphWidth = textGlyphWidth(face, glyph) glyphWidth = textGlyphWidth(face, glyph)
} }
if i < len(xs) { advance := glyphWidth * hScale
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 { if verticalAdvance {
previousAdvance = sizeMM advance = sizeMM
} }
position := positioner.Next(advance)
cx, cy := position.X, position.Y
var canvasX, canvasY float64 var canvasX, canvasY float64
if boundaryInCTM && parentCTM != nil { if boundaryInCTM && parentCTM != nil {
tx, ty := localCTM.Transform(cx, cy) tx, ty := localCTM.Transform(cx, cy)
@@ -328,7 +295,7 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64
if len(strokeStyle.dashPattern) > 0 { if len(strokeStyle.dashPattern) > 0 {
path = path.Dash(strokeStyle.dashOffset, strokeStyle.dashPattern...) path = path.Dash(strokeStyle.dashOffset, strokeStyle.dashPattern...)
} }
path = path.Stroke(strokeStyle.lineWidth, strokeStyle.lineCap, strokeStyle.lineJoin, canvas.Tolerance) path = r.strokeCanvasPath(path, strokeStyle.lineWidth, strokeStyle.lineCap, strokeStyle.lineJoin)
path = applyClipPath(path, strokeClip) path = applyClipPath(path, strokeClip)
if strokeStyle.strokePattern != nil { if strokeStyle.strokePattern != nil {
r.renderPattern(ctx, strokeStyle.strokePattern, pageH, path, objectCTM) r.renderPattern(ctx, strokeStyle.strokePattern, pageH, path, objectCTM)
@@ -341,7 +308,7 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64
underline := &canvas.Path{} underline := &canvas.Path{}
underline.MoveTo(0, -sizeMM*0.1) underline.MoveTo(0, -sizeMM*0.1)
underline.LineTo(glyphWidth*scaleX, -sizeMM*0.1) underline.LineTo(glyphWidth*scaleX, -sizeMM*0.1)
underline = underline.Stroke(sizeMM*0.05, canvas.ButtCap, canvas.MiterJoin, canvas.Tolerance) underline = r.strokeCanvasPath(underline, sizeMM*0.05, canvas.ButtCap, canvas.MiterJoin)
if shouldFill && fillPattern != nil { if shouldFill && fillPattern != nil {
r.renderPattern(ctx, fillPattern, pageH, applyClipPath(underline.Transform(transform), clipPath), objectCTM) r.renderPattern(ctx, fillPattern, pageH, applyClipPath(underline.Transform(transform), clipPath), objectCTM)
} else if shouldFill { } else if shouldFill {
@@ -380,7 +347,7 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64
underline := &canvas.Path{} underline := &canvas.Path{}
underline.MoveTo(0, -off) underline.MoveTo(0, -off)
underline.LineTo(textWidth, -off) underline.LineTo(textWidth, -off)
underline = underline.Stroke(uw, canvas.ButtCap, canvas.MiterJoin, canvas.Tolerance) underline = r.strokeCanvasPath(underline, uw, canvas.ButtCap, canvas.MiterJoin)
underline = applyClipPath(underline.Transform(textTransform), clipPath) underline = applyClipPath(underline.Transform(textTransform), clipPath)
if fillPattern != nil { if fillPattern != nil {
r.renderPattern(ctx, fillPattern, pageH, underline, objectCTM) r.renderPattern(ctx, fillPattern, pageH, underline, objectCTM)
+6 -2
View File
@@ -185,10 +185,14 @@ func (r *Renderer) fontGlyphRune(fontID string, glyphID int) (rune, bool) {
return 0, false return 0, false
} }
id := uint16(glyphID) id := uint16(glyphID)
if mapped, ok := r.fontCIDMap[fontID][id]; ok { prepared := r.preparedFonts[fontID]
if prepared == nil {
return 0, false
}
if mapped, ok := prepared.CIDs[id]; ok {
return mapped, true return mapped, true
} }
if mapped, ok := r.fontGIDMap[fontID][id]; ok { if mapped, ok := prepared.Glyphs[id]; ok {
return mapped, true return mapped, true
} }
return 0, false return 0, false
+68
View File
@@ -0,0 +1,68 @@
// 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
// TextPositioner 按OFD坐标及增量定位连续字形,不依赖字体或绘图库
type TextPositioner struct {
xs, ys, dxs, dys []float64
direction Point
current Point
previous float64
index int
}
// NewTextPositioner 创建单个TextCode的定位器,方向采用OFD角度
// 入参: code 文本定位数据, direction 阅读方向
// 返回: TextPositioner 字形定位器
func NewTextPositioner(code TextCode, direction int) TextPositioner {
result := TextPositioner{xs: parseFloats(code.X), ys: parseFloats(code.Y), dxs: parseFloats(code.DeltaX), dys: parseFloats(code.DeltaY), direction: Point{X: 1}}
switch direction {
case 90:
result.direction = Point{Y: 1}
case 180:
result.direction = Point{X: -1}
case 270:
result.direction = Point{Y: -1}
}
return result
}
// Next 返回下一个字形位置并保存其推进长度,显式坐标和增量优先
// 入参: advance 当前字形推进长度,单位为毫米
// 返回: Point 字形原点
func (p *TextPositioner) Next(advance float64) Point {
i := p.index
if i < len(p.xs) {
p.current.X = p.xs[i]
} else if i > 0 {
if dx, ok := textDelta(p.dxs, i-1); ok {
p.current.X += dx
} else if len(p.dys) == 0 {
p.current.X += p.previous * p.direction.X
}
}
if i < len(p.ys) {
p.current.Y = p.ys[i]
} else if i > 0 {
if dy, ok := textDelta(p.dys, i-1); ok {
p.current.Y += dy
} else if len(p.dxs) == 0 {
p.current.Y += p.previous * p.direction.Y
}
}
p.previous = advance
p.index++
return p.current
}