feat(后端架构): 完善GG后端,修复渐变与选区异常
build.yaml / build (push) Successful in 37s

This commit is contained in:
xiaoqidun committed 2026-09-23 18:43:53 +08:00
1 parent c15547f0d2
commit f44d0d8a60
30 files changed
+2075 -463

No files matched your search

+1 -1
View File
@@ -3134,7 +3134,7 @@ function displayMode() {
}
function renderBackendLabel(name) {
return { svg: "SVG", canvas: "Canvas", gg: "GoGPU" }[name] || name || "无";
return { svg: "SVG", canvas: "Canvas", gg: "GoGPU", "gg+canvas": "GoGPU + Canvas" }[name] || name || "无";
}
function updateRenderBackend() {
+1
View File
@@ -74,6 +74,7 @@ func NewRenderer(reader *Reader, opts ...RendererOption) *Renderer {
templatePageCache: make(map[string]*PageContent),
imageCache: make(map[string]image.Image),
backends: defaultRenderBackends(),
backendStates: make(map[any]any),
}
r.resetFontCache()
for _, opt := range opts {
+20 -2
View File
@@ -19,6 +19,7 @@ import (
"fmt"
"image"
"io"
"reflect"
)
// ErrBackendUnavailable 表示未配置所需后端,不会自动切换其他实现
@@ -124,7 +125,7 @@ type BackendInfo struct {
EPS string `json:"eps"`
}
// NewRenderBackends 创建内置后端组合,gg仅替换光栅绘制,其他能力通过Info明确报告
// NewRenderBackends 创建内置后端组合,各项能力通过Info报告实际提供者
// 入参: name canvas或gg
// 返回: RenderBackends 后端组合, error 未知后端错误
func NewRenderBackends(name string) (RenderBackends, error) {
@@ -132,6 +133,9 @@ func NewRenderBackends(name string) (RenderBackends, error) {
switch name {
case "canvas":
case "gg":
backends.Fonts = GGBackend{}
backends.Geometry = GGGeometryBackend{GeometryBackend: backends.Geometry}
backends.Compiler = GGBackend{}
backends.Raster = GGBackend{}
default:
return RenderBackends{}, fmt.Errorf("unknown render backend %q", name)
@@ -178,11 +182,25 @@ func backendName(backend Backend) string {
// 返回: RendererOption 渲染选项
func WithRenderBackends(backends RenderBackends) RendererOption {
return func(r *Renderer) {
fontsChanged := !sameBackend(r.backends.Fonts, backends.Fonts)
r.backends = backends
r.resetFontCache()
if fontsChanged {
r.resetFontBackendCache()
}
}
}
// sameBackend 判断可比较值配置是否未变,指针配置重新应用时刷新可变状态
// 入参: left 原后端, right 新后端
// 返回: bool 是否为同一配置
func sameBackend(left, right Backend) bool {
if left == nil || right == nil {
return left == nil && right == nil
}
a, b := reflect.ValueOf(left), reflect.ValueOf(right)
return a.Kind() != reflect.Pointer && b.Kind() != reflect.Pointer && a.Comparable() && b.Comparable() && left == right
}
// Backends 返回当前后端配置副本,可修改后通过WithRenderBackends应用
// 返回: RenderBackends 后端组合
func (r *Renderer) Backends() RenderBackends {
+8 -3
View File
@@ -81,12 +81,17 @@ func (e *Editor) SetPageCompiler(compiler PageCompiler) {
}
}
// SetRenderBackends 设置完整后端组合并重置字体度量,不改变文档或撤销历史
// SetRenderBackends 设置完整后端组合,字体能力变化时更新度量,不改变文档或撤销历史
// 入参: backends 后端组合,未配置的能力不会沿用默认实现
func (e *Editor) SetRenderBackends(backends RenderBackends) {
fontsChanged := !sameBackend(e.backends.Fonts, backends.Fonts)
e.backends = backends
e.fontRenderer = nil
e.fontMetrics = make(map[string]FontMetrics)
if e.fontRenderer != nil {
WithRenderBackends(backends)(e.fontRenderer)
}
if fontsChanged {
e.fontMetrics = make(map[string]FontMetrics)
}
}
// Backends 返回编辑器后端配置副本
+26 -5
View File
@@ -28,6 +28,13 @@ type FontMetrics interface {
Write() []byte
}
// FontOutlines 提供基线原点、纵轴向下的字形轮廓,不绑定字体实现
// GlyphOutline返回只读路径,字号与路径单位为毫米
type FontOutlines interface {
FontMetrics
GlyphOutline(glyph uint16, size float64) (GeometryPath, error)
}
// ResolvedFont 保存字体来源和只读字体数据,不将外部字体写入文档
type ResolvedFont struct {
Data []byte
@@ -57,11 +64,27 @@ type resolvedFontResult struct {
err error
}
// resetFontCache 重置字体解析及后端私有缓存,不预先创建任何绘图库对象
// resetFontCache 重置字体解析及字形缓存,保留图片和非字体后端状态
func (r *Renderer) resetFontCache() {
r.fontSourcesCache = newFontSourceCache()
r.resetFontBackendCache()
}
// resetFontBackendCache 清理后端字体对象,保留字体文件和图片缓存
func (r *Renderer) resetFontBackendCache() {
r.preparedFonts = make(map[string]*PreparedFont)
r.resolvedFonts = make(map[resolvedFontKey]resolvedFontResult)
r.backendStates = make(map[any]any)
if r.fontSourcesCache != nil {
r.fontSourcesCache.fallback = ResolvedFont{}
r.fontSourcesCache.fallbackRead = false
}
r.fontMetrics = make(map[*PreparedFont]FontMetrics)
r.glyphOutlines = make(map[glyphOutlineKey]GeometryPath)
for _, state := range r.backendStates {
if fonts, ok := state.(interface{ resetFonts() }); ok {
fonts.resetFonts()
}
}
}
// ResolveFont 使用配置的字体后端解析资源,保留精确匹配与回退的区别
@@ -121,8 +144,6 @@ func (r *Renderer) PrepareFont(id string) (*PreparedFont, error) {
}
}
}
if definition != nil {
r.preparedFonts[id] = prepared
}
r.preparedFonts[id] = prepared
return prepared, nil
}
+344
View File
@@ -0,0 +1,344 @@
// 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"
"io"
"io/fs"
"os"
"path"
"path/filepath"
"runtime"
"sort"
"sync"
"github.com/tdewolff/font"
)
// fontSourceKind 字体来源类型
type fontSourceKind uint8
const (
fontSourceFile fontSourceKind = iota
fontSourceFS
fontSourceSystem
)
// fontSource 字体来源
type fontSource struct {
kind fontSourceKind
index int
face int
name string
exact bool
priority int
}
// fontSourceKey 字体来源去重键
type fontSourceKey struct {
kind fontSourceKind
index int
face int
name string
}
// fontSourceCache 保存字体文件候选与原始数据,不包含绘图库对象
type fontSourceCache struct {
candidates map[string][]fontSource
directories map[string][]fontFileCandidate
filesystems map[int][]fontFileCandidate
data map[fontSourceKey]fontSourceData
fallback ResolvedFont
fallbackRead bool
}
// fontSourceData 保存读取结果,避免重复打开同一字体文件
type fontSourceData struct {
data []byte
err error
}
// newFontSourceCache 创建独立字体来源缓存
// 返回: *fontSourceCache 字体来源缓存
func newFontSourceCache() *fontSourceCache {
return &fontSourceCache{candidates: make(map[string][]fontSource), directories: make(map[string][]fontFileCandidate), filesystems: make(map[int][]fontFileCandidate), data: make(map[fontSourceKey]fontSourceData)}
}
// systemFontIndex 串行初始化系统字体索引,不注册绘图后端
var systemFontIndex struct {
sync.Mutex
fonts *font.SystemFonts
}
// readFontSource 读取并拆出独立字体,保留集合中的名称和样式选择
// 入参: source 字体来源, definition 字体定义
// 返回: []byte 字体数据, error 读取或集合错误
func (r *Renderer) readFontSource(source fontSource, definition *Font) ([]byte, error) {
key := fontSourceKey{kind: source.kind, index: source.index, face: source.face, name: source.name}
if source.kind == fontSourceSystem {
systemFontIndex.Lock()
if systemFontIndex.fonts == nil {
systemFontIndex.fonts, _ = font.FindSystemFonts(font.DefaultFontDirs())
}
weight := 400
if definition.Bold {
weight = 700
}
match, ok := systemFontIndex.fonts.Match(source.name, font.ParseStyleCSS(weight, definition.Italic))
systemFontIndex.Unlock()
if !ok {
return nil, fmt.Errorf("font %q not found", source.name)
}
key = fontSourceKey{kind: fontSourceFile, name: match.Filename}
}
if cached, ok := r.fontSourcesCache.data[key]; ok {
return cached.data, cached.err
}
var data []byte
var err error
if key.kind == fontSourceFS {
data, err = fs.ReadFile(r.fontFS[key.index], key.name)
} else {
data, err = os.ReadFile(key.name)
}
if err == nil && bytes.HasPrefix(data, []byte("ttcf")) {
data, err = extractCollectionFont(data, key.face)
}
r.fontSourcesCache.data[key] = fontSourceData{data, err}
return data, err
}
// fontSourceMatch 匹配可被当前字体后端解析的来源
// 入参: backend 字体后端, fontID 字体ID, definition 字体定义, exact 是否禁止无关回退
// 返回: fontSource 字体来源, FontMetrics 独立字体度量
func (r *Renderer) fontSourceMatch(backend FontBackend, fontID string, definition *Font, exact bool) (fontSource, FontMetrics) {
for _, source := range r.fontSources(fontID, definition) {
if exact && !source.exact {
continue
}
data, err := r.readFontSource(source, definition)
if err != nil {
continue
}
if metrics, err := backend.OpenFont(data); err == nil {
return source, metrics
}
}
return fontSource{}, nil
}
// resolveFontSource 按公共匹配规则读取字体,由指定后端验证外部字体
// 入参: backend 字体后端, id 字体ID, exact 是否禁止无关回退
// 返回: ResolvedFont 字体及来源, error 解析错误
func (r *Renderer) resolveFontSource(backend FontBackend, id string, exact bool) (ResolvedFont, error) {
definition := r.Reader.fontCache[id]
if definition == nil && !r.Reader.fontResourcesRead && r.Reader.OFD != nil {
if _, err := r.Reader.Fonts(); err != nil {
return ResolvedFont{}, err
}
definition = r.Reader.fontCache[id]
}
if definition != nil {
if definition.FontFile != "" {
data, err := r.Reader.FontData(id)
if err != nil {
return ResolvedFont{}, err
}
face, err := r.Reader.embeddedFontFace(*definition)
if err != nil {
return ResolvedFont{}, err
}
return ResolvedFont{Data: data, Source: path.Base(definition.FontFile), Face: face, Exact: true}, nil
}
if source, metrics := r.fontSourceMatch(backend, id, definition, exact); metrics != nil {
data := metrics.Write()
faces, err := (FontFile{Data: data}).Faces()
if err != nil {
return ResolvedFont{}, err
}
face := faces[0]
face.Index = source.face
return ResolvedFont{Data: data, Source: source.name, Face: &face, Exact: source.exact}, nil
}
}
if exact {
return ResolvedFont{}, fmt.Errorf("font %q is unavailable", id)
}
if !r.fontSourcesCache.fallbackRead {
r.fontSourcesCache.fallbackRead = true
if canLoadSystemFonts() {
for _, name := range fontDefaultSystemNames() {
data, err := r.readFontSource(fontSource{kind: fontSourceSystem, name: name}, &Font{})
if err != nil {
continue
}
metrics, err := backend.OpenFont(data)
if err == nil {
r.fontSourcesCache.fallback = ResolvedFont{Data: metrics.Write()}
break
}
}
}
}
return r.fontSourcesCache.fallback, nil
}
// fontSources 获取字体来源列表
// 入参: fontID 字体ID, font OFD字体定义
// 返回: []fontSource 字体来源列表
func (r *Renderer) fontSources(fontID string, font *Font) []fontSource {
if sources, ok := r.fontSourcesCache.candidates[fontID]; ok {
return sources
}
bold, italic := font.Bold, font.Italic
names := []string{font.FontName, font.FamilyName}
sources := make([]fontSource, 0)
seen := make(map[fontSourceKey]bool)
for _, dir := range r.fontDirs {
for _, match := range r.matchFontFiles(dir, names, bold, italic) {
sources = appendFontSource(sources, seen, fontSource{kind: fontSourceFile, name: match.name, face: match.face, exact: true, priority: min(match.priority, 0)})
}
}
for index := range r.fontFS {
for _, match := range r.matchFontFS(index, names, bold, italic) {
sources = appendFontSource(sources, seen, fontSource{kind: fontSourceFS, index: index, name: match.name, face: match.face, exact: true, priority: min(match.priority, 0)})
}
}
sortFontSources(sources)
systemStart := len(sources)
if canLoadSystemFonts() {
for _, dir := range systemFontDirs() {
for _, match := range r.matchFontFiles(dir, names, bold, italic) {
sources = appendFontSource(sources, seen, fontSource{kind: fontSourceFile, name: match.name, face: match.face, exact: true, priority: min(match.priority, 0)})
}
}
for _, name := range fontSystemNames(names...) {
source := fontSource{kind: fontSourceSystem, name: name, exact: true}
for i, requested := range names {
if fontNormalizeName(name) == fontNormalizeName(requested) {
source.priority = (i - len(names)) * 2
break
}
}
sources = appendFontSource(sources, seen, source)
}
}
sortFontSources(sources[systemStart:])
for index := range r.fontFS {
for _, match := range r.matchFontFS(index, nil, bold, italic) {
sources = appendFontSource(sources, seen, fontSource{kind: fontSourceFS, index: index, name: match.name, face: match.face})
}
}
for index, fsys := range r.fontFS {
names, _ := fs.Glob(fsys, "*")
for _, name := range names {
sources = appendFontSource(sources, seen, fontSource{kind: fontSourceFS, index: index, name: name})
}
}
r.fontSourcesCache.candidates[fontID] = sources
return sources
}
// sortFontSources 将完整名称匹配排在回退候选之前
// 入参: sources 字体来源列表
func sortFontSources(sources []fontSource) {
sort.SliceStable(sources, func(i, j int) bool {
return sources[i].priority < sources[j].priority
})
}
// appendFontSource 追加字体来源
// 入参: sources 字体来源列表, seen 去重映射, source 字体来源
// 返回: []fontSource 字体来源列表
func appendFontSource(sources []fontSource, seen map[fontSourceKey]bool, source fontSource) []fontSource {
if source.name == "" {
return sources
}
key := fontSourceKey{kind: source.kind, index: source.index, name: source.name, face: source.face}
if seen[key] {
return sources
}
seen[key] = true
return append(sources, source)
}
// matchFontFiles 查找字体文件
// 入参: dir 字体目录, names 字体名称, bold 是否粗体, italic 是否斜体
// 返回: []fontFileMatch 字体匹配列表
func (r *Renderer) matchFontFiles(dir string, names []string, bold, italic bool) []fontFileMatch {
candidates, ok := r.fontSourcesCache.directories[dir]
if !ok {
files, _ := filepath.Glob(filepath.Join(dir, "*"))
candidates = fontFileCandidates(files, filepath.Base)
for i, candidate := range candidates {
file, err := os.Open(candidate.name)
if err != nil {
continue
}
candidates = appendFontFileNames(candidates, i, fontFileNames(file))
file.Close()
}
r.fontSourcesCache.directories[dir] = candidates
}
return fontFileMatches(candidates, names, bold, italic)
}
// matchFontFS 匹配字体文件系统中的字体名称
// 入参: index 字体文件系统索引, names 字体名称, bold 是否粗体, italic 是否斜体
// 返回: []fontFileMatch 字体匹配列表
func (r *Renderer) matchFontFS(index int, names []string, bold, italic bool) []fontFileMatch {
fsys := r.fontFS[index]
if fonts, ok := fsys.(*FontFS); ok {
return fonts.matchStyle(names, bold, italic)
}
candidates, ok := r.fontSourcesCache.filesystems[index]
if !ok {
files, _ := fs.Glob(fsys, "*")
candidates = fontFileCandidates(files, path.Base)
for i, candidate := range candidates {
file, err := fsys.Open(candidate.name)
if err != nil {
continue
}
reader, ok := file.(io.ReaderAt)
if !ok {
data, _ := io.ReadAll(file)
reader = bytes.NewReader(data)
}
candidates = appendFontFileNames(candidates, i, fontFileNames(reader))
file.Close()
}
r.fontSourcesCache.filesystems[index] = candidates
}
return fontFileMatches(candidates, names, bold, italic)
}
// systemFontDirs 获取系统字体目录
// 返回: []string 字体目录列表
func systemFontDirs() []string {
switch runtime.GOOS {
case "android":
return []string{"/product/fonts", "/system/fonts"}
case "darwin":
return []string{`/Library/Fonts`}
case "linux":
return []string{`/usr/share/fonts`}
case "windows":
return []string{`C:\Windows\Fonts`}
}
return nil
}
@@ -16,21 +16,6 @@
package ofdgo
import "github.com/tdewolff/canvas"
// loadDefaultFonts 加载默认字体
// 返回: bool 是否加载成功
func (r *Renderer) loadDefaultFonts() bool {
for _, name := range fontDefaultSystemNames() {
if err := r.canvasState().fontFamily.LoadSystemFont(name, canvas.FontRegular); err == nil {
return true
}
}
return false
}
// canLoadSystemFonts 判断是否可以加载系统字体
// 返回: bool 是否可以加载系统字体
func canLoadSystemFonts() bool {
return true
}
// canLoadSystemFonts 判断是否可以读取系统字体
// 返回: bool 是否可以读取
func canLoadSystemFonts() bool { return true }
@@ -16,14 +16,6 @@
package ofdgo
// loadDefaultFonts 加载默认字体
// 返回: bool 是否加载成功
func (r *Renderer) loadDefaultFonts() bool {
return false
}
// canLoadSystemFonts 判断是否可以加载系统字体
// 返回: bool 是否可以加载系统字体
func canLoadSystemFonts() bool {
return false
}
// canLoadSystemFonts 判断是否可以读取系统字体
// 返回: bool 是否可以读取
func canLoadSystemFonts() bool { return false }
+5
View File
@@ -49,6 +49,11 @@ type GeometryBackend interface {
Clip(renderer *Renderer, clips *Clips, matrix Matrix, parent *GeometryPath) (*GeometryPath, error)
}
// GeometryCurves 将椭圆弧转换为光栅后端可消费的贝塞尔曲线,不改变源路径
type GeometryCurves interface {
Curves(path GeometryPath) (GeometryPath, error)
}
// Geometry 返回当前几何后端,不隐式恢复默认实现
// 返回: GeometryBackend 几何后端, error 未配置错误
func (r *Renderer) Geometry() (GeometryBackend, error) {
+24
View File
@@ -23,6 +23,30 @@ import (
// 坐标以左上角为原点,单位为毫米,子路径从Move开始,Close回到当前子路径起点
type GeometryPath []GeometrySegment
// closedGeometry 为填充轮廓补齐隐式闭合边,不改变源路径
// 入参: path 填充路径
// 返回: GeometryPath 显式闭合路径
func closedGeometry(path GeometryPath) GeometryPath {
result := make(GeometryPath, 0, len(path)+1)
open := false
for _, segment := range path {
if segment.Verb == GeometryMove && open {
result = append(result, GeometrySegment{Verb: GeometryClose})
}
result = append(result, segment)
if segment.Verb == GeometryMove {
open = true
}
if segment.Verb == GeometryClose {
open = false
}
}
if open {
result = append(result, GeometrySegment{Verb: GeometryClose})
}
return result
}
// GeometryVerb 表示几何路径指令
type GeometryVerb uint8
+33 -2
View File
@@ -56,13 +56,14 @@ type RasterSegment struct {
// RasterCommand 填充路径或绘制图像,Image非空时为图像指令
// Transform将局部坐标映射到页面,图像局部坐标与image.Image的像素坐标一致
// 字形和描边由页面编译器生成轮廓,裁剪与底纹在编译时解析
// 字形和描边由页面编译器生成轮廓,Clip为页面坐标图像裁剪,nil表示不裁剪
type RasterCommand struct {
Path []RasterSegment
Paint RasterPaint
EvenOdd bool
Transform RasterMatrix
Image image.Image
Clip []RasterSegment
}
// RasterPaint 预乘RGBA纯色或渐变,Gradient非空时忽略Color
@@ -79,12 +80,20 @@ const (
RasterRadial
)
// RasterGradient 局部坐标下的线性或双圆径向渐变,区间外延续边界颜色
// RasterGradient 局部坐标下的线性或双圆径向渐变,Spread为空时延续边界颜色
type RasterGradient struct {
Kind RasterGradientKind
Start, End RasterPoint
R0, R1 float64
Stops []ColorStop
Spread *RasterSpread
}
// RasterSpread 保存OFD渐变延伸和周期,nil表示连续延伸
type RasterSpread struct {
Extend int
MapType string
Period float64
}
// PixelSize 检查页面尺寸并计算目标像素大小
@@ -135,9 +144,31 @@ func (g *RasterGradient) At(x, y float64) color.RGBA {
dr := g.R1 - g.R0
a, b, c := dx*dx+dy*dy-dr*dr, -2*(px*dx+py*dy+g.R0*dr), px*px+py*py-g.R0*g.R0
t = rasterRadialPosition(a, b, c, g.R0, dr)
if g.Spread != nil && math.Hypot(dx, dy) < math.Abs(dr) {
d := b*b - 4*a*c
if d < 0 {
return color.RGBA{}
}
t = (-b - math.Copysign(math.Sqrt(d), dr)) / (2 * a)
}
} else if length := dx*dx + dy*dy; length > 0 {
t = (px*dx + py*dy) / length
}
if spread := g.Spread; spread != nil {
if t < 0 && spread.Extend&1 == 0 || t > 1 && spread.Extend&2 == 0 {
return color.RGBA{}
}
period := spread.Period
if period <= 0 {
period = 1
}
switch spread.MapType {
case "Repeat":
t = t/period - math.Floor(t/period)
case "Reflect":
t = reflectShdPosition(t / period)
}
}
return g.colorAt(t)
}
+18
View File
@@ -16,6 +16,7 @@ package ofdgo
import (
"image"
"image/color"
"golang.org/x/image/draw"
"golang.org/x/image/math/f64"
@@ -27,6 +28,23 @@ func drawRasterImage(dst draw.Image, src image.Image, m RasterMatrix) {
draw.CatmullRom.Transform(dst, f64.Aff3{m[0], m[2], m[4], m[1], m[3], m[5]}, src, src.Bounds(), draw.Over, nil)
}
// drawRasterImageCommand 使用当前后端生成裁剪蒙版,共用高质量图像采样
// 入参: backend 当前后端, page 页面, dst 目标图像, src 原始图像, command 图像指令
// 返回: error 裁剪绘制错误
func drawRasterImageCommand(backend RasterBackend, page *RasterPage, dst draw.Image, src image.Image, command RasterCommand) error {
m := page.PixelTransform(command.Transform, dst.Bounds().Dy())
if command.Clip == nil {
drawRasterImage(dst, src, m)
return nil
}
mask, err := backend.Render(&RasterPage{Width: page.Width, Height: page.Height, DPI: page.DPI, Commands: []RasterCommand{{Path: command.Clip, Paint: RasterPaint{Color: color.RGBA{255, 255, 255, 255}}, Transform: RasterMatrix{1, 0, 0, 1, 0, 0}}}})
if err != nil {
return err
}
draw.CatmullRom.Transform(dst, f64.Aff3{m[0], m[2], m[4], m[1], m[3], m[5]}, imagePixelSource(src), src.Bounds(), draw.Over, &draw.Options{DstMask: mask})
return nil
}
// PixelTransform 将页面毫米变换转换为像素变换,保留尺寸取整后的原点位置
// 入参: m 局部到页面变换, height 目标像素高度
// 返回: RasterMatrix 局部到像素变换
+466
View File
@@ -0,0 +1,466 @@
// 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"
"math"
"strconv"
"strings"
)
// semanticCompiler 将公共页面语义编译为路径和原始图片,绘制、搜索和度量共享定位
type semanticCompiler struct {
renderer *Renderer
geometry GeometryBackend
page *RasterPage
text PageText
measurement ObjectMeasurement
measure, contours, textOnly, pattern bool
}
// newSemanticCompiler 绑定当前字体和几何能力
// 入参: r 渲染器
// 返回: *semanticCompiler 编译器, error 能力错误
func newSemanticCompiler(r *Renderer) (*semanticCompiler, error) {
geometry, err := r.Geometry()
if err != nil {
return nil, err
}
return &semanticCompiler{renderer: r, geometry: geometry}, nil
}
// DrawObject 解释叶子对象,不调用任何具体页面编译器
// 入参: object 源对象, state 继承状态
// 返回: error 编译错误
func (c *semanticCompiler) DrawObject(object *GraphicObject, state RenderState) error {
switch object.Type {
case "TextObject":
return c.textObject(object.TextObject, state)
case "PathObject":
if !c.textOnly {
return c.pathObject(object.PathObject, state)
}
case "ImageObject":
if !c.textOnly {
return c.imageObject(object.ImageObject, state)
}
default:
return fmt.Errorf("unsupported graphic object %q", object.Type)
}
return nil
}
// segments 转换光栅曲线,仅在输出边界展开椭圆弧
// 入参: path 页面路径
// 返回: []RasterSegment 光栅路径, error 能力或路径错误
func (c *semanticCompiler) segments(path GeometryPath) ([]RasterSegment, error) {
for _, segment := range path {
if segment.Verb == GeometryArc {
curves, ok := c.geometry.(GeometryCurves)
if !ok {
return nil, fmt.Errorf("geometry curves: %w", ErrBackendUnavailable)
}
var err error
path, err = curves.Curves(path)
if err != nil {
return nil, err
}
break
}
}
result := make([]RasterSegment, 0, len(path))
for _, s := range path {
segment := RasterSegment{End: RasterPoint{s.End.X, s.End.Y}, Control1: RasterPoint{s.Control1.X, s.Control1.Y}, Control2: RasterPoint{s.Control2.X, s.Control2.Y}}
switch s.Verb {
case GeometryMove:
segment.Verb = RasterMove
case GeometryLine:
segment.Verb = RasterLine
case GeometryQuad:
segment.Verb = RasterQuad
case GeometryCubic:
segment.Verb = RasterCubic
case GeometryClose:
segment.Verb = RasterClose
default:
return nil, fmt.Errorf("unsupported raster geometry %d", s.Verb)
}
result = append(result, segment)
}
return result, nil
}
// addBounds 收集精确范围和独立轮廓,不分配页面像素
// 入参: path 页面路径, evenOdd 奇偶规则
// 返回: error 几何错误
func (c *semanticCompiler) addBounds(path GeometryPath, evenOdd bool) error {
if len(path) == 0 {
return nil
}
box, err := c.geometry.Bounds(path)
if err != nil {
return err
}
c.measurement.Bounds = unionTextBox(c.measurement.Bounds, box)
if c.contours {
svg, err := closedGeometry(path).SVG()
if err != nil {
return err
}
c.measurement.Contours = append(c.measurement.Contours, ObjectContour{Path: svg, EvenOdd: evenOdd})
}
return nil
}
// fill 应用对象裁剪、底纹和渐变,不改变输入路径
// 入参: path 页面路径, paint 画刷, evenOdd 奇偶规则, clip 对象裁剪, matrix 图案矩阵, shadingMatrix 渐变矩阵
// 返回: error 几何或编译错误
func (c *semanticCompiler) fill(path GeometryPath, paint Paint, evenOdd bool, clip *GeometryPath, matrix, shadingMatrix Matrix) error {
if len(path) == 0 || paint.Kind == PaintNone || paint.Kind == PaintSolid && paint.Color.A == 0 {
return nil
}
var err error
if clip != nil || paint.Kind == PaintPattern || paint.Gradient != nil {
path = closedGeometry(path)
}
if evenOdd && (clip != nil || paint.Kind == PaintPattern || paint.Gradient != nil) {
path, err = c.geometry.Normalize(path, true)
if err != nil {
return err
}
evenOdd = false
}
path, err = clipGeometry(c.geometry, path, clip)
if err != nil || len(path) == 0 {
return err
}
if paint.Gradient != nil {
path, err = c.shadingClip(path, paint, shadingMatrix)
if err != nil || len(path) == 0 {
return err
}
}
if c.measure {
return c.addBounds(path, evenOdd)
}
if paint.Kind == PaintPattern {
return c.fillPattern(path, paint.Pattern, matrix)
}
command := RasterCommand{Paint: RasterPaint{Color: paint.Color}, EvenOdd: evenOdd, Transform: RasterMatrix{1, 0, 0, 1, 0, 0}}
if paint.Gradient != nil {
gradient, view := semanticGradient(paint, shadingMatrix)
inverse, ok := view.Invert()
if !ok {
return fmt.Errorf("invalid shading transform")
}
path, err = c.geometry.Transform(path, inverse)
if err != nil {
return err
}
command.Transform = RasterMatrix(view.Values())
command.Paint.Gradient = gradient
}
command.Path, err = c.segments(path)
if err != nil {
return err
}
c.page.Commands = append(c.page.Commands, command)
return nil
}
// semanticGradient 保留渐变周期、延伸与椭圆坐标
// 入参: paint 渐变画刷, matrix 渐变父矩阵
// 返回: *RasterGradient 光栅渐变, Matrix 局部到页面矩阵
func semanticGradient(paint Paint, matrix Matrix) (*RasterGradient, Matrix) {
s := paint.Gradient
extend, _ := strconv.Atoi(s.Extend)
period := 1.0
length := math.Hypot(s.End.X-s.Start.X, s.End.Y-s.Start.Y)
if s.MapUnit > 0 && length > 0 {
period = s.MapUnit / length
}
g := &RasterGradient{Start: RasterPoint{s.Start.X, s.Start.Y}, End: RasterPoint{s.End.X, s.End.Y}, R0: s.StartRadius, R1: s.EndRadius, Stops: s.Stops, Spread: &RasterSpread{Extend: extend, MapType: s.MapType, Period: period}}
if paint.Kind == PaintRadial {
g.Kind = RasterRadial
if s.Eccentricity > 0 && s.Eccentricity < 1 {
angle := s.Angle * math.Pi / 180
view := TranslationMatrix(s.Start.X, s.Start.Y).Multiply(Matrix{a: math.Cos(angle), b: math.Sin(angle), c: -math.Sin(angle), d: math.Cos(angle)}).Multiply(Matrix{a: 1, d: math.Sqrt(1 - s.Eccentricity*s.Eccentricity)}).Multiply(TranslationMatrix(-s.Start.X, -s.Start.Y))
inverse, _ := view.Invert()
g.End.X, g.End.Y = inverse.Transform(g.End.X, g.End.Y)
matrix = matrix.Multiply(view)
}
if math.Hypot(g.End.X-g.Start.X, g.End.Y-g.Start.Y) >= math.Abs(g.R1-g.R0) {
g.Spread = nil
}
}
return g, matrix
}
// semanticStyle 保存已继承的路径颜色和描边参数
type semanticStyle struct {
fill, stroke *FillColor
options StrokeOptions
}
// objectStyle 合并继承与对象样式,不修改共享绘制参数
// 入参: object 路径样式, defaults 继承样式, linear 组合变换
// 返回: semanticStyle 解析样式
func (c *semanticCompiler) objectStyle(object PathObject, defaults *DrawParam, linear Matrix) semanticStyle {
defaults = c.renderer.drawParamDefaults(object.DrawParam, defaults)
style := semanticStyle{options: StrokeOptions{Width: defaultPathLineWidth, Cap: "Butt", Join: "Miter", MiterLimit: defaultMiterLimit}}
if defaults != nil {
style.fill = defaults.FillColor
style.stroke = (*FillColor)(defaults.StrokeColor)
if defaults.LineWidth > 0 {
style.options.Width = defaults.LineWidth
}
if defaults.Cap != "" {
style.options.Cap = defaults.Cap
}
if defaults.Join != "" {
style.options.Join = defaults.Join
}
if defaults.MiterLimit > 0 {
style.options.MiterLimit = defaults.MiterLimit
}
style.options.Dashes = parseFloats(defaults.DashPattern)
if defaults.DashOffset != nil {
style.options.DashOffset = *defaults.DashOffset
}
}
if object.FillColor != nil {
style.fill = object.FillColor
}
if object.StrokeColor != nil {
style.stroke = (*FillColor)(object.StrokeColor)
}
if object.LineWidth > 0 {
style.options.Width = object.LineWidth
}
if object.Cap != "" {
style.options.Cap = object.Cap
}
if object.Join != "" {
style.options.Join = object.Join
}
if object.MiterLimit > 0 {
style.options.MiterLimit = object.MiterLimit
}
if object.DashPattern != "" || object.dashPatternSet {
style.options.Dashes = parseFloats(object.DashPattern)
}
if object.DashOffset != nil {
style.options.DashOffset = *object.DashOffset
}
if style.stroke == nil {
style.stroke = &FillColor{}
}
style.fill = withFillAlpha(style.fill, object.Alpha)
style.stroke = withFillAlpha(style.stroke, object.Alpha)
if scale := math.Sqrt(math.Abs(linear.a*linear.d - linear.b*linear.c)); scale > 0 {
style.options.Width *= scale
style.options.DashOffset *= scale
for i := range style.options.Dashes {
style.options.Dashes[i] *= scale
}
}
return style
}
// pathObject 编译填充、描边和虚线路径
// 入参: object 路径对象, state 继承状态
// 返回: error 路径或画刷错误
func (c *semanticCompiler) pathObject(object PathObject, state RenderState) error {
if object.Visible != nil && !*object.Visible {
return nil
}
matrix, linear := renderObjectMatrix(object.Boundary, NewMatrix(object.CTM), state)
local := object
local.Boundary = "0 0 1 1"
local.CTM = ""
var path GeometryPath
var err error
tinyMatrix := matrix
if state.BoundaryInCTM && state.Parent != nil {
tinyMatrix, _ = renderObjectMatrix(object.Boundary, NewMatrix(object.CTM), RenderState{})
}
path = tinyFillRectPath(object, tinyMatrix)
if path != nil {
if state.BoundaryInCTM && state.Parent != nil {
path, err = c.geometry.Transform(path, *state.Parent)
}
} else {
path, err = c.geometry.Path(local)
if err != nil {
return err
}
path, err = c.geometry.Transform(path, matrix)
}
if err != nil {
return err
}
clip, err := c.renderer.objectGeometryClip(object.Clips, object.Boundary, NewMatrix(object.CTM), state)
if err != nil {
return err
}
style := c.objectStyle(object, state.Defaults, linear)
shading, _ := renderObjectMatrix(object.Boundary, IdentityMatrix, state)
if object.Fill != nil && *object.Fill {
if err := c.fill(path, c.renderer.ResolvePaint(style.fill), object.Rule == "Even-Odd", clip, matrix, shading); err != nil {
return err
}
}
if object.Stroke == nil || *object.Stroke {
stroke, err := c.geometry.Stroke(path, style.options)
if err != nil {
return err
}
return c.fill(stroke, c.renderer.ResolvePaint(style.stroke), false, clip, matrix, shading)
}
return nil
}
// textObject 共用字形定位处理绘制、原文提取和轮廓
// 入参: object 文字对象, state 继承状态
// 返回: error 字体或几何错误
func (c *semanticCompiler) textObject(object TextObject, state RenderState) error {
if object.Visible != nil && !*object.Visible || object.Fill != nil && !*object.Fill && (object.Stroke == nil || !*object.Stroke) {
return nil
}
positioned, err := c.renderer.PositionText(object, state)
if err != nil {
return err
}
if !c.pattern && positioned.Run.Text != "" {
c.text.Runs = append(c.text.Runs, positioned.Run)
}
if c.textOnly {
return nil
}
if c.measure && !c.contours {
for _, box := range positioned.Run.Boxes {
c.measurement.Bounds = unionTextBox(c.measurement.Bounds, box)
}
return nil
}
_, linear := renderObjectMatrix(object.Boundary, NewMatrix(object.CTM), state)
style := c.objectStyle(PathObject{DrawParam: object.DrawParam, FillColor: object.FillColor, StrokeColor: object.StrokeColor, Alpha: object.Alpha, LineWidth: object.LineWidth, Join: object.Join, MiterLimit: object.MiterLimit}, state.Defaults, linear)
if style.fill == nil {
style.fill = withFillAlpha(&FillColor{}, object.Alpha)
}
shading, _ := renderObjectMatrix(object.Boundary, IdentityMatrix, state)
for _, glyph := range positioned.Glyphs {
if object.Fill == nil || *object.Fill {
if err := c.fill(glyph.Path, c.renderer.ResolvePaint(style.fill), false, positioned.Clip, positioned.Matrix, shading); err != nil {
return err
}
}
if object.Stroke != nil && *object.Stroke && len(glyph.Path) > 0 {
path, err := c.geometry.Stroke(glyph.Path, style.options)
if err != nil {
return err
}
if err := c.fill(path, c.renderer.ResolvePaint(style.stroke), false, positioned.Clip, positioned.Matrix, shading); err != nil {
return err
}
}
if err := c.fill(glyph.Underline, c.renderer.ResolvePaint(style.fill), false, positioned.Clip, positioned.Matrix, shading); err != nil {
return err
}
}
return nil
}
// fillPattern 在对象裁剪区域内展开底纹,保留单元反射和继承样式
// 入参: clip 绘制区域, pattern 底纹, object 对象矩阵
// 返回: error 编译错误
func (c *semanticCompiler) fillPattern(clip GeometryPath, pattern *PatternPaint, object Matrix) error {
xstep, ystep := math.Max(pattern.XStep, pattern.Width), math.Max(pattern.YStep, pattern.Height)
if xstep <= 0 || ystep <= 0 {
return fmt.Errorf("invalid pattern cell size")
}
matrix := NewMatrix(pattern.CTM)
if pattern.RelativeTo != "Page" {
matrix = object.Multiply(matrix)
}
inverse, ok := matrix.Invert()
if !ok {
return fmt.Errorf("invalid pattern transform")
}
box, err := c.geometry.Bounds(clip)
if err != nil {
return err
}
box = inverse.TransformBox(box)
prior := c.pattern
c.pattern = true
defer func() { c.pattern = prior }()
defaults := &DrawParam{FillColor: &pattern.Color, StrokeColor: (*StrokeColor)(&pattern.Color)}
for x, end := int(math.Floor(box.X/xstep))-1, int(math.Ceil((box.X+box.W)/xstep))+1; x <= end; x++ {
for y, end := int(math.Floor(box.Y/ystep))-1, int(math.Ceil((box.Y+box.H)/ystep))+1; y <= end; y++ {
tile := matrix.Multiply(TranslationMatrix(float64(x)*xstep, float64(y)*ystep))
if x%2 != 0 && (pattern.ReflectMethod == "Column" || pattern.ReflectMethod == "RowAndColumn") {
tile = tile.Multiply(Matrix{a: -1, d: 1, e: pattern.Width})
}
if y%2 != 0 && (pattern.ReflectMethod == "Row" || pattern.ReflectMethod == "RowAndColumn") {
tile = tile.Multiply(Matrix{a: 1, d: -1, f: pattern.Height})
}
for _, object := range pattern.CellContent.Objects {
object = mergeGraphicObjectAlpha(object, pattern.Alpha)
if err := c.renderer.WalkObject(&object, RenderState{Defaults: defaults, Parent: &tile, BoundaryInCTM: true, Clip: &clip}, c); err != nil {
return err
}
}
}
}
return nil
}
// tinyFillRectPath 保留历史文档中微小填充块的缺省末端处理
// 入参: object 路径对象, matrix 不含外层变换的页面矩阵
// 返回: GeometryPath 修正路径,非匹配格式时为空
func tinyFillRectPath(object PathObject, matrix Matrix) GeometryPath {
if object.Fill == nil || !*object.Fill || object.Stroke == nil || *object.Stroke {
return nil
}
box, err := ParseBox(object.Boundary)
if err != nil || box.W <= 0 || box.H <= 0 || box.W > .6 || box.H > .6 {
return nil
}
tokens := strings.Fields(object.AbbreviatedData)
if len(tokens) != 11 || tokens[0] != "M" || tokens[3] != "L" || tokens[6] != "L" || tokens[9] != "L" || tokens[10] != "C" {
return nil
}
var minX, minY, maxX, maxY float64
for i := 1; i < 10; i += 3 {
x, errX := strconv.ParseFloat(tokens[i], 64)
y, errY := strconv.ParseFloat(tokens[i+1], 64)
if errX != nil || errY != nil {
return nil
}
x, y = matrix.Transform(x, y)
if i == 1 {
minX, maxX, minY, maxY = x, x, y, y
} else {
minX = math.Min(minX, x)
maxX = math.Max(maxX, x)
minY = math.Min(minY, y)
maxY = math.Max(maxY, y)
}
}
expand := math.Min(box.W, box.H) * .08
return geometryRectangle(Box{X: minX - expand, Y: minY - expand, W: maxX - minX + 2*expand, H: maxY - minY + 2*expand})
}
+197
View File
@@ -0,0 +1,197 @@
// 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"
"slices"
)
// imageObject 共用图片定位、裁剪和边框,不在度量时读取像素
// 入参: object 图片对象, state 继承状态
// 返回: error 资源或几何错误
func (c *semanticCompiler) imageObject(object ImageObject, state RenderState) error {
if object.Visible != nil && !*object.Visible || object.Alpha != nil && *object.Alpha == 0 {
return nil
}
box, _ := ParseBox(object.Boundary)
local := NewMatrix(object.CTM)
if object.CTM == "" {
local = Matrix{a: box.W, d: box.H}
}
matrix, _ := renderObjectMatrix(object.Boundary, local, state)
clip, err := c.renderer.objectGeometryClip(object.Clips, object.Boundary, local, state)
if err != nil {
return err
}
if c.measure {
path, err := c.geometry.Transform(geometryRectangle(Box{W: 1, H: 1}), matrix)
if err != nil {
return err
}
path, err = clipGeometry(c.geometry, path, clip)
if err != nil {
return err
}
if err = c.addBounds(path, false); err != nil {
return err
}
} else {
img, err := c.renderer.ImageResource(object.ResourceID)
if err != nil {
return err
}
if object.ImageMask != "" {
mask, err := c.renderer.ImageResource(object.ImageMask)
if err != nil {
return err
}
img = imageWithMask(img, mask)
}
img = imageWithAlpha(img, object.Alpha)
bounds := img.Bounds()
matrix = matrix.Multiply(Matrix{a: 1 / float64(bounds.Dx()), d: 1 / float64(bounds.Dy())}).Multiply(TranslationMatrix(-float64(bounds.Min.X), -float64(bounds.Min.Y)))
if err := c.addImage(img, matrix, clip); err != nil {
return err
}
}
if object.Border != nil {
border := imageBorderObject(object, box)
if border.LineWidth == 0 {
return nil
}
state.Clip = clip
state.Defaults = nil
return c.pathObject(border, state)
}
return nil
}
// imageBorderObject 共用图像圆角边框的标准路径与样式
// 入参: object 图像对象, box 物理范围
// 返回: PathObject 边框
func imageBorderObject(object ImageObject, box Box) PathObject {
border := object.Border
width := defaultPathLineWidth
if border.LineWidth != nil {
width = *border.LineWidth
}
w, h := box.W, box.H
data := fmt.Sprintf("M 0 0 L %g 0 L %g %g L 0 %g C", w, w, h, h)
rx, ry := min(border.HorizonalCornerRadius, w/2), min(border.VerticalCornerRadius, h/2)
if rx > 0 && ry > 0 {
data = fmt.Sprintf("M 0 %g A %g %g 0 0 1 %g 0 L %g 0 A %g %g 0 0 1 %g %g L %g %g A %g %g 0 0 1 %g %g L %g %g A %g %g 0 0 1 0 %g C",
ry, rx, ry, rx, w-rx, rx, ry, w, ry, w, h-ry, rx, ry, w-rx, h, rx, h, rx, ry, h-ry)
}
return PathObject{Boundary: object.Boundary, LineWidth: width, DashOffset: &border.DashOffset, DashPattern: border.DashPattern, Alpha: object.Alpha, StrokeColor: border.BorderColor, AbbreviatedData: data}
}
// addImage 保留图像资源与页面坐标裁剪,延迟像素采样
// 入参: img 图像, matrix 像素到页面矩阵, clip 页面裁剪
// 返回: error 裁剪转换错误
func (c *semanticCompiler) addImage(img image.Image, matrix Matrix, clip *GeometryPath) error {
command := RasterCommand{Image: img, Transform: RasterMatrix(matrix.Values())}
if clip != nil {
var err error
command.Clip, err = c.segments(*clip)
if err != nil {
return err
}
}
c.page.Commands = append(c.page.Commands, command)
return nil
}
// DrawStamp 保留签章图片和嵌套OFD的绘制顺序,不混入正文搜索
// 入参: stamp 印章
// 返回: error 印章解析或编译错误
func (c *semanticCompiler) DrawStamp(stamp Stamp) error {
if c.textOnly || c.measure || len(stamp.Data) == 0 {
return nil
}
if stamp.Type == "ofd" {
reader, err := NewReader(bytes.NewReader(stamp.Data), int64(len(stamp.Data)))
if err != nil {
return err
}
defer reader.Close()
document, err := reader.Doc()
if err != nil {
return err
}
renderer := c.renderer.childRenderer(reader)
renderer.TransparentBackground = true
for _, reference := range document.Pages.Page {
content, err := reader.PageContent(reference)
if err != nil {
return err
}
page, err := renderer.CompilePage(content)
if err != nil {
return err
}
if stamp.Clip != nil {
if renderer.backends.Raster == nil {
return fmt.Errorf("stamp raster: %w", ErrBackendUnavailable)
}
img, err := renderer.backends.Raster.Render(page)
if err != nil {
return err
}
return c.stampImage(stampImageWithTransparentWhite(img), stamp)
}
matrix := TranslationMatrix(stamp.Box.X, stamp.Box.Y).Multiply(Matrix{a: stamp.Box.W / page.Width, d: stamp.Box.H / page.Height})
for _, command := range page.Commands {
command.Transform = RasterMatrix(matrix.Multiply(MatrixFromValues([6]float64(command.Transform))).Values())
if command.Clip != nil {
command.Clip = slices.Clone(command.Clip)
for i := range command.Clip {
segment := command.Clip[i]
segment.End = RasterMatrix(matrix.Values()).Apply(segment.End)
segment.Control1 = RasterMatrix(matrix.Values()).Apply(segment.Control1)
segment.Control2 = RasterMatrix(matrix.Values()).Apply(segment.Control2)
command.Clip[i] = segment
}
}
c.page.Commands = append(c.page.Commands, command)
}
}
return nil
}
img, _, err := decodeImageData(stamp.Data)
if err != nil {
return err
}
return c.stampImage(stampImageWithTransparentWhite(img), stamp)
}
// stampImage 按印章裁剪区域放置图像
// 入参: img 印章图像, stamp 印章
// 返回: error 编译错误
func (c *semanticCompiler) stampImage(img image.Image, stamp Stamp) error {
box := stamp.Box
if stamp.Clip != nil {
img = clipStampImage(img, box, *stamp.Clip)
box.X += stamp.Clip.X
box.Y += stamp.Clip.Y
box.W = stamp.Clip.W
box.H = stamp.Clip.H
}
bounds := img.Bounds()
matrix := TranslationMatrix(box.X, box.Y).Multiply(Matrix{a: box.W / float64(bounds.Dx()), d: box.H / float64(bounds.Dy())}).Multiply(TranslationMatrix(-float64(bounds.Min.X), -float64(bounds.Min.Y)))
return c.addImage(img, matrix, nil)
}
+108
View File
@@ -0,0 +1,108 @@
// 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 "math"
// reflectShdPosition 获取镜像周期内的位置
// 入参: position 渐变位置
// 返回: float64 周期内的位置
func reflectShdPosition(position float64) float64 {
index := math.Floor(position)
position -= index
if math.Mod(index, 2) != 0 {
return 1 - position
}
return position
}
// shadingClip 按渐变延伸规则裁剪绘制与选区,避免只在采样时隐藏区间外像素
// 入参: path 页面路径, paint 渐变画刷, matrix 渐变父矩阵
// 返回: GeometryPath 可见路径, error 几何错误
func (c *semanticCompiler) shadingClip(path GeometryPath, paint Paint, matrix Matrix) (GeometryPath, error) {
gradient, view := semanticGradient(paint, matrix)
spread := gradient.Spread
if spread == nil || spread.Extend == 3 {
return path, nil
}
inverse, ok := view.Invert()
if !ok {
return nil, nil
}
local, err := c.geometry.Transform(path, inverse)
if err != nil {
return nil, err
}
if gradient.Kind == RasterLinear {
dx, dy := gradient.End.X-gradient.Start.X, gradient.End.Y-gradient.Start.Y
axis := Matrix{a: dx, b: dy, c: -dy, d: dx, e: gradient.Start.X, f: gradient.Start.Y}
inv, _ := axis.Invert()
box, err := c.geometry.Bounds(local)
if err != nil {
return nil, err
}
box = inv.TransformBox(box)
end := box.X + box.W
if spread.Extend&1 == 0 {
box.X = math.Max(box.X, 0)
}
if spread.Extend&2 == 0 {
end = math.Min(end, 1)
}
box.W = end - box.X
if box.W <= 0 {
return nil, nil
}
clip, err := c.geometry.Transform(geometryRectangle(box), axis)
if err != nil {
return nil, err
}
local, err = c.geometry.Combine(local, clip, GeometryIntersect)
if err != nil {
return nil, err
}
} else {
inner, outer, r0, r1, extend := gradient.Start, gradient.End, gradient.R0, gradient.R1, spread.Extend
if r1 < r0 {
inner, outer, r0, r1 = outer, inner, r1, r0
extend = (extend&1)<<1 | (extend&2)>>1
}
if extend&2 == 0 {
local, err = c.geometry.Combine(local, geometryCircle(outer, r1), GeometryIntersect)
if err != nil {
return nil, err
}
}
if extend&1 == 0 && r0 > 0 {
local, err = c.geometry.Combine(local, geometryCircle(inner, r0), GeometrySubtract)
if err != nil {
return nil, err
}
}
}
return c.geometry.Transform(local, view)
}
// geometryCircle 构建标准圆弧,不提前折线化
// 入参: center 圆心, radius 半径
// 返回: GeometryPath 圆路径
func geometryCircle(center RasterPoint, radius float64) GeometryPath {
return GeometryPath{
{Verb: GeometryMove, End: Point{X: center.X + radius, Y: center.Y}},
{Verb: GeometryArc, RadiusX: radius, RadiusY: radius, Sweep: true, End: Point{X: center.X - radius, Y: center.Y}},
{Verb: GeometryArc, RadiusX: radius, RadiusY: radius, Sweep: true, End: Point{X: center.X + radius, Y: center.Y}},
{Verb: GeometryClose},
}
}
+373
View File
@@ -0,0 +1,373 @@
// 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"
"math"
"strings"
)
// PositionedGlyph 保存页面坐标字形与对应原文区间
type PositionedGlyph struct {
Path GeometryPath
Underline GeometryPath
Start, End int
}
// PositionedText 保存绘制、搜索和度量共同使用的定位结果
// Glyphs中的路径和Clip为只读页面坐标,Run保留原文而非包装字符
type PositionedText struct {
Glyphs []PositionedGlyph
Run TextRun
Clip *GeometryPath
Matrix Matrix
}
// glyphOutlineKey 区分字体、字形与毫米字号
type glyphOutlineKey struct {
font *PreparedFont
glyph uint16
size float64
}
// preparedMetrics 复用已包装字体的后端度量,不混用不同后端实例
// 入参: prepared 已包装字体
// 返回: FontMetrics 字体度量, error 字体解析错误
func (r *Renderer) preparedMetrics(prepared *PreparedFont) (FontMetrics, error) {
if metrics := r.fontMetrics[prepared]; metrics != nil {
return metrics, nil
}
metrics, err := r.backends.Fonts.OpenFont(prepared.Data)
if err != nil {
return nil, err
}
r.fontMetrics[prepared] = metrics
return metrics, nil
}
// preparedOutline 按字形和字号复用只读轮廓
// 入参: prepared 字体, metrics 度量, glyph 字形编号, size 毫米字号
// 返回: GeometryPath 字形路径, error 能力或解析错误
func (r *Renderer) preparedOutline(prepared *PreparedFont, metrics FontMetrics, glyph uint16, size float64) (GeometryPath, error) {
key := glyphOutlineKey{prepared, glyph, size}
if path, ok := r.glyphOutlines[key]; ok {
return path, nil
}
provider, ok := metrics.(FontOutlines)
if !ok {
return nil, fmt.Errorf("font outlines: %w", ErrBackendUnavailable)
}
path, err := provider.GlyphOutline(glyph, size)
if err == nil {
r.glyphOutlines[key] = path
}
return path, err
}
// renderObjectMatrix 统一边界、局部变换与父变换的组合顺序
// 入参: boundary 对象边界, local 局部矩阵, state 继承状态
// 返回: Matrix 页面矩阵, Matrix 不含边界平移的组合矩阵
func renderObjectMatrix(boundary string, local Matrix, state RenderState) (Matrix, Matrix) {
box, _ := ParseBox(boundary)
linear := local
if state.Parent != nil {
linear = state.Parent.Multiply(local)
}
matrix := TranslationMatrix(box.X, box.Y).Multiply(linear)
if state.BoundaryInCTM && state.Parent != nil {
matrix = state.Parent.Multiply(TranslationMatrix(box.X, box.Y)).Multiply(local)
}
return matrix, linear
}
// objectGeometryClip 解析对象裁剪并合并父裁剪
// 入参: clips 对象裁剪, boundary 边界, local 局部矩阵, state 继承状态
// 返回: *GeometryPath 页面裁剪, error 几何错误
func (r *Renderer) objectGeometryClip(clips *Clips, boundary string, local Matrix, state RenderState) (*GeometryPath, error) {
if clips == nil {
return state.Clip, nil
}
matrix, _ := renderObjectMatrix(boundary, local, state)
if clips.TransFlag != nil && !*clips.TransFlag {
copy := *clips
copy.TransFlag = nil
clips = &copy
box, _ := ParseBox(boundary)
matrix = TranslationMatrix(box.X, box.Y)
if state.BoundaryInCTM && state.Parent != nil {
matrix = state.Parent.Multiply(matrix)
}
}
geometry, err := r.Geometry()
if err != nil {
return nil, err
}
return geometry.Clip(r, clips, matrix, state.Clip)
}
// clipGeometry 将非零填充路径与页面裁剪相交
// 入参: geometry 几何后端, path 页面路径, clip 页面裁剪
// 返回: GeometryPath 裁剪路径, error 几何错误
func clipGeometry(geometry GeometryBackend, path GeometryPath, clip *GeometryPath) (GeometryPath, error) {
if clip == nil {
return path, nil
}
return geometry.Combine(path, *clip, GeometryIntersect)
}
// PositionText 定位文字、字形与搜索范围,不依赖页面输出后端
// 入参: object 文字对象, state 继承状态
// 返回: *PositionedText 定位结果, error 字体或几何错误
func (r *Renderer) PositionText(object TextObject, state RenderState) (*PositionedText, error) {
result := &PositionedText{Run: TextRun{ID: object.ID, Text: object.Text()}}
result.Run.Boxes = make([]Box, len([]rune(result.Run.Text)))
if object.Visible != nil && !*object.Visible {
return result, nil
}
geometry, err := r.Geometry()
if err != nil {
return nil, err
}
local := NewMatrix(object.CTM)
matrix, linear := renderObjectMatrix(object.Boundary, local, state)
result.Matrix = matrix
result.Clip, err = r.objectGeometryClip(object.Clips, object.Boundary, local, state)
if err != nil {
return nil, err
}
defaults := r.drawParamDefaults(object.DrawParam, state.Defaults)
size := object.Size
if size == 0 && defaults != nil {
size = defaults.Size
}
if size == 0 {
size = 3.5
}
if object.VScale != 0 {
size *= object.VScale
}
horizontal := object.HScale
if horizontal == 0 {
horizontal = 1
}
id := object.Font
if id == "" && defaults != nil {
id = defaults.Font
}
prepared, err := r.PrepareFont(id)
if err != nil {
return nil, err
}
if len(prepared.Data) == 0 {
box, _ := ParseBox(object.Boundary)
boundary, _ := renderObjectMatrix(object.Boundary, IdentityMatrix, state)
path, err := geometry.Transform(geometryRectangle(Box{W: box.W, H: box.H}), boundary)
if err != nil {
return nil, err
}
path, err = clipGeometry(geometry, path, result.Clip)
if err != nil {
return nil, err
}
box, err = geometry.Bounds(path)
if err != nil {
return nil, err
}
for i := range result.Run.Boxes {
result.Run.Boxes[i] = box
}
return result, nil
}
metrics, err := r.preparedMetrics(prepared)
if err != nil {
return nil, err
}
weight, italic := object.Weight, object.Italic
if defaults != nil {
if weight == 0 {
weight = defaults.Weight
}
italic = italic || defaults.Italic
}
bold := weight >= 700
if definition := r.Reader.fontCache[id]; definition != nil {
bold = bold && !definition.Bold
if definition.FontFile == "" && fontNoSyntheticBold(definition.FontName, definition.FamilyName) {
bold = false
}
italic = italic && !definition.Italic
}
ascent, descent, _ := metrics.VerticalMetrics()
unit := size / float64(metrics.UnitsPerEm())
transforms := r.textObjectGlyphTransforms(id, object)
codeOffset, textOffset := 0, 0
angle := float64(object.CharDirection) * math.Pi / 180
rotate := Matrix{a: math.Cos(angle), b: math.Sin(angle), c: -math.Sin(angle), d: math.Cos(angle)}
glyphLinear := Matrix{a: linear.a, b: linear.b, c: linear.c, d: linear.d}.Multiply(rotate)
for index, code := range object.TextCode {
if object.textCodeLineBreak(index) {
textOffset++
}
runes := textCodeRunes(code.Value)
glyphs := textCodeGlyphs(runes, transforms, codeOffset)
spans := textGlyphSpans(runes, transforms, codeOffset)
if code.Index != "" {
runes = r.parseIndexRunes(code.Index, id)
glyphs = textRuneGlyphs(runes)
spans = make([][2]int, len(glyphs))
for i := range spans {
spans[i] = [2]int{0, 1}
}
}
positioner := NewTextPositioner(code, object.ReadDirection)
for i, glyph := range glyphs {
gid := uint16(glyph.GlyphID)
if glyph.GlyphID < 0 {
characters := []rune(glyph.Text)
if len(characters) > 0 {
gid = metrics.GlyphIndex(characters[0])
}
}
path, err := r.preparedOutline(prepared, metrics, gid, size)
if err != nil {
return nil, err
}
width := float64(metrics.GlyphAdvance(gid)) * unit
advance := width * horizontal
if (object.ReadDirection-object.CharDirection)%180 != 0 {
advance = size
}
origin := positioner.Next(advance)
scaleX := horizontal
if object.CharDirection == 0 {
limit := textGlyphAdvanceLimit(positioner.dxs, positioner.dys, positioner.xs, i, len(glyphs), origin.X)
if limit > 0 && width*scaleX > limit {
scaleX = limit / width
}
}
if bold && len(path) > 0 {
outline, err := geometry.Stroke(path, StrokeOptions{Width: size * 0.04, Join: "Round", Cap: "Round"})
if err != nil {
return nil, err
}
path, err = geometry.Combine(path, outline, GeometryUnion)
if err != nil {
return nil, err
}
}
if italic {
path, err = geometry.Transform(path, Matrix{a: 1, c: -0.3, d: 1})
if err != nil {
return nil, err
}
}
x, y := matrix.Transform(origin.X, origin.Y)
transform := TranslationMatrix(x, y).Multiply(glyphLinear).Multiply(Matrix{a: scaleX, d: 1})
bounds := Box{Y: -float64(ascent) * unit, W: width, H: (float64(ascent) + float64(descent)) * unit}
if len(path) > 0 {
shape, err := geometry.Bounds(path)
if err != nil {
return nil, err
}
bounds = unionTextBox(bounds, shape)
}
placed, err := geometry.Transform(path, transform)
if err != nil {
return nil, err
}
clipped, err := clipGeometry(geometry, placed, result.Clip)
if err != nil {
return nil, err
}
box, err := geometry.Bounds(clipped)
if err != nil {
return nil, err
}
span := spans[i]
item := PositionedGlyph{Path: placed, Start: textOffset + span[0], End: textOffset + span[1]}
if strings.Contains(object.Decoration, "Underline") {
line := GeometryPath{{Verb: GeometryMove, End: Point{Y: size * 0.1}}, {Verb: GeometryLine, End: Point{X: width * scaleX, Y: size * 0.1}}}
line, err = geometry.Stroke(line, StrokeOptions{Width: size * 0.05})
if err != nil {
return nil, err
}
item.Underline, err = geometry.Transform(line, TranslationMatrix(x, y).Multiply(glyphLinear))
if err != nil {
return nil, err
}
}
for j := item.Start; j < item.End; j++ {
result.Run.Boxes[j] = unionTextBox(result.Run.Boxes[j], box)
}
if err := result.Run.addGeometrySpan(geometry, item.Start, item.End, bounds, transform, result.Clip); err != nil {
return nil, err
}
result.Glyphs = append(result.Glyphs, item)
}
codeOffset += len(runes)
if code.Index == "" {
textOffset += len(runes)
} else {
textOffset++
}
}
return result, nil
}
// addGeometrySpan 以公共几何语义收集文字选择区域,保留旋转和斜切
// 入参: geometry 几何后端, start 起始字符, end 结束字符, bounds 局部范围, matrix 页面变换, clip 页面裁剪
// 返回: error 几何错误
func (run *TextRun) addGeometrySpan(geometry GeometryBackend, start, end int, bounds Box, matrix Matrix, clip *GeometryPath) error {
inverse, ok := matrix.Invert()
if !ok {
return nil
}
if clip != nil {
path, err := geometry.Transform(geometryRectangle(bounds), matrix)
if err != nil {
return err
}
path, err = clipGeometry(geometry, path, clip)
if err != nil {
return err
}
if len(path) == 0 {
return nil
}
path, err = geometry.Transform(path, inverse)
if err != nil {
return err
}
bounds, err = geometry.Bounds(path)
if err != nil {
return err
}
}
if bounds.W < 0 || bounds.H <= 0 {
return nil
}
matrix = matrix.Multiply(TranslationMatrix(bounds.X, bounds.Y)).Multiply(Matrix{a: bounds.W, d: bounds.H})
if n := len(run.Spans); n > 0 && run.Spans[n-1].Start == start && run.Spans[n-1].End == end {
previous := MatrixFromValues(run.Spans[n-1].Matrix)
if inverse, ok := previous.Invert(); ok {
box := unionTextBox(Box{W: 1, H: 1}, inverse.Multiply(matrix).TransformBox(Box{W: 1, H: 1}))
matrix = previous.Multiply(TranslationMatrix(box.X, box.Y)).Multiply(Matrix{a: box.W, d: box.H})
}
run.Spans = run.Spans[:n-1]
}
run.Spans = append(run.Spans, TextSpan{Start: start, End: end, Matrix: matrix.Values()})
return nil
}
+3
View File
@@ -43,6 +43,9 @@ type Renderer struct {
backendStates map[any]any
resolvedFonts map[resolvedFontKey]resolvedFontResult
preparedFonts map[string]*PreparedFont
fontSourcesCache *fontSourceCache
fontMetrics map[*PreparedFont]FontMetrics
glyphOutlines map[glyphOutlineKey]GeometryPath
renderError error
TransparentBackground bool
}
+26 -1
View File
@@ -58,7 +58,9 @@ func (b CanvasBackend) Render(page *RasterPage) (image.Image, error) {
}
source = converted
}
drawRasterImage(img, source, page.PixelTransform(cmd.Transform, h))
if err := drawRasterImageCommand(b, page, img, source, cmd); err != nil {
return nil, err
}
continue
}
p := &canvas.Path{}
@@ -93,6 +95,9 @@ func (b CanvasBackend) Render(page *RasterPage) (image.Image, error) {
default:
return nil, fmt.Errorf("canvas command %d: unsupported gradient %d", i, g.Kind)
}
if g.Spread != nil {
paint = canvas.Paint{Gradient: rasterGradientCanvas{g}}
}
}
style := canvas.Style{Fill: paint, FillRule: canvas.NonZero}
if cmd.EvenOdd {
@@ -104,3 +109,23 @@ func (b CanvasBackend) Render(page *RasterPage) (image.Image, error) {
r.Close()
return img, nil
}
// rasterGradientCanvas 将公共渐变交给Canvas采样,不改变OFD周期语义
type rasterGradientCanvas struct{ gradient *RasterGradient }
// At 返回局部坐标的预乘颜色
// 入参: x 横坐标, y 纵坐标
// 返回: color.RGBA 渐变颜色
func (g rasterGradientCanvas) At(x, y float64) color.RGBA { return g.gradient.At(x, y) }
// SetColorSpace 返回转换颜色空间后的独立渐变
// 入参: space 目标颜色空间
// 返回: canvas.Gradient 采样器
func (g rasterGradientCanvas) SetColorSpace(space canvas.ColorSpace) canvas.Gradient {
copy := *g.gradient
copy.Stops = append([]ColorStop(nil), copy.Stops...)
for i := range copy.Stops {
copy.Stops[i].Color = space.ToLinear(copy.Stops[i].Color)
}
return rasterGradientCanvas{&copy}
}
+2 -22
View File
@@ -15,6 +15,7 @@
package ofdgo
import (
"fmt"
"image"
"github.com/tdewolff/canvas"
@@ -68,28 +69,7 @@ func (r *clipRenderer) RenderImage(img image.Image, m canvas.Matrix) {}
// 入参: clips 裁剪对象, pageH 页面高度, bx 边界X坐标, by 边界Y坐标, objectCTM 对象CTM, parentCTM 父级CTM, boundaryInCTM 边界是否参与父级CTM
// 返回: *canvas.Path 路径对象
func (r *Renderer) buildObjectClipPath(clips *Clips, pageH float64, bx, by float64, objectCTM Matrix, parentCTM *Matrix, boundaryInCTM bool) *canvas.Path {
if clips == nil {
return nil
}
if !boundaryInCTM && parentCTM != nil {
objectCTM = parentCTM.Multiply(objectCTM)
}
if clips.TransFlag != nil && !*clips.TransFlag {
copy := *clips
copy.TransFlag = nil
clips = &copy
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)
path, err := r.objectGeometryClip(clips, fmt.Sprintf("%g %g 0 0", bx, by), objectCTM, RenderState{Parent: parentCTM, BoundaryInCTM: boundaryInCTM})
if err != nil {
r.renderError = err
return nil
+88
View File
@@ -0,0 +1,88 @@
// 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"
// CompilePage 使用公共OFD语义和配置的字形、几何能力编译页面
// 入参: r 渲染器, page 页面
// 返回: *RasterPage 绘制页面, error 编译错误
func (GGBackend) CompilePage(r *Renderer, page *PageContent) (*RasterPage, error) {
box, err := r.GetPageBox(page)
if err != nil {
return nil, err
}
c, err := newSemanticCompiler(r)
if err != nil {
return nil, err
}
c.page = &RasterPage{Width: box.W, Height: box.H, DPI: r.DPI}
if _, _, err := c.page.PixelSize(); err != nil {
return nil, err
}
if !r.TransparentBackground {
if err := c.fill(geometryRectangle(Box{W: box.W, H: box.H}), Paint{Kind: PaintSolid, Color: color.RGBA{255, 255, 255, 255}}, false, nil, IdentityMatrix, IdentityMatrix); err != nil {
return nil, err
}
}
if err := r.WalkPage(page, c); err != nil {
return nil, err
}
if r.OnPageText != nil {
r.OnPageText(page, &c.text)
}
return c.page, nil
}
// PageText 提取与GG页面绘制相同定位的原文,不读取图片像素
// 入参: r 渲染器, page 页面
// 返回: *PageText 页面文字, error 定位错误
func (GGBackend) PageText(r *Renderer, page *PageContent) (*PageText, error) {
if _, err := r.GetPageBox(page); err != nil {
return nil, err
}
c, err := newSemanticCompiler(r)
if err != nil {
return nil, err
}
c.textOnly = true
if err := r.WalkPage(page, c); err != nil {
return nil, err
}
return &c.text, nil
}
// MeasureObject 复用文字和图形编译规则收集范围,不分配页面像素
// 入参: r 渲染器, object 对象, options 度量上下文
// 返回: ObjectMeasurement 对象范围与轮廓, error 度量错误
func (GGBackend) MeasureObject(r *Renderer, object GraphicObject, options MeasureOptions) (ObjectMeasurement, error) {
c, err := newSemanticCompiler(r)
if err != nil {
return ObjectMeasurement{}, err
}
boundary, ctm := editorGeometry(object)
if _, err := creationBox(boundary); err != nil {
return ObjectMeasurement{}, err
}
if ctm != "" {
if _, err := creationNumbers(ctm, 6); err != nil {
return ObjectMeasurement{}, err
}
}
c.measure = true
c.contours = options.Contours
err = r.WalkObject(&object, RenderState{Defaults: options.Defaults, Parent: options.Parent, BoundaryInCTM: options.BoundaryInCTM, Clip: options.Clip}, c)
return c.measurement, err
}
+11 -55
View File
@@ -15,9 +15,6 @@
package ofdgo
import (
"fmt"
"path"
"github.com/tdewolff/canvas"
"github.com/tdewolff/font"
)
@@ -25,54 +22,8 @@ import (
// ResolveFont 按需加载字体资源,保留内嵌数据及外部字体的名称、样式和集合索引匹配
// 入参: r 渲染器, id 字体标识, exact 是否禁止无关回退
// 返回: ResolvedFont 字体与来源, error 字体不可用错误
func (CanvasBackend) ResolveFont(r *Renderer, id string, exact bool) (ResolvedFont, error) {
definition := r.Reader.fontCache[id]
if definition == nil && !r.Reader.fontResourcesRead && r.Reader.OFD != nil {
if _, err := r.Reader.Fonts(); err != nil {
return ResolvedFont{}, err
}
definition = r.Reader.fontCache[id]
}
if definition == nil {
if exact {
return ResolvedFont{}, fmt.Errorf("font %q not found", id)
}
return r.canvasFallbackFont(), nil
}
if definition.FontFile != "" {
data, err := r.Reader.FontData(id)
if err != nil {
return ResolvedFont{}, err
}
face, err := r.Reader.embeddedFontFace(*definition)
if err != nil {
return ResolvedFont{}, err
}
return ResolvedFont{Data: data, Source: path.Base(definition.FontFile), Face: face, Exact: true}, nil
}
style := canvasFontStyle(definition)
if source, family := r.fontSourceMatch(id, definition, exact); family != nil {
sfnt := family.Face(12, style).Font.SFNT
face := fontFaceInfo(sfnt.Tables["name"], source.face)
r.canvasState().fontSourceUsed[id] = source
return ResolvedFont{Data: fontSFNTData(sfnt), Source: source.name, Face: &face, Exact: source.exact}, nil
}
if !exact {
return r.canvasFallbackFont(), nil
}
return ResolvedFont{}, fmt.Errorf("font %q is unavailable", definition.FontName)
}
// canvasFallbackFont 按需加载默认阅读字体,不用于精确编辑匹配
// 返回: ResolvedFont 默认字体,未安装时数据为空
func (r *Renderer) canvasFallbackFont() ResolvedFont {
if r.canvasState().fontFamily == nil {
r.initCanvasFonts()
}
if r.canvasState().defaultFontLoaded {
return ResolvedFont{Data: fontSFNTData(r.canvasState().fontFamily.Face(12, canvas.FontRegular).Font.SFNT)}
}
return ResolvedFont{}
func (b CanvasBackend) ResolveFont(r *Renderer, id string, exact bool) (ResolvedFont, error) {
return r.resolveFontSource(b, id, exact)
}
// OpenFont 解析独立SFNT数据,度量与原有编辑排版保持一致
@@ -97,8 +48,13 @@ func (f canvasFontMetrics) Write() []byte {
return fontSFNTData(f.SFNT)
}
// initCanvasFonts 初始化Canvas默认字体
func (r *Renderer) initCanvasFonts() {
r.canvasState().fontFamily = canvas.NewFontFamily("default")
r.canvasState().defaultFontLoaded = r.loadDefaultFonts()
// GlyphOutline 返回基线原点、纵轴向下的字形轮廓
// 入参: glyph 字形编号, size 字号,单位为毫米
// 返回: GeometryPath 字形路径, error 字形解析错误
func (f canvasFontMetrics) GlyphOutline(glyph uint16, size float64) (GeometryPath, error) {
path := &canvas.Path{}
if err := f.GlyphPath(path, glyph, 0, 0, 0, size/float64(f.UnitsPerEm()), font.NoHinting); err != nil {
return nil, err
}
return *geometryFromCanvasPath(path), nil
}
+117
View File
@@ -0,0 +1,117 @@
// 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"
"math"
ggtext "github.com/gogpu/gg/text"
)
// ResolveFont 使用公共字体来源匹配,不创建Canvas字体
// 入参: r 渲染器, id 字体ID, exact 是否禁止无关回退
// 返回: ResolvedFont 字体及来源, error 解析错误
func (b GGBackend) ResolveFont(r *Renderer, id string, exact bool) (ResolvedFont, error) {
return r.resolveFontSource(b, id, exact)
}
// OpenFont 通过GG解析独立字体,保留原始SFNT编码
// 入参: data 字体数据
// 返回: FontMetrics 字体度量, error 解析错误
func (GGBackend) OpenFont(data []byte) (FontMetrics, error) {
data = bytes.Clone(data)
source, err := ggtext.NewFontSource(data)
if err != nil {
return nil, err
}
return &ggFontMetrics{font: source.Parsed(), data: data}, nil
}
// ggFontMetrics 适配GG设计单位度量与字形轮廓
type ggFontMetrics struct {
font ggtext.ParsedFont
data []byte
}
// UnitsPerEm 返回字体设计单位
// 返回: uint16 每字单位数
func (f *ggFontMetrics) UnitsPerEm() uint16 { return uint16(f.font.UnitsPerEm()) }
// NumGlyphs 返回字形数量
// 返回: uint16 字形数量
func (f *ggFontMetrics) NumGlyphs() uint16 { return uint16(f.font.NumGlyphs()) }
// GlyphIndex 返回字符对应的字形编号
// 入参: character 字符
// 返回: uint16 字形编号
func (f *ggFontMetrics) GlyphIndex(character rune) uint16 { return f.font.GlyphIndex(character) }
// GlyphAdvance 返回字形设计单位推进宽度
// 入参: glyph 字形编号
// 返回: uint16 推进宽度
func (f *ggFontMetrics) GlyphAdvance(glyph uint16) uint16 {
return uint16(math.Round(f.font.GlyphAdvance(glyph, float64(f.font.UnitsPerEm()))))
}
// VerticalMetrics 返回非负的上升、下降与行间隙
// 返回: uint16 上升高度, uint16 下降高度, uint16 行间隙
func (f *ggFontMetrics) VerticalMetrics() (uint16, uint16, uint16) {
m := f.font.Metrics(float64(f.font.UnitsPerEm()))
return uint16(math.Round(math.Max(0, m.Ascent))), uint16(math.Round(math.Max(0, -m.Descent))), uint16(math.Round(math.Max(0, m.LineGap)))
}
// Write 返回独立字体数据,不更新字体元信息
// 返回: []byte SFNT数据
func (f *ggFontMetrics) Write() []byte { return bytes.Clone(f.data) }
// GlyphOutline 返回基线原点、纵轴向下的字形路径
// 入参: glyph 字形编号, size 字号,单位为毫米
// 返回: GeometryPath 字形路径, error 轮廓解析错误
func (f *ggFontMetrics) GlyphOutline(glyph uint16, size float64) (GeometryPath, error) {
if !rasterPositive(size) || int(glyph) >= f.font.NumGlyphs() {
return nil, fmt.Errorf("invalid glyph or size")
}
outline, err := ggtext.NewOutlineExtractor().ExtractOutline(f.font, ggtext.GlyphID(glyph), size)
if err != nil || outline == nil {
return nil, err
}
result := make(GeometryPath, 0, len(outline.Segments)+1)
point := func(p ggtext.OutlinePoint) Point { return Point{X: float64(p.X), Y: float64(p.Y)} }
for _, s := range outline.Segments {
segment := GeometrySegment{End: point(s.Points[0])}
switch s.Op {
case ggtext.OutlineOpMoveTo:
if len(result) > 0 {
result = append(result, GeometrySegment{Verb: GeometryClose})
}
segment.Verb = GeometryMove
case ggtext.OutlineOpLineTo:
segment.Verb = GeometryLine
case ggtext.OutlineOpQuadTo:
segment.Verb, segment.Control1, segment.End = GeometryQuad, point(s.Points[0]), point(s.Points[1])
case ggtext.OutlineOpCubicTo:
segment.Verb, segment.Control1, segment.Control2, segment.End = GeometryCubic, point(s.Points[0]), point(s.Points[1]), point(s.Points[2])
default:
return nil, fmt.Errorf("unsupported GG outline command %d", s.Op)
}
result = append(result, segment)
}
if len(result) > 0 {
result = append(result, GeometrySegment{Verb: GeometryClose})
}
return result, nil
}
+2 -256
View File
@@ -14,51 +14,7 @@
package ofdgo
import (
"bytes"
"io"
"io/fs"
"os"
"path"
"path/filepath"
"runtime"
"sort"
"github.com/tdewolff/canvas"
)
// fontSourceKind 字体来源类型
type fontSourceKind uint8
const (
fontSourceFile fontSourceKind = iota
fontSourceFS
fontSourceSystem
)
// fontSource 字体来源
type fontSource struct {
kind fontSourceKind
index int
face int
name string
exact bool
priority int
}
// fontSourceKey 字体来源去重键
type fontSourceKey struct {
kind fontSourceKind
index int
face int
name string
}
// fontCacheKey 字体加载缓存键
type fontCacheKey struct {
fontSourceKey
style canvas.FontStyle
}
import "github.com/tdewolff/canvas"
// loadFont 加载字体
// 入参: fontID 字体ID
@@ -86,9 +42,7 @@ func (r *Renderer) loadFont(fontID string) *canvas.FontFamily {
r.renderError = err
return nil
}
if r.Reader.fontCache[fontID] != nil {
r.canvasState().fontMap[fontID] = ff
}
r.canvasState().fontMap[fontID] = ff
return ff
}
@@ -105,211 +59,3 @@ func canvasFontStyle(font *Font) canvas.FontStyle {
}
return style
}
// fontSources 获取字体来源列表
// 入参: fontID 字体ID, font OFD字体定义, style Canvas字体样式
// 返回: []fontSource 字体来源列表
func (r *Renderer) fontSources(fontID string, font *Font, style canvas.FontStyle) []fontSource {
if sources, ok := r.canvasState().fontSourceCache[fontID]; ok {
return sources
}
bold := style&canvas.FontBold != 0
italic := style&canvas.FontItalic != 0
names := []string{font.FontName, font.FamilyName}
sources := make([]fontSource, 0)
seen := make(map[fontSourceKey]bool)
for _, dir := range r.fontDirs {
for _, match := range r.matchFontFiles(dir, names, bold, italic) {
sources = appendFontSource(sources, seen, fontSource{kind: fontSourceFile, name: match.name, face: match.face, exact: true, priority: min(match.priority, 0)})
}
}
for index := range r.fontFS {
for _, match := range r.matchFontFS(index, names, bold, italic) {
sources = appendFontSource(sources, seen, fontSource{kind: fontSourceFS, index: index, name: match.name, face: match.face, exact: true, priority: min(match.priority, 0)})
}
}
sortFontSources(sources)
systemStart := len(sources)
if canLoadSystemFonts() {
for _, dir := range systemFontDirs() {
for _, match := range r.matchFontFiles(dir, names, bold, italic) {
sources = appendFontSource(sources, seen, fontSource{kind: fontSourceFile, name: match.name, face: match.face, exact: true, priority: min(match.priority, 0)})
}
}
for _, name := range fontSystemNames(names...) {
source := fontSource{kind: fontSourceSystem, name: name, exact: true}
for i, requested := range names {
if fontNormalizeName(name) == fontNormalizeName(requested) {
source.priority = (i - len(names)) * 2
break
}
}
sources = appendFontSource(sources, seen, source)
}
}
sortFontSources(sources[systemStart:])
for index := range r.fontFS {
for _, match := range r.matchFontFS(index, nil, bold, italic) {
sources = appendFontSource(sources, seen, fontSource{kind: fontSourceFS, index: index, name: match.name, face: match.face})
}
}
for index, fsys := range r.fontFS {
names, _ := fs.Glob(fsys, "*")
for _, name := range names {
sources = appendFontSource(sources, seen, fontSource{kind: fontSourceFS, index: index, name: name})
}
}
r.canvasState().fontSourceCache[fontID] = sources
return sources
}
// sortFontSources 将完整名称匹配排在回退候选之前
// 入参: sources 字体来源列表
func sortFontSources(sources []fontSource) {
sort.SliceStable(sources, func(i, j int) bool {
return sources[i].priority < sources[j].priority
})
}
// appendFontSource 追加字体来源
// 入参: sources 字体来源列表, seen 去重映射, source 字体来源
// 返回: []fontSource 字体来源列表
func appendFontSource(sources []fontSource, seen map[fontSourceKey]bool, source fontSource) []fontSource {
if source.name == "" {
return sources
}
key := fontSourceKey{kind: source.kind, index: source.index, name: source.name, face: source.face}
if seen[key] {
return sources
}
seen[key] = true
return append(sources, source)
}
// fontSourceMatch 获取匹配的字体来源
// 入参: fontID 字体ID, font OFD字体定义, exact 是否禁止无关回退
// 返回: fontSource 字体来源, *canvas.FontFamily 已加载的字体族,未匹配时为空
func (r *Renderer) fontSourceMatch(fontID string, font *Font, exact bool) (fontSource, *canvas.FontFamily) {
if source, ok := r.canvasState().fontSourceUsed[fontID]; ok && (!exact || source.exact) && r.canvasState().fontMap[fontID] != nil {
return source, r.canvasState().fontMap[fontID]
}
style := canvasFontStyle(font)
for _, source := range r.fontSources(fontID, font, style) {
if exact && !source.exact {
continue
}
key := fontCacheKey{fontSourceKey: fontSourceKey{kind: source.kind, index: source.index, name: source.name, face: source.face}, style: style}
if cached, ok := r.canvasState().fontCache[key]; ok {
if cached != nil {
return source, cached
}
continue
}
family := canvas.NewFontFamily(font.FontName)
if loaded := r.loadFontSource(family, source, style); loaded != nil {
return source, loaded
}
}
return fontSource{}, nil
}
// loadFontSource 加载字体来源
// 入参: family 字体族, source 字体来源, style Canvas字体样式
// 返回: *canvas.FontFamily 字体族
func (r *Renderer) loadFontSource(family *canvas.FontFamily, source fontSource, style canvas.FontStyle) *canvas.FontFamily {
key := fontCacheKey{fontSourceKey: fontSourceKey{kind: source.kind, index: source.index, name: source.name, face: source.face}, style: style}
if cached, ok := r.canvasState().fontCache[key]; ok {
return cached
}
var err error
var data []byte
switch source.kind {
case fontSourceFile:
data, err = os.ReadFile(source.name)
case fontSourceFS:
data, err = fs.ReadFile(r.fontFS[source.index], source.name)
case fontSourceSystem:
err = family.LoadSystemFont(source.name, style)
}
if err == nil && source.kind != fontSourceSystem {
if bytes.HasPrefix(data, []byte("ttcf")) {
data, err = extractCollectionFont(data, source.face)
}
if err == nil {
err = family.LoadFont(data, 0, style)
}
}
if err != nil {
r.canvasState().fontCache[key] = nil
return nil
}
r.canvasState().fontCache[key] = family
return family
}
// matchFontFiles 查找字体文件
// 入参: dir 字体目录, names 字体名称, bold 是否粗体, italic 是否斜体
// 返回: []fontFileMatch 字体匹配列表
func (r *Renderer) matchFontFiles(dir string, names []string, bold, italic bool) []fontFileMatch {
candidates, ok := r.canvasState().fontDirCandidates[dir]
if !ok {
files, _ := filepath.Glob(filepath.Join(dir, "*"))
candidates = fontFileCandidates(files, filepath.Base)
for i, candidate := range candidates {
file, err := os.Open(candidate.name)
if err != nil {
continue
}
candidates = appendFontFileNames(candidates, i, fontFileNames(file))
file.Close()
}
r.canvasState().fontDirCandidates[dir] = candidates
}
return fontFileMatches(candidates, names, bold, italic)
}
// matchFontFS 匹配字体文件系统中的字体名称
// 入参: index 字体文件系统索引, names 字体名称, bold 是否粗体, italic 是否斜体
// 返回: []fontFileMatch 字体匹配列表
func (r *Renderer) matchFontFS(index int, names []string, bold, italic bool) []fontFileMatch {
fsys := r.fontFS[index]
if fonts, ok := fsys.(*FontFS); ok {
return fonts.matchStyle(names, bold, italic)
}
candidates, ok := r.canvasState().fontFSCandidates[index]
if !ok {
files, _ := fs.Glob(fsys, "*")
candidates = fontFileCandidates(files, path.Base)
for i, candidate := range candidates {
file, err := fsys.Open(candidate.name)
if err != nil {
continue
}
reader, ok := file.(io.ReaderAt)
if !ok {
data, _ := io.ReadAll(file)
reader = bytes.NewReader(data)
}
candidates = appendFontFileNames(candidates, i, fontFileNames(reader))
file.Close()
}
r.canvasState().fontFSCandidates[index] = candidates
}
return fontFileMatches(candidates, names, bold, italic)
}
// systemFontDirs 获取系统字体目录
// 返回: []string 字体目录列表
func systemFontDirs() []string {
switch runtime.GOOS {
case "android":
return []string{"/product/fonts", "/system/fonts"}
case "darwin":
return []string{`/Library/Fonts`}
case "linux":
return []string{`/usr/share/fonts`}
case "windows":
return []string{`C:\Windows\Fonts`}
}
return nil
}
+22
View File
@@ -21,6 +21,28 @@ import (
"github.com/tdewolff/canvas"
)
// canvasNativeStroke 判断描边是否仍由Canvas提供,组合几何不改变矢量描边编码
// 入参: geometry 几何后端
// 返回: bool 是否采用原生描边
func canvasNativeStroke(geometry GeometryBackend) bool {
if combined, ok := geometry.(GGGeometryBackend); ok {
return canvasNativeStroke(combined.GeometryBackend)
}
_, ok := geometry.(CanvasBackend)
return ok
}
// Curves 在输出边界转换椭圆弧,其他曲线保持原样
// 入参: path 页面路径
// 返回: GeometryPath 贝塞尔路径, error 路径错误
func (CanvasBackend) Curves(path GeometryPath) (GeometryPath, error) {
p, err := geometryToCanvasPath(&path)
if err != nil {
return nil, err
}
return *geometryFromCanvasPath(p.ReplaceArcs()), nil
}
// canvasStrokeOptions 转换Canvas描边样式,虚线由调用方传入
// 入参: width 描边宽度, cap 线帽, join 连接, tolerance 展开误差
// 返回: StrokeOptions 公共描边样式
+135
View File
@@ -0,0 +1,135 @@
// 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"
"github.com/gogpu/gg"
)
// GGGeometryBackend 使用GG处理贝塞尔边界和变换,其他运算由显式配置的几何后端承担
// GeometryBackend不可为空,椭圆弧保持精确表示,不为接入GG而预先离散化
type GGGeometryBackend struct{ GeometryBackend }
// Name 返回实际几何提供者组合
// 返回: string 后端组合标识
func (b GGGeometryBackend) Name() string { return "gg+" + backendName(b.GeometryBackend) }
// Curves 通过显式配置的曲线适配器展开椭圆弧
// 入参: path 页面路径
// 返回: GeometryPath 贝塞尔路径, error 能力或路径错误
func (b GGGeometryBackend) Curves(path GeometryPath) (GeometryPath, error) {
curves, ok := b.GeometryBackend.(GeometryCurves)
if !ok {
return nil, fmt.Errorf("geometry curves: %w", ErrBackendUnavailable)
}
return curves.Curves(path)
}
// Bounds 使用GG计算贝塞尔曲线极值,椭圆弧交给配置的几何后端
// 入参: path 页面路径
// 返回: Box 精确范围, error 路径或能力错误
func (b GGGeometryBackend) Bounds(path GeometryPath) (Box, error) {
for _, s := range path {
if s.Verb == GeometryArc {
if b.GeometryBackend == nil {
return Box{}, ErrBackendUnavailable
}
return b.GeometryBackend.Bounds(path)
}
}
p, err := ggGeometryPath(path)
if err != nil {
return Box{}, err
}
if len(path) == 0 {
return Box{}, nil
}
box := p.BoundingBox()
return Box{X: box.Min.X, Y: box.Min.Y, W: box.Max.X - box.Min.X, H: box.Max.Y - box.Min.Y}, nil
}
// Transform 使用GG变换控制点,椭圆弧交给配置的几何后端保持弧线参数
// 入参: path 页面路径, matrix 页面变换
// 返回: GeometryPath 新路径, error 路径或变换错误
func (b GGGeometryBackend) Transform(path GeometryPath, matrix Matrix) (GeometryPath, error) {
for _, v := range matrix.Values() {
if !finite(v) {
return nil, fmt.Errorf("invalid geometry transform")
}
}
for _, s := range path {
if s.Verb == GeometryArc {
if b.GeometryBackend == nil {
return nil, ErrBackendUnavailable
}
return b.GeometryBackend.Transform(path, matrix)
}
}
p, err := ggGeometryPath(path)
if err != nil {
return nil, err
}
return geometryFromGG(p.Transform(gg.Matrix{A: matrix.a, B: matrix.c, C: matrix.e, D: matrix.b, E: matrix.d, F: matrix.f})), nil
}
// ggGeometryPath 转换贝塞尔路径,不舍入控制点或静默略过指令
// 入参: path 库路径
// 返回: *gg.Path GG路径, error 不支持的路径指令
func ggGeometryPath(path GeometryPath) (*gg.Path, error) {
p := gg.NewPath()
for _, s := range path {
switch s.Verb {
case GeometryMove:
p.MoveTo(s.End.X, s.End.Y)
case GeometryLine:
p.LineTo(s.End.X, s.End.Y)
case GeometryQuad:
p.QuadraticTo(s.Control1.X, s.Control1.Y, s.End.X, s.End.Y)
case GeometryCubic:
p.CubicTo(s.Control1.X, s.Control1.Y, s.Control2.X, s.Control2.Y, s.End.X, s.End.Y)
case GeometryClose:
p.Close()
default:
return nil, fmt.Errorf("unsupported GG geometry command %d", s.Verb)
}
}
return p, nil
}
// geometryFromGG 返回库自有路径,不保留GG对象
// 入参: path GG路径
// 返回: GeometryPath 库路径
func geometryFromGG(path *gg.Path) GeometryPath {
var result GeometryPath
path.Iterate(func(verb gg.PathVerb, p []float64) {
s := GeometrySegment{}
switch verb {
case gg.MoveTo:
s.Verb, s.End = GeometryMove, Point{p[0], p[1]}
case gg.LineTo:
s.Verb, s.End = GeometryLine, Point{p[0], p[1]}
case gg.QuadTo:
s.Verb, s.Control1, s.End = GeometryQuad, Point{p[0], p[1]}, Point{p[2], p[3]}
case gg.CubicTo:
s.Verb, s.Control1, s.Control2, s.End = GeometryCubic, Point{p[0], p[1]}, Point{p[2], p[3]}, Point{p[4], p[5]}
case gg.Close:
s.Verb = GeometryClose
}
result = append(result, s)
})
return result
}
+4 -2
View File
@@ -22,7 +22,7 @@ import (
"github.com/gogpu/gg"
)
// GGBackend 不注册全局GPU设备或回退到其他绘图库的CPU后端
// GGBackend 提供字体、页面编译和CPU绘制,不注册全局GPU设备或隐式切换后端
type GGBackend struct{}
// Name 返回后端标识
@@ -43,7 +43,9 @@ func (GGBackend) Render(page *RasterPage) (image.Image, error) {
for i, cmd := range page.Commands {
m := page.PixelTransform(cmd.Transform, h)
if cmd.Image != nil {
drawRasterImage(img, cmd.Image, m)
if err := drawRasterImageCommand(GGBackend{}, page, img, imagePixelSource(cmd.Image), cmd); err != nil {
return nil, err
}
continue
}
path := gg.NewPath()
+3 -22
View File
@@ -121,30 +121,11 @@ func (r *Renderer) canvasEncodedImage(img image.Image) image.Image {
// renderImageBorder 渲染图像边框
// 入参: ctx 画布上下文, obj 图片对象, box 图像边界, pageH 页面高度, parentCTM 父级CTM, boundaryInCTM 边界是否参与父级CTM, clipPath 裁剪路径
func (r *Renderer) renderImageBorder(ctx *canvas.Context, obj ImageObject, box Box, pageH float64, parentCTM *Matrix, boundaryInCTM bool, clipPath *canvas.Path) {
border := obj.Border
width := defaultPathLineWidth
if border.LineWidth != nil {
width = *border.LineWidth
}
if width == 0 {
border := imageBorderObject(obj, box)
if border.LineWidth == 0 {
return
}
w, h := box.W, box.H
data := fmt.Sprintf("M 0 0 L %g 0 L %g %g L 0 %g C", w, w, h, h)
rx, ry := min(border.HorizonalCornerRadius, w/2), min(border.VerticalCornerRadius, h/2)
if rx > 0 && ry > 0 {
data = fmt.Sprintf("M 0 %g A %g %g 0 0 1 %g 0 L %g 0 A %g %g 0 0 1 %g %g L %g %g A %g %g 0 0 1 %g %g L %g %g A %g %g 0 0 1 0 %g C",
ry, rx, ry, rx, w-rx, rx, ry, w, ry, w, h-ry, rx, ry, w-rx, h, rx, h, rx, ry, h-ry)
}
r.renderPath(ctx, PathObject{
Boundary: obj.Boundary,
LineWidth: width,
DashOffset: &border.DashOffset,
DashPattern: border.DashPattern,
Alpha: obj.Alpha,
StrokeColor: border.BorderColor,
AbbreviatedData: data,
}, pageH, nil, parentCTM, boundaryInCTM, clipPath)
r.renderPath(ctx, border, pageH, nil, parentCTM, boundaryInCTM, clipPath)
}
// imageWithClip 应用图片裁剪区域
+23 -35
View File
@@ -173,6 +173,22 @@ func (r *Renderer) renderPath(ctx *canvas.Context, obj PathObject, pageH float64
if r.textOnly {
return
}
if bounds, ok := ctx.Renderer.(*boundsRenderer); ok {
compiler, err := newSemanticCompiler(r)
if err != nil {
r.renderError = err
return
}
compiler.measure, compiler.contours = true, bounds.collect
err = compiler.pathObject(obj, RenderState{Defaults: defaults, Parent: parentCTM, BoundaryInCTM: boundaryInCTM, Clip: geometryFromCanvasPath(parentClip)})
if err != nil {
r.renderError = err
return
}
bounds.box = unionTextBox(bounds.box, compiler.measurement.Bounds)
bounds.contours = append(bounds.contours, compiler.measurement.Contours...)
return
}
if obj.Visible != nil && !*obj.Visible {
return
}
@@ -280,8 +296,7 @@ func (r *Renderer) renderPath(ctx *canvas.Context, obj PathObject, pageH float64
}
strokeClip := intersectClipPath(clipPath, shadingClip)
_, repeat := strokePaint.(*repeatAxialGradient)
_, nativeGeometry := r.backends.Geometry.(CanvasBackend)
if strokeClip != nil || strokeView != canvas.Identity || repeat || style.strokePattern != nil || !nativeGeometry {
if strokeClip != nil || strokeView != canvas.Identity || repeat || style.strokePattern != nil || !canvasNativeStroke(r.backends.Geometry) {
sp = sp.Copy()
sp = r.strokeCanvasPath(sp, style.lineWidth, style.lineCap, style.lineJoin)
sp = applyClipPath(sp, strokeClip)
@@ -458,41 +473,14 @@ func (r *Renderer) buildPath(obj PathObject, pageH float64, ctm Matrix, boundary
// 入参: obj 路径对象, pageH 页面高度, ctm 变换矩阵, bx 边界X坐标, by 边界Y坐标
// 返回: *canvas.Path 路径对象
func (r *Renderer) buildTinyFillRectPath(obj PathObject, pageH float64, ctm Matrix, bx, by float64) *canvas.Path {
if obj.Fill == nil || !*obj.Fill || obj.Stroke == nil || *obj.Stroke {
path := tinyFillRectPath(obj, TranslationMatrix(bx, by).Multiply(ctm))
if path == nil {
return nil
}
box, err := ParseBox(obj.Boundary)
if err != nil || box.W <= 0 || box.H <= 0 || box.W > 0.6 || box.H > 0.6 {
p, err := geometryToCanvasPath(&path)
if err != nil {
r.renderError = err
return nil
}
tokens := strings.Fields(obj.AbbreviatedData)
if len(tokens) != 11 || tokens[0] != "M" || tokens[3] != "L" || tokens[6] != "L" || tokens[9] != "L" || tokens[10] != "C" {
return nil
}
points := make([][2]float64, 0, 4)
for i := 1; i < 10; i += 3 {
x, errX := strconv.ParseFloat(tokens[i], 64)
y, errY := strconv.ParseFloat(tokens[i+1], 64)
if errX != nil || errY != nil {
return nil
}
tx, ty := ctm.Transform(x, y)
points = append(points, [2]float64{tx + bx, pageH - (ty + by)})
}
minX, maxX := points[0][0], points[0][0]
minY, maxY := points[0][1], points[0][1]
for _, point := range points[1:] {
minX = math.Min(minX, point[0])
maxX = math.Max(maxX, point[0])
minY = math.Min(minY, point[1])
maxY = math.Max(maxY, point[1])
}
expand := math.Min(box.W, box.H) * 0.08
p := &canvas.Path{}
p.MoveTo(minX-expand, minY-expand)
p.LineTo(maxX+expand, minY-expand)
p.LineTo(maxX+expand, maxY+expand)
p.LineTo(minX-expand, maxY+expand)
p.Close()
return p
return p.Translate(0, pageH)
}
-12
View File
@@ -287,15 +287,3 @@ func reflectShdGrad(gradient canvas.Grad, period, lo, hi float64) canvas.Grad {
}
return append(result, canvas.Stop{Offset: 1, Color: gradient.At(reflectShdPosition(hi / period))})
}
// reflectShdPosition 获取镜像周期内的位置
// 入参: position 渐变位置
// 返回: float64 周期内的位置
func reflectShdPosition(position float64) float64 {
index := math.Floor(position)
position -= index
if math.Mod(index, 2) != 0 {
return 1 - position
}
return position
}
+9 -16
View File
@@ -23,15 +23,8 @@ import (
// 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
}
@@ -43,16 +36,16 @@ func (r *Renderer) canvasState() *canvasBackendState {
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),
images: make(map[*EncodedImage]image.Image),
}
state.resetFonts()
r.backendStates[key] = state
return state
}
// resetFonts 清除Canvas字体及字形缓存,保留编码图片适配对象
func (s *canvasBackendState) resetFonts() {
s.fontMap = make(map[string]*canvas.FontFamily)
s.svgFontCache = make(map[*canvas.Font]SVGFont)
s.textGlyphPathCache = make(map[textGlyphPathCacheKey]textGlyphPathCacheValue)
}