mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 16:59:27 +08:00
1 parent
77ffbe6a72
commit
c15547f0d2
27 files changed
+1162
-461
No files matched your search
@@ -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
@@ -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)
|
||||||
|
|||||||
@@ -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))
|
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
|
||||||
|
|||||||
@@ -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
|
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 检查页面尺寸并计算目标像素大小
|
||||||
|
|||||||
@@ -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可选,同步回报文档导出的已处理页数及总页数,返回错误则停止,页数完成不代表写入成功
|
// 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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 = ©
|
||||||
{-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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 无效路径错误
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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, ¤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 渲染图形对象
|
// 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
@@ -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
|
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
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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