mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 08:46:55 +08:00
1 parent
77ffbe6a72
commit
c15547f0d2
27 files changed
+1166
-465
No files matched your search
@@ -57,6 +57,13 @@ type resolvedFontResult struct {
|
||||
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 使用配置的字体后端解析资源,保留精确匹配与回退的区别
|
||||
// 入参: id 字体资源标识, exact 是否要求精确匹配
|
||||
// 返回: ResolvedFont 字体与来源, error 解析错误
|
||||
@@ -73,3 +80,49 @@ func (r *Renderer) ResolveFont(id string, exact bool) (ResolvedFont, error) {
|
||||
r.resolvedFonts[key] = resolvedFontResult{font: resolved, err: 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
@@ -23,7 +23,6 @@ import (
|
||||
_ "image/gif"
|
||||
"math"
|
||||
|
||||
canvasimage "github.com/tdewolff/canvas/image"
|
||||
_ "github.com/xiaoqidun/jbig2"
|
||||
_ "golang.org/x/image/bmp"
|
||||
"golang.org/x/image/draw"
|
||||
@@ -34,15 +33,12 @@ import (
|
||||
// 入参: data 图片数据
|
||||
// 返回: image.Image 图片对象, string 图片格式, error 错误信息
|
||||
func decodeImageData(data []byte) (image.Image, string, error) {
|
||||
if isJPEGData(data) {
|
||||
if img, err := canvasimage.NewJPEGImage(bytes.NewReader(data)); err == nil {
|
||||
return img, "jpeg", nil
|
||||
}
|
||||
}
|
||||
if isPNGData(data) {
|
||||
if img, err := canvasimage.NewPNGImage(bytes.NewReader(data)); err == nil {
|
||||
return img, "png", nil
|
||||
if isJPEGData(data) || isPNGData(data) {
|
||||
img, err := newEncodedImage(data)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return img, img.format, nil
|
||||
}
|
||||
img, format, err := image.Decode(bytes.NewReader(data))
|
||||
if err == nil {
|
||||
@@ -412,7 +408,7 @@ func imageWithTransparentEdge(img image.Image) (image.Image, int) {
|
||||
if w == 0 || h == 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
|
||||
}
|
||||
source := imagePixelSource(img)
|
||||
|
||||
@@ -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
@@ -191,21 +191,21 @@ func (r *Renderer) parseStrokeColor(strokeColor *StrokeColor) color.Color {
|
||||
return r.parseFillColor((*FillColor)(strokeColor))
|
||||
}
|
||||
|
||||
// patternPaint 底纹画刷
|
||||
type patternPaint struct {
|
||||
// PatternPaint 底纹画刷
|
||||
type PatternPaint struct {
|
||||
*Pattern
|
||||
color FillColor
|
||||
alpha *int
|
||||
Color FillColor
|
||||
Alpha *int
|
||||
}
|
||||
|
||||
// parsePatternPaint 解析底纹画刷
|
||||
// 入参: fill 填充颜色节点
|
||||
// 返回: *patternPaint 底纹画刷
|
||||
func parsePatternPaint(fill *FillColor) *patternPaint {
|
||||
// 返回: *PatternPaint 底纹画刷
|
||||
func parsePatternPaint(fill *FillColor) *PatternPaint {
|
||||
if fill.Pattern == 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 解析渐变颜色
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
// GradientStops 解析OFD渐变分段的位置与透明度,返回独立数据并保留原分段顺序
|
||||
// GradientStops 解析OFD渐变分段的位置与透明度,返回独立数据并保留原分段顺序
|
||||
// 入参: segments 渐变分段, alpha 透明度
|
||||
// 返回: []RasterStop 后端无关的渐变分段
|
||||
func (r *Renderer) GradientStops(segments []ShdSegment, alpha *int) []RasterStop {
|
||||
var gradient []RasterStop
|
||||
// 返回: []ColorStop 后端无关的渐变分段
|
||||
func (r *Renderer) GradientStops(segments []ShdSegment, alpha *int) []ColorStop {
|
||||
var gradient []ColorStop
|
||||
position, step := 0.0, 0.0
|
||||
for i, segment := range segments {
|
||||
if !segment.positionMissing {
|
||||
@@ -246,7 +245,7 @@ func (r *Renderer) GradientStops(segments []ShdSegment, alpha *int) []RasterStop
|
||||
offset := position
|
||||
position += step
|
||||
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 {
|
||||
return nil
|
||||
|
||||
@@ -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
@@ -79,18 +79,12 @@ const (
|
||||
RasterRadial
|
||||
)
|
||||
|
||||
// RasterStop 按Offset升序排列的预乘RGBA颜色节点
|
||||
type RasterStop struct {
|
||||
Offset float64
|
||||
Color color.RGBA
|
||||
}
|
||||
|
||||
// RasterGradient 局部坐标下的线性或双圆径向渐变,区间外延续边界颜色
|
||||
type RasterGradient struct {
|
||||
Kind RasterGradientKind
|
||||
Start, End RasterPoint
|
||||
R0, R1 float64
|
||||
Stops []RasterStop
|
||||
Stops []ColorStop
|
||||
}
|
||||
|
||||
// PixelSize 检查页面尺寸并计算目标像素大小
|
||||
|
||||
@@ -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})
|
||||
}
|
||||
@@ -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 = ©, 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
@@ -25,7 +25,6 @@ import (
|
||||
// OnExportProgress可选,同步回报文档导出的已处理页数及总页数,返回错误则停止,页数完成不代表写入成功
|
||||
// TransparentBackground关闭页面白底,供嵌套图案和印章保持透明背景
|
||||
type Renderer struct {
|
||||
canvasFontState
|
||||
Reader *Reader
|
||||
DPI float64
|
||||
RenderAnnotations bool
|
||||
@@ -41,7 +40,9 @@ type Renderer struct {
|
||||
pageText *PageText
|
||||
textOnly bool
|
||||
backends RenderBackends
|
||||
backendStates map[any]any
|
||||
resolvedFonts map[resolvedFontKey]resolvedFontResult
|
||||
preparedFonts map[string]*PreparedFont
|
||||
renderError error
|
||||
TransparentBackground bool
|
||||
}
|
||||
|
||||
@@ -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
|
||||
// 返回: *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 {
|
||||
objectCTM = parentCTM.Multiply(objectCTM)
|
||||
}
|
||||
p := r.buildClipPath(clips, pageH, bx, by, objectCTM)
|
||||
if p != nil && 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},
|
||||
})
|
||||
if clips.TransFlag != nil && !*clips.TransFlag {
|
||||
copy := *clips
|
||||
copy.TransFlag = nil
|
||||
clips = ©
|
||||
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
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ import (
|
||||
"github.com/tdewolff/canvas"
|
||||
)
|
||||
|
||||
// CompilePage 使用默认字体和几何解释器生成独立绘制数据
|
||||
// CompilePage 使用Canvas页面解释器和配置的字体、几何后端生成独立绘制数据
|
||||
// 入参: r 渲染器, page 页面内容
|
||||
// 返回: *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 {
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
@@ -46,8 +50,9 @@ func (CanvasBackend) CompilePage(r *Renderer, page *PageContent) (*RasterPage, e
|
||||
|
||||
// canvasPageCompiler 将已解释的页面绘制转换为自有数据,不保存第三方绘图对象
|
||||
type canvasPageCompiler struct {
|
||||
page *RasterPage
|
||||
err error
|
||||
page *RasterPage
|
||||
geometry GeometryBackend
|
||||
err error
|
||||
}
|
||||
|
||||
// Size 返回物理页面尺寸
|
||||
@@ -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)
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
return
|
||||
}
|
||||
g.Stops = make([]RasterStop, len(stops))
|
||||
g.Stops = make([]ColorStop, len(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
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ func (CanvasBackend) ResolveFont(r *Renderer, id string, exact bool) (ResolvedFo
|
||||
if source, family := r.fontSourceMatch(id, definition, exact); family != nil {
|
||||
sfnt := family.Face(12, style).Font.SFNT
|
||||
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
|
||||
}
|
||||
if !exact {
|
||||
@@ -66,11 +66,11 @@ func (CanvasBackend) ResolveFont(r *Renderer, id string, exact bool) (ResolvedFo
|
||||
// canvasFallbackFont 按需加载默认阅读字体,不用于精确编辑匹配
|
||||
// 返回: ResolvedFont 默认字体,未安装时数据为空
|
||||
func (r *Renderer) canvasFallbackFont() ResolvedFont {
|
||||
if r.fontFamily == nil {
|
||||
if r.canvasState().fontFamily == nil {
|
||||
r.initCanvasFonts()
|
||||
}
|
||||
if r.defaultFontLoaded {
|
||||
return ResolvedFont{Data: fontSFNTData(r.fontFamily.Face(12, canvas.FontRegular).Font.SFNT)}
|
||||
if r.canvasState().defaultFontLoaded {
|
||||
return ResolvedFont{Data: fontSFNTData(r.canvasState().fontFamily.Face(12, canvas.FontRegular).Font.SFNT)}
|
||||
}
|
||||
return ResolvedFont{}
|
||||
}
|
||||
@@ -97,39 +97,8 @@ func (f canvasFontMetrics) Write() []byte {
|
||||
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默认字体
|
||||
func (r *Renderer) initCanvasFonts() {
|
||||
r.fontFamily = canvas.NewFontFamily("default")
|
||||
r.defaultFontLoaded = r.loadDefaultFonts()
|
||||
r.canvasState().fontFamily = canvas.NewFontFamily("default")
|
||||
r.canvasState().defaultFontLoaded = r.loadDefaultFonts()
|
||||
}
|
||||
@@ -22,7 +22,7 @@ import "github.com/tdewolff/canvas"
|
||||
// 返回: bool 是否加载成功
|
||||
func (r *Renderer) loadDefaultFonts() bool {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,10 +64,10 @@ type fontCacheKey struct {
|
||||
// 入参: fontID 字体ID
|
||||
// 返回: *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
|
||||
}
|
||||
resolved, err := r.ResolveFont(fontID, false)
|
||||
resolved, err := r.PrepareFont(fontID)
|
||||
if err != nil {
|
||||
r.renderError = err
|
||||
return nil
|
||||
@@ -82,34 +82,12 @@ func (r *Renderer) loadFont(fontID string) *canvas.FontFamily {
|
||||
fontData := resolved.Data
|
||||
fontStyle := canvasFontStyle(of)
|
||||
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 {
|
||||
r.renderError = err
|
||||
return nil
|
||||
}
|
||||
if r.Reader.fontCache[fontID] != nil {
|
||||
r.fontMap[fontID] = ff
|
||||
r.canvasState().fontMap[fontID] = ff
|
||||
}
|
||||
return ff
|
||||
}
|
||||
@@ -132,7 +110,7 @@ func canvasFontStyle(font *Font) canvas.FontStyle {
|
||||
// 入参: fontID 字体ID, font OFD字体定义, style Canvas字体样式
|
||||
// 返回: []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
|
||||
}
|
||||
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})
|
||||
}
|
||||
}
|
||||
r.fontSourceCache[fontID] = sources
|
||||
r.canvasState().fontSourceCache[fontID] = sources
|
||||
return sources
|
||||
}
|
||||
|
||||
@@ -212,8 +190,8 @@ func appendFontSource(sources []fontSource, seen map[fontSourceKey]bool, source
|
||||
// 入参: fontID 字体ID, font OFD字体定义, exact 是否禁止无关回退
|
||||
// 返回: 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 {
|
||||
return source, r.fontMap[fontID]
|
||||
if source, ok := r.canvasState().fontSourceUsed[fontID]; ok && (!exact || source.exact) && r.canvasState().fontMap[fontID] != nil {
|
||||
return source, r.canvasState().fontMap[fontID]
|
||||
}
|
||||
style := canvasFontStyle(font)
|
||||
for _, source := range r.fontSources(fontID, font, style) {
|
||||
@@ -221,7 +199,7 @@ func (r *Renderer) fontSourceMatch(fontID string, font *Font, exact bool) (fontS
|
||||
continue
|
||||
}
|
||||
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 {
|
||||
return source, cached
|
||||
}
|
||||
@@ -240,7 +218,7 @@ func (r *Renderer) fontSourceMatch(fontID string, font *Font, exact bool) (fontS
|
||||
// 返回: *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}
|
||||
if cached, ok := r.fontCache[key]; ok {
|
||||
if cached, ok := r.canvasState().fontCache[key]; ok {
|
||||
return cached
|
||||
}
|
||||
var err error
|
||||
@@ -262,10 +240,10 @@ func (r *Renderer) loadFontSource(family *canvas.FontFamily, source fontSource,
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
r.fontCache[key] = nil
|
||||
r.canvasState().fontCache[key] = nil
|
||||
return nil
|
||||
}
|
||||
r.fontCache[key] = family
|
||||
r.canvasState().fontCache[key] = family
|
||||
return family
|
||||
}
|
||||
|
||||
@@ -273,7 +251,7 @@ func (r *Renderer) loadFontSource(family *canvas.FontFamily, source fontSource,
|
||||
// 入参: dir 字体目录, names 字体名称, bold 是否粗体, italic 是否斜体
|
||||
// 返回: []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 {
|
||||
files, _ := filepath.Glob(filepath.Join(dir, "*"))
|
||||
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))
|
||||
file.Close()
|
||||
}
|
||||
r.fontDirCandidates[dir] = candidates
|
||||
r.canvasState().fontDirCandidates[dir] = candidates
|
||||
}
|
||||
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 {
|
||||
return fonts.matchStyle(names, bold, italic)
|
||||
}
|
||||
candidates, ok := r.fontFSCandidates[index]
|
||||
candidates, ok := r.canvasState().fontFSCandidates[index]
|
||||
if !ok {
|
||||
files, _ := fs.Glob(fsys, "*")
|
||||
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))
|
||||
file.Close()
|
||||
}
|
||||
r.fontFSCandidates[index] = candidates
|
||||
r.canvasState().fontFSCandidates[index] = candidates
|
||||
}
|
||||
return fontFileMatches(candidates, names, bold, italic)
|
||||
}
|
||||
|
||||
@@ -21,6 +21,56 @@ import (
|
||||
"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 解析对象的标准紧缩路径并转换到页面坐标
|
||||
// 入参: object 路径对象
|
||||
// 返回: GeometryPath 页面路径, error 无效路径错误
|
||||
|
||||
@@ -92,11 +92,11 @@ func (r *Renderer) cachedTextGlyphPath(face *canvas.FontFace, glyph textGlyph) (
|
||||
direction: face.Direction,
|
||||
glyph: glyph,
|
||||
}
|
||||
if cached, ok := r.textGlyphPathCache[key]; ok {
|
||||
if cached, ok := r.canvasState().textGlyphPathCache[key]; ok {
|
||||
return cached.path, cached.width
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -15,10 +15,9 @@
|
||||
package ofdgo
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"image"
|
||||
"io"
|
||||
"math"
|
||||
|
||||
"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[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 {
|
||||
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 渲染图像边框
|
||||
// 入参: 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) {
|
||||
@@ -166,7 +188,7 @@ func (r *Renderer) imageWithClip(img image.Image, clipPath *canvas.Path, m canva
|
||||
bounds, w, h = resized.Bounds(), nw, nh
|
||||
}
|
||||
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.SetFillColor(canvas.White)
|
||||
ctx.SetStrokeColor(canvas.Transparent)
|
||||
@@ -216,61 +238,3 @@ func (r *Renderer) imageWithClip(img image.Image, clipPath *canvas.Path, m canva
|
||||
}
|
||||
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
|
||||
}
|
||||
+85
-143
@@ -16,159 +16,101 @@ package ofdgo
|
||||
|
||||
import "github.com/tdewolff/canvas"
|
||||
|
||||
// renderAnnotations 渲染页面注释外观
|
||||
// 入参: ctx 画布上下文, pageID 页面ID, pageH 页面高度
|
||||
func (r *Renderer) renderAnnotations(ctx *canvas.Context, pageID string, pageH float64) {
|
||||
for _, annot := range r.Reader.Annots[pageID] {
|
||||
if annot.Visible != nil && !*annot.Visible {
|
||||
continue
|
||||
}
|
||||
groups, grouped := ctx.Renderer.(interface {
|
||||
beginAnnotation(string) bool
|
||||
endObject()
|
||||
})
|
||||
grouped = grouped && groups.beginAnnotation(annot.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()
|
||||
}
|
||||
}
|
||||
// canvasPageVisitor 将公共页面遍历结果交给Canvas解释器
|
||||
type canvasPageVisitor struct {
|
||||
renderer *Renderer
|
||||
context *canvas.Context
|
||||
height float64
|
||||
}
|
||||
|
||||
// renderTemplate 渲染模板
|
||||
// 入参: ctx 画布上下文, templateID 模板ID, pageH 页面高度
|
||||
func (r *Renderer) renderTemplate(ctx *canvas.Context, templateID string, pageH float64) {
|
||||
tplContent := r.loadTemplate(templateID)
|
||||
if tplContent == nil {
|
||||
return
|
||||
// DrawObject 绘制叶子对象,转换页面坐标裁剪
|
||||
// 入参: object 源对象, state 继承状态
|
||||
// 返回: error 绘制错误
|
||||
func (v *canvasPageVisitor) DrawObject(object *GraphicObject, state RenderState) error {
|
||||
clip, err := geometryToCanvasPath(state.Clip)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for order := range 3 {
|
||||
r.renderLayers(ctx, tplContent.Content.Layer, pageH, order)
|
||||
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
|
||||
}
|
||||
|
||||
// renderLayers 按原顺序渲染指定类型的图层
|
||||
// 入参: ctx 画布上下文, layers 图层列表, pageH 页面高度, order 图层类型顺序
|
||||
func (r *Renderer) renderLayers(ctx *canvas.Context, layers []Layer, pageH float64, order int) {
|
||||
for _, layer := range layers {
|
||||
if layerOrder(layer.Type) == order {
|
||||
r.renderLayer(ctx, layer, pageH, nil, nil)
|
||||
}
|
||||
}
|
||||
// DrawStamp 绘制签章外观
|
||||
// 入参: stamp 签章
|
||||
// 返回: error 绘制错误
|
||||
func (v *canvasPageVisitor) DrawStamp(stamp Stamp) error {
|
||||
v.renderer.renderStamp(v.context, stamp, v.height)
|
||||
return v.renderer.renderError
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
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, ¤tCTM, true, clipPath)
|
||||
}
|
||||
}
|
||||
if len(cgu.Objects) > 0 {
|
||||
for _, obj := range cgu.Objects {
|
||||
obj = mergeGraphicObjectAlpha(obj, cgu.Alpha)
|
||||
r.renderObject(ctx, &obj, pageH, defaults, ¤tCTM, boundaryInCTM, clipPath)
|
||||
}
|
||||
ctx.Pop()
|
||||
return
|
||||
}
|
||||
for _, imgObj := range cgu.ImageObject {
|
||||
imgObj.Alpha = mergeAlpha(imgObj.Alpha, cgu.Alpha)
|
||||
r.renderImage(ctx, imgObj, pageH, ¤tCTM, boundaryInCTM, clipPath)
|
||||
}
|
||||
for _, pathObj := range cgu.PathObject {
|
||||
pathObj.Alpha = mergeAlpha(pathObj.Alpha, cgu.Alpha)
|
||||
r.renderPath(ctx, pathObj, pageH, defaults, ¤tCTM, boundaryInCTM, clipPath)
|
||||
}
|
||||
for _, textObj := range cgu.TextObject {
|
||||
textObj.Alpha = mergeAlpha(textObj.Alpha, cgu.Alpha)
|
||||
r.renderText(ctx, textObj, pageH, defaults, ¤tCTM, boundaryInCTM, clipPath)
|
||||
}
|
||||
for _, subCgu := range cgu.CompositeGraphicUnit {
|
||||
subCgu.Alpha = mergeAlpha(subCgu.Alpha, cgu.Alpha)
|
||||
r.renderCompositeGraphicUnit(ctx, subCgu, pageH, defaults, ¤tCTM, boundaryInCTM, clipPath)
|
||||
}
|
||||
ctx.Pop()
|
||||
}
|
||||
|
||||
// renderObject 渲染图形对象
|
||||
// 入参: ctx 画布上下文, obj 图形对象, pageH 页面高度, defaults 默认绘制参数, parentCTM 父级CTM, boundaryInCTM 边界是否参与CTM变换, parentClip 父级裁剪路径
|
||||
func (r *Renderer) renderObject(ctx *canvas.Context, obj *GraphicObject, pageH float64, defaults *DrawParam, parentCTM *Matrix, boundaryInCTM bool, parentClip *canvas.Path) {
|
||||
if groups, ok := ctx.Renderer.(interface {
|
||||
// BeginObject 开始可编辑对象分组
|
||||
// 入参: object 源对象
|
||||
// 返回: bool 是否建立分组
|
||||
func (v *canvasPageVisitor) BeginObject(object *GraphicObject) bool {
|
||||
groups, ok := v.context.Renderer.(interface {
|
||||
beginObject(*GraphicObject) bool
|
||||
endObject()
|
||||
}); ok && groups.beginObject(obj) {
|
||||
defer groups.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
|
||||
endObject()
|
||||
})
|
||||
return ok && groups.beginAnnotation(id)
|
||||
}
|
||||
|
||||
// EndObject 结束对象或注解分组
|
||||
func (v *canvasPageVisitor) EndObject() {
|
||||
v.context.Renderer.(interface{ endObject() }).endObject()
|
||||
}
|
||||
|
||||
// SkipObjects 保留不可见对象的编号
|
||||
// 入参: count 对象数量
|
||||
func (v *canvasPageVisitor) SkipObjects(count int) {
|
||||
if groups, ok := v.context.Renderer.(interface{ skipObjects(int) }); ok {
|
||||
groups.skipObjects(count)
|
||||
}
|
||||
switch obj.Type {
|
||||
case "TextObject":
|
||||
r.renderText(ctx, obj.TextObject, pageH, defaults, parentCTM, boundaryInCTM, parentClip)
|
||||
case "PathObject":
|
||||
r.renderPath(ctx, obj.PathObject, pageH, defaults, parentCTM, boundaryInCTM, parentClip)
|
||||
case "ImageObject":
|
||||
r.renderImage(ctx, obj.ImageObject, pageH, parentCTM, boundaryInCTM, parentClip)
|
||||
case "CompositeGraphicUnit", "CompositeObject":
|
||||
r.renderCompositeGraphicUnit(ctx, obj.CompositeGraphicUnit, pageH, defaults, parentCTM, boundaryInCTM, parentClip)
|
||||
}
|
||||
|
||||
// canvasRenderState 将Canvas裁剪转换为公共页面状态
|
||||
// 入参: pageH 页面高度, defaults 默认样式, parentCTM 父变换, boundaryInCTM 边界是否参与变换, parentClip 父裁剪
|
||||
// 返回: RenderState 公共页面状态
|
||||
func canvasRenderState(pageH float64, defaults *DrawParam, parentCTM *Matrix, boundaryInCTM bool, parentClip *canvas.Path) RenderState {
|
||||
var clip *GeometryPath
|
||||
if parentClip != nil {
|
||||
clip = geometryFromCanvasPath(parentClip.Copy().Translate(0, -pageH))
|
||||
}
|
||||
return RenderState{Defaults: defaults, Parent: parentCTM, BoundaryInCTM: boundaryInCTM, Clip: clip}
|
||||
}
|
||||
|
||||
// renderCompositeGraphicUnit 通过公共遍历绘制复合图元
|
||||
// 入参: ctx 画布上下文, cgu 复合图元, pageH 页面高度, defaults 默认样式, parentCTM 父变换, boundaryInCTM 边界是否参与变换, parentClip 父裁剪
|
||||
func (r *Renderer) renderCompositeGraphicUnit(ctx *canvas.Context, cgu CompositeGraphicUnit, pageH float64, defaults *DrawParam, parentCTM *Matrix, boundaryInCTM bool, parentClip *canvas.Path) {
|
||||
if err := r.walkComposite(cgu, canvasRenderState(pageH, defaults, parentCTM, boundaryInCTM, parentClip), &canvasPageVisitor{r, ctx, pageH}, nil); err != nil {
|
||||
r.renderError = err
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
@@ -63,27 +63,8 @@ func (r *Renderer) renderCanvasPageToContext(ctx *canvas.Context, page *PageCont
|
||||
ctx.SetFillColor(canvas.White)
|
||||
ctx.DrawPath(0, 0, canvas.Rectangle(box.W, box.H))
|
||||
}
|
||||
for order := range 3 {
|
||||
if r.Reader.doc != nil {
|
||||
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 err := r.WalkPage(page, &canvasPageVisitor{r, ctx, pageH}); err != nil {
|
||||
return err
|
||||
}
|
||||
if r.renderError != nil {
|
||||
return r.renderError
|
||||
|
||||
@@ -15,27 +15,24 @@
|
||||
package ofdgo
|
||||
|
||||
import (
|
||||
"github.com/tdewolff/canvas"
|
||||
"math"
|
||||
|
||||
"github.com/tdewolff/canvas"
|
||||
)
|
||||
|
||||
// parseFillPaint 解析填充画刷
|
||||
// parseFillPaint 将公共画刷转换为Canvas对象
|
||||
// 入参: fillColor 填充颜色节点, x X坐标, y Y坐标, pageH 页面高度
|
||||
// 返回: 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
|
||||
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 解析勾边画刷
|
||||
@@ -45,39 +42,34 @@ func (r *Renderer) parseStrokePaint(strokeColor *StrokeColor, x, y, pageH float6
|
||||
return r.parseFillPaint((*FillColor)(strokeColor), x, y, pageH)
|
||||
}
|
||||
|
||||
// parseShdSegments 将通用渐变分段转换为Canvas画刷
|
||||
// 入参: segments 渐变分段, alpha 透明度
|
||||
// canvasGradient 将公共颜色节点转换为Canvas渐变
|
||||
// 入参: stops 渐变分段
|
||||
// 返回: canvas.Grad 渐变分段
|
||||
func (r *Renderer) parseShdSegments(segments []ShdSegment, alpha *int) canvas.Grad {
|
||||
func canvasGradient(stops []ColorStop) canvas.Grad {
|
||||
var gradient canvas.Grad
|
||||
for _, stop := range r.GradientStops(segments, alpha) {
|
||||
for _, stop := range stops {
|
||||
gradient.Add(stop.Offset, stop.Color)
|
||||
}
|
||||
return gradient
|
||||
}
|
||||
|
||||
// parseAxialShdGradient 解析轴向渐变
|
||||
// 入参: axialShd 轴向渐变节点, alpha 透明度, x X坐标, y Y坐标, pageH 页面高度
|
||||
// 返回: canvas.Gradient 渐变对象
|
||||
func (r *Renderer) parseAxialShdGradient(axialShd *AxialShd, alpha *int, x, y, pageH float64) canvas.Gradient {
|
||||
if axialShd == nil {
|
||||
return nil
|
||||
// canvasShading 转换坐标与椭圆渐变变换,不重新解析OFD样式
|
||||
// 入参: paint 公共渐变画刷, x X坐标, y Y坐标, pageH 页面高度
|
||||
// 返回: *shdPaint Canvas渐变画刷
|
||||
func canvasShading(paint Paint, x, y, pageH float64) *shdPaint {
|
||||
source := paint.Gradient
|
||||
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)
|
||||
end := parseFloats(axialShd.EndPoint)
|
||||
if len(start) < 2 || len(end) < 2 {
|
||||
return nil
|
||||
result := newShdPaint(gradient.ToRadial(start, source.StartRadius, end, source.EndRadius), source.Extend, source.MapType, source.MapUnit)
|
||||
if e := source.Eccentricity; 0 < e && e < 1 {
|
||||
result.view = canvas.Identity.Translate(start.X, start.Y).Rotate(-source.Angle).Scale(1, math.Sqrt(1-e*e)).Translate(-start.X, -start.Y)
|
||||
result.gradient = gradient.ToRadial(start, source.StartRadius, result.view.Inv().Dot(end), source.EndRadius)
|
||||
}
|
||||
gradient := r.parseShdSegments(axialShd.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])}
|
||||
if startPoint.Equals(endPoint) {
|
||||
return nil
|
||||
}
|
||||
return gradient.ToLinear(startPoint, endPoint)
|
||||
return result
|
||||
}
|
||||
|
||||
// axialShdClip 获取轴向渐变的延伸裁剪区域
|
||||
@@ -101,29 +93,3 @@ func axialShdClip(gradient *canvas.LinearGradient, extend int, bounds canvas.Rec
|
||||
}
|
||||
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
|
||||
}
|
||||
@@ -29,8 +29,8 @@ type pathStyle struct {
|
||||
strokeColor color.Color
|
||||
fillPaint any
|
||||
strokePaint any
|
||||
fillPattern *patternPaint
|
||||
strokePattern *patternPaint
|
||||
fillPattern *PatternPaint
|
||||
strokePattern *PatternPaint
|
||||
lineWidth float64
|
||||
lineCap canvas.Capper
|
||||
lineJoin canvas.Joiner
|
||||
@@ -280,9 +280,10 @@ func (r *Renderer) renderPath(ctx *canvas.Context, obj PathObject, pageH float64
|
||||
}
|
||||
strokeClip := intersectClipPath(clipPath, shadingClip)
|
||||
_, 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.Stroke(style.lineWidth, style.lineCap, style.lineJoin, canvas.Tolerance)
|
||||
sp = r.strokeCanvasPath(sp, style.lineWidth, style.lineCap, style.lineJoin)
|
||||
sp = applyClipPath(sp, strokeClip)
|
||||
if style.strokePattern != nil {
|
||||
r.renderPattern(ctx, style.strokePattern, pageH, sp, objectCTM)
|
||||
@@ -300,9 +301,9 @@ func (r *Renderer) renderPath(ctx *canvas.Context, obj PathObject, pageH float64
|
||||
|
||||
// renderPattern 渲染图案填充
|
||||
// 入参: 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 pattern.alpha == nil || *pattern.alpha > 0 {
|
||||
if pattern.Alpha == nil || *pattern.Alpha > 0 {
|
||||
bounds.add(clip)
|
||||
}
|
||||
return
|
||||
@@ -358,7 +359,7 @@ func (r *Renderer) renderPattern(ctx *canvas.Context, pattern *patternPaint, pag
|
||||
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)}
|
||||
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))
|
||||
@@ -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})
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,6 +120,6 @@ func (r *Renderer) renderStampImage(ctx *canvas.Context, img image.Image, s Stam
|
||||
ctx.Push()
|
||||
ctx.Translate(box.X, pageH-(box.Y+box.H))
|
||||
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()
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -186,7 +186,7 @@ func (s *svgResourceRenderer) RenderText(text *canvas.Text, m canvas.Matrix) {
|
||||
return
|
||||
}
|
||||
font := text.MostCommonFontFace().Font
|
||||
resource, ok := s.renderer.svgFontCache[font]
|
||||
resource, ok := s.renderer.canvasState().svgFontCache[font]
|
||||
if !ok {
|
||||
data := fontSFNTData(font.SFNT)
|
||||
resource = SVGFont{
|
||||
@@ -198,7 +198,7 @@ func (s *svgResourceRenderer) RenderText(text *canvas.Text, m canvas.Matrix) {
|
||||
if font.Style().Italic() {
|
||||
resource.Style = "italic"
|
||||
}
|
||||
s.renderer.svgFontCache[font] = resource
|
||||
s.renderer.canvasState().svgFontCache[font] = resource
|
||||
}
|
||||
if !s.seen[resource.Name] {
|
||||
s.seen[resource.Name] = true
|
||||
|
||||
@@ -76,7 +76,7 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64
|
||||
sizePt := sizeMM * ptPerMM
|
||||
fillColor := colorWithAlpha(canvas.Black, obj.Alpha)
|
||||
var fillPaint any = fillColor
|
||||
var fillPattern *patternPaint
|
||||
var fillPattern *PatternPaint
|
||||
var fillColorNode *FillColor
|
||||
if defaults != nil {
|
||||
fillColorNode = defaults.FillColor
|
||||
@@ -187,15 +187,6 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64
|
||||
hasUnderline := strings.Contains(obj.Decoration, "Underline")
|
||||
_, shadedFill := fillPaint.(canvas.Gradient)
|
||||
verticalAdvance := (obj.ReadDirection-obj.CharDirection)%180 != 0
|
||||
advanceX, advanceY := 1.0, 0.0
|
||||
switch obj.ReadDirection {
|
||||
case 90:
|
||||
advanceX, advanceY = 0, 1
|
||||
case 180:
|
||||
advanceX = -1
|
||||
case 270:
|
||||
advanceX, advanceY = 0, -1
|
||||
}
|
||||
codePos := 0
|
||||
textPos := 0
|
||||
for 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 == "" {
|
||||
spans = textGlyphSpans(runes, glyphTransforms, codePos)
|
||||
}
|
||||
dxs, dys := parseFloats(tc.DeltaX), parseFloats(tc.DeltaY)
|
||||
xs, ys := parseFloats(tc.X), parseFloats(tc.Y)
|
||||
positioner := NewTextPositioner(tc, obj.ReadDirection)
|
||||
dxs, dys, xs, ys := positioner.dxs, positioner.dys, positioner.xs, positioner.ys
|
||||
drawAsPath := embeddedFont || face.FauxBold > 0 || textCodePositioned(tc, xs, ys) || fillClip != nil || shadedFill || fillPattern != nil || shouldStroke
|
||||
cx, cy := 0.0, 0.0
|
||||
previousAdvance := 0.0
|
||||
if len(xs) > 0 {
|
||||
cx = xs[0]
|
||||
}
|
||||
if len(ys) > 0 {
|
||||
cy = ys[0]
|
||||
}
|
||||
for i, glyph := range glyphs {
|
||||
str := glyph.Text
|
||||
drawAsGlyphPath := drawAsPath || glyph.GlyphID >= 0
|
||||
@@ -244,28 +227,12 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64
|
||||
} else {
|
||||
glyphWidth = textGlyphWidth(face, glyph)
|
||||
}
|
||||
if i < len(xs) {
|
||||
cx = xs[i]
|
||||
} else if i > 0 {
|
||||
if dx, ok := textDelta(dxs, i-1); ok {
|
||||
cx += dx
|
||||
} else if len(dys) == 0 {
|
||||
cx += previousAdvance * advanceX
|
||||
}
|
||||
}
|
||||
if i < len(ys) {
|
||||
cy = ys[i]
|
||||
} else if i > 0 {
|
||||
if dy, ok := textDelta(dys, i-1); ok {
|
||||
cy += dy
|
||||
} else if len(dxs) == 0 {
|
||||
cy += previousAdvance * advanceY
|
||||
}
|
||||
}
|
||||
previousAdvance = glyphWidth * hScale
|
||||
advance := glyphWidth * hScale
|
||||
if verticalAdvance {
|
||||
previousAdvance = sizeMM
|
||||
advance = sizeMM
|
||||
}
|
||||
position := positioner.Next(advance)
|
||||
cx, cy := position.X, position.Y
|
||||
var canvasX, canvasY float64
|
||||
if boundaryInCTM && parentCTM != nil {
|
||||
tx, ty := localCTM.Transform(cx, cy)
|
||||
@@ -328,7 +295,7 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64
|
||||
if len(strokeStyle.dashPattern) > 0 {
|
||||
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)
|
||||
if strokeStyle.strokePattern != nil {
|
||||
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.MoveTo(0, -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 {
|
||||
r.renderPattern(ctx, fillPattern, pageH, applyClipPath(underline.Transform(transform), clipPath), objectCTM)
|
||||
} else if shouldFill {
|
||||
@@ -380,7 +347,7 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64
|
||||
underline := &canvas.Path{}
|
||||
underline.MoveTo(0, -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)
|
||||
if fillPattern != nil {
|
||||
r.renderPattern(ctx, fillPattern, pageH, underline, objectCTM)
|
||||
|
||||
+6
-2
@@ -185,10 +185,14 @@ func (r *Renderer) fontGlyphRune(fontID string, glyphID int) (rune, bool) {
|
||||
return 0, false
|
||||
}
|
||||
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
|
||||
}
|
||||
if mapped, ok := r.fontGIDMap[fontID][id]; ok {
|
||||
if mapped, ok := prepared.Glyphs[id]; ok {
|
||||
return mapped, true
|
||||
}
|
||||
return 0, false
|
||||
|
||||
@@ -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
|
||||
}
|
||||
Reference in new issue
Block a user