mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 08:46:55 +08:00
This commit is contained in:
1 parent
c15547f0d2
commit
f44d0d8a60
30 files changed
+2071
-459
No files matched your search
@@ -3134,7 +3134,7 @@ function displayMode() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderBackendLabel(name) {
|
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() {
|
function updateRenderBackend() {
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ func NewRenderer(reader *Reader, opts ...RendererOption) *Renderer {
|
|||||||
templatePageCache: make(map[string]*PageContent),
|
templatePageCache: make(map[string]*PageContent),
|
||||||
imageCache: make(map[string]image.Image),
|
imageCache: make(map[string]image.Image),
|
||||||
backends: defaultRenderBackends(),
|
backends: defaultRenderBackends(),
|
||||||
|
backendStates: make(map[any]any),
|
||||||
}
|
}
|
||||||
r.resetFontCache()
|
r.resetFontCache()
|
||||||
for _, opt := range opts {
|
for _, opt := range opts {
|
||||||
|
|||||||
+20
-2
@@ -19,6 +19,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"image"
|
"image"
|
||||||
"io"
|
"io"
|
||||||
|
"reflect"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ErrBackendUnavailable 表示未配置所需后端,不会自动切换其他实现
|
// ErrBackendUnavailable 表示未配置所需后端,不会自动切换其他实现
|
||||||
@@ -124,7 +125,7 @@ type BackendInfo struct {
|
|||||||
EPS string `json:"eps"`
|
EPS string `json:"eps"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewRenderBackends 创建内置后端组合,gg仅替换光栅绘制,其他能力通过Info明确报告
|
// NewRenderBackends 创建内置后端组合,各项能力通过Info报告实际提供者
|
||||||
// 入参: name canvas或gg
|
// 入参: name canvas或gg
|
||||||
// 返回: RenderBackends 后端组合, error 未知后端错误
|
// 返回: RenderBackends 后端组合, error 未知后端错误
|
||||||
func NewRenderBackends(name string) (RenderBackends, error) {
|
func NewRenderBackends(name string) (RenderBackends, error) {
|
||||||
@@ -132,6 +133,9 @@ func NewRenderBackends(name string) (RenderBackends, error) {
|
|||||||
switch name {
|
switch name {
|
||||||
case "canvas":
|
case "canvas":
|
||||||
case "gg":
|
case "gg":
|
||||||
|
backends.Fonts = GGBackend{}
|
||||||
|
backends.Geometry = GGGeometryBackend{GeometryBackend: backends.Geometry}
|
||||||
|
backends.Compiler = GGBackend{}
|
||||||
backends.Raster = GGBackend{}
|
backends.Raster = GGBackend{}
|
||||||
default:
|
default:
|
||||||
return RenderBackends{}, fmt.Errorf("unknown render backend %q", name)
|
return RenderBackends{}, fmt.Errorf("unknown render backend %q", name)
|
||||||
@@ -178,10 +182,24 @@ func backendName(backend Backend) string {
|
|||||||
// 返回: RendererOption 渲染选项
|
// 返回: RendererOption 渲染选项
|
||||||
func WithRenderBackends(backends RenderBackends) RendererOption {
|
func WithRenderBackends(backends RenderBackends) RendererOption {
|
||||||
return func(r *Renderer) {
|
return func(r *Renderer) {
|
||||||
|
fontsChanged := !sameBackend(r.backends.Fonts, backends.Fonts)
|
||||||
r.backends = backends
|
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应用
|
// Backends 返回当前后端配置副本,可修改后通过WithRenderBackends应用
|
||||||
// 返回: RenderBackends 后端组合
|
// 返回: RenderBackends 后端组合
|
||||||
|
|||||||
+7
-2
@@ -81,13 +81,18 @@ func (e *Editor) SetPageCompiler(compiler PageCompiler) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetRenderBackends 设置完整后端组合并重置字体度量,不改变文档或撤销历史
|
// SetRenderBackends 设置完整后端组合,字体能力变化时更新度量,不改变文档或撤销历史
|
||||||
// 入参: backends 后端组合,未配置的能力不会沿用默认实现
|
// 入参: backends 后端组合,未配置的能力不会沿用默认实现
|
||||||
func (e *Editor) SetRenderBackends(backends RenderBackends) {
|
func (e *Editor) SetRenderBackends(backends RenderBackends) {
|
||||||
|
fontsChanged := !sameBackend(e.backends.Fonts, backends.Fonts)
|
||||||
e.backends = backends
|
e.backends = backends
|
||||||
e.fontRenderer = nil
|
if e.fontRenderer != nil {
|
||||||
|
WithRenderBackends(backends)(e.fontRenderer)
|
||||||
|
}
|
||||||
|
if fontsChanged {
|
||||||
e.fontMetrics = make(map[string]FontMetrics)
|
e.fontMetrics = make(map[string]FontMetrics)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Backends 返回编辑器后端配置副本
|
// Backends 返回编辑器后端配置副本
|
||||||
// 返回: RenderBackends 后端组合
|
// 返回: RenderBackends 后端组合
|
||||||
|
|||||||
+25
-4
@@ -28,6 +28,13 @@ type FontMetrics interface {
|
|||||||
Write() []byte
|
Write() []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FontOutlines 提供基线原点、纵轴向下的字形轮廓,不绑定字体实现
|
||||||
|
// GlyphOutline返回只读路径,字号与路径单位为毫米
|
||||||
|
type FontOutlines interface {
|
||||||
|
FontMetrics
|
||||||
|
GlyphOutline(glyph uint16, size float64) (GeometryPath, error)
|
||||||
|
}
|
||||||
|
|
||||||
// ResolvedFont 保存字体来源和只读字体数据,不将外部字体写入文档
|
// ResolvedFont 保存字体来源和只读字体数据,不将外部字体写入文档
|
||||||
type ResolvedFont struct {
|
type ResolvedFont struct {
|
||||||
Data []byte
|
Data []byte
|
||||||
@@ -57,11 +64,27 @@ type resolvedFontResult struct {
|
|||||||
err error
|
err error
|
||||||
}
|
}
|
||||||
|
|
||||||
// resetFontCache 重置字体解析及后端私有缓存,不预先创建任何绘图库对象
|
// resetFontCache 重置字体解析及字形缓存,保留图片和非字体后端状态
|
||||||
func (r *Renderer) resetFontCache() {
|
func (r *Renderer) resetFontCache() {
|
||||||
|
r.fontSourcesCache = newFontSourceCache()
|
||||||
|
r.resetFontBackendCache()
|
||||||
|
}
|
||||||
|
|
||||||
|
// resetFontBackendCache 清理后端字体对象,保留字体文件和图片缓存
|
||||||
|
func (r *Renderer) resetFontBackendCache() {
|
||||||
r.preparedFonts = make(map[string]*PreparedFont)
|
r.preparedFonts = make(map[string]*PreparedFont)
|
||||||
r.resolvedFonts = make(map[resolvedFontKey]resolvedFontResult)
|
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 使用配置的字体后端解析资源,保留精确匹配与回退的区别
|
// 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
|
return prepared, nil
|
||||||
}
|
}
|
||||||
@@ -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
|
package ofdgo
|
||||||
|
|
||||||
import "github.com/tdewolff/canvas"
|
// canLoadSystemFonts 判断是否可以读取系统字体
|
||||||
|
// 返回: bool 是否可以读取
|
||||||
// loadDefaultFonts 加载默认字体
|
func canLoadSystemFonts() bool { return true }
|
||||||
// 返回: 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
|
|
||||||
}
|
|
||||||
@@ -16,14 +16,6 @@
|
|||||||
|
|
||||||
package ofdgo
|
package ofdgo
|
||||||
|
|
||||||
// loadDefaultFonts 加载默认字体
|
// canLoadSystemFonts 判断是否可以读取系统字体
|
||||||
// 返回: bool 是否加载成功
|
// 返回: bool 是否可以读取
|
||||||
func (r *Renderer) loadDefaultFonts() bool {
|
func canLoadSystemFonts() bool { return false }
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// canLoadSystemFonts 判断是否可以加载系统字体
|
|
||||||
// 返回: bool 是否可以加载系统字体
|
|
||||||
func canLoadSystemFonts() bool {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
@@ -49,6 +49,11 @@ type GeometryBackend interface {
|
|||||||
Clip(renderer *Renderer, clips *Clips, matrix Matrix, parent *GeometryPath) (*GeometryPath, error)
|
Clip(renderer *Renderer, clips *Clips, matrix Matrix, parent *GeometryPath) (*GeometryPath, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GeometryCurves 将椭圆弧转换为光栅后端可消费的贝塞尔曲线,不改变源路径
|
||||||
|
type GeometryCurves interface {
|
||||||
|
Curves(path GeometryPath) (GeometryPath, error)
|
||||||
|
}
|
||||||
|
|
||||||
// Geometry 返回当前几何后端,不隐式恢复默认实现
|
// Geometry 返回当前几何后端,不隐式恢复默认实现
|
||||||
// 返回: GeometryBackend 几何后端, error 未配置错误
|
// 返回: GeometryBackend 几何后端, error 未配置错误
|
||||||
func (r *Renderer) Geometry() (GeometryBackend, error) {
|
func (r *Renderer) Geometry() (GeometryBackend, error) {
|
||||||
|
|||||||
@@ -23,6 +23,30 @@ import (
|
|||||||
// 坐标以左上角为原点,单位为毫米,子路径从Move开始,Close回到当前子路径起点
|
// 坐标以左上角为原点,单位为毫米,子路径从Move开始,Close回到当前子路径起点
|
||||||
type GeometryPath []GeometrySegment
|
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 表示几何路径指令
|
// GeometryVerb 表示几何路径指令
|
||||||
type GeometryVerb uint8
|
type GeometryVerb uint8
|
||||||
|
|
||||||
|
|||||||
+33
-2
@@ -56,13 +56,14 @@ type RasterSegment struct {
|
|||||||
|
|
||||||
// RasterCommand 填充路径或绘制图像,Image非空时为图像指令
|
// RasterCommand 填充路径或绘制图像,Image非空时为图像指令
|
||||||
// Transform将局部坐标映射到页面,图像局部坐标与image.Image的像素坐标一致
|
// Transform将局部坐标映射到页面,图像局部坐标与image.Image的像素坐标一致
|
||||||
// 字形和描边由页面编译器生成轮廓,裁剪与底纹在编译时解析
|
// 字形和描边由页面编译器生成轮廓,Clip为页面坐标图像裁剪,nil表示不裁剪
|
||||||
type RasterCommand struct {
|
type RasterCommand struct {
|
||||||
Path []RasterSegment
|
Path []RasterSegment
|
||||||
Paint RasterPaint
|
Paint RasterPaint
|
||||||
EvenOdd bool
|
EvenOdd bool
|
||||||
Transform RasterMatrix
|
Transform RasterMatrix
|
||||||
Image image.Image
|
Image image.Image
|
||||||
|
Clip []RasterSegment
|
||||||
}
|
}
|
||||||
|
|
||||||
// RasterPaint 预乘RGBA纯色或渐变,Gradient非空时忽略Color
|
// RasterPaint 预乘RGBA纯色或渐变,Gradient非空时忽略Color
|
||||||
@@ -79,12 +80,20 @@ const (
|
|||||||
RasterRadial
|
RasterRadial
|
||||||
)
|
)
|
||||||
|
|
||||||
// RasterGradient 局部坐标下的线性或双圆径向渐变,区间外延续边界颜色
|
// RasterGradient 局部坐标下的线性或双圆径向渐变,Spread为空时延续边界颜色
|
||||||
type RasterGradient struct {
|
type RasterGradient struct {
|
||||||
Kind RasterGradientKind
|
Kind RasterGradientKind
|
||||||
Start, End RasterPoint
|
Start, End RasterPoint
|
||||||
R0, R1 float64
|
R0, R1 float64
|
||||||
Stops []ColorStop
|
Stops []ColorStop
|
||||||
|
Spread *RasterSpread
|
||||||
|
}
|
||||||
|
|
||||||
|
// RasterSpread 保存OFD渐变延伸和周期,nil表示连续延伸
|
||||||
|
type RasterSpread struct {
|
||||||
|
Extend int
|
||||||
|
MapType string
|
||||||
|
Period float64
|
||||||
}
|
}
|
||||||
|
|
||||||
// PixelSize 检查页面尺寸并计算目标像素大小
|
// PixelSize 检查页面尺寸并计算目标像素大小
|
||||||
@@ -135,9 +144,31 @@ func (g *RasterGradient) At(x, y float64) color.RGBA {
|
|||||||
dr := g.R1 - g.R0
|
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
|
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)
|
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 {
|
} else if length := dx*dx + dy*dy; length > 0 {
|
||||||
t = (px*dx + py*dy) / length
|
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)
|
return g.colorAt(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ package ofdgo
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"image"
|
"image"
|
||||||
|
"image/color"
|
||||||
|
|
||||||
"golang.org/x/image/draw"
|
"golang.org/x/image/draw"
|
||||||
"golang.org/x/image/math/f64"
|
"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)
|
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 将页面毫米变换转换为像素变换,保留尺寸取整后的原点位置
|
// PixelTransform 将页面毫米变换转换为像素变换,保留尺寸取整后的原点位置
|
||||||
// 入参: m 局部到页面变换, height 目标像素高度
|
// 入参: m 局部到页面变换, height 目标像素高度
|
||||||
// 返回: RasterMatrix 局部到像素变换
|
// 返回: RasterMatrix 局部到像素变换
|
||||||
|
|||||||
@@ -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})
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 = ©
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -43,6 +43,9 @@ type Renderer struct {
|
|||||||
backendStates map[any]any
|
backendStates map[any]any
|
||||||
resolvedFonts map[resolvedFontKey]resolvedFontResult
|
resolvedFonts map[resolvedFontKey]resolvedFontResult
|
||||||
preparedFonts map[string]*PreparedFont
|
preparedFonts map[string]*PreparedFont
|
||||||
|
fontSourcesCache *fontSourceCache
|
||||||
|
fontMetrics map[*PreparedFont]FontMetrics
|
||||||
|
glyphOutlines map[glyphOutlineKey]GeometryPath
|
||||||
renderError error
|
renderError error
|
||||||
TransparentBackground bool
|
TransparentBackground bool
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,7 +58,9 @@ func (b CanvasBackend) Render(page *RasterPage) (image.Image, error) {
|
|||||||
}
|
}
|
||||||
source = converted
|
source = converted
|
||||||
}
|
}
|
||||||
drawRasterImage(img, source, page.PixelTransform(cmd.Transform, h))
|
if err := drawRasterImageCommand(b, page, img, source, cmd); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
p := &canvas.Path{}
|
p := &canvas.Path{}
|
||||||
@@ -93,6 +95,9 @@ func (b CanvasBackend) Render(page *RasterPage) (image.Image, error) {
|
|||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("canvas command %d: unsupported gradient %d", i, g.Kind)
|
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}
|
style := canvas.Style{Fill: paint, FillRule: canvas.NonZero}
|
||||||
if cmd.EvenOdd {
|
if cmd.EvenOdd {
|
||||||
@@ -104,3 +109,23 @@ func (b CanvasBackend) Render(page *RasterPage) (image.Image, error) {
|
|||||||
r.Close()
|
r.Close()
|
||||||
return img, nil
|
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{©}
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@
|
|||||||
package ofdgo
|
package ofdgo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"image"
|
"image"
|
||||||
|
|
||||||
"github.com/tdewolff/canvas"
|
"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
|
// 入参: 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 {
|
path, err := r.objectGeometryClip(clips, fmt.Sprintf("%g %g 0 0", bx, by), objectCTM, RenderState{Parent: parentCTM, BoundaryInCTM: boundaryInCTM})
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if !boundaryInCTM && parentCTM != nil {
|
|
||||||
objectCTM = parentCTM.Multiply(objectCTM)
|
|
||||||
}
|
|
||||||
if clips.TransFlag != nil && !*clips.TransFlag {
|
|
||||||
copy := *clips
|
|
||||||
copy.TransFlag = nil
|
|
||||||
clips = ©
|
|
||||||
objectCTM = NewMatrix("")
|
|
||||||
}
|
|
||||||
matrix := TranslationMatrix(bx, by).Multiply(objectCTM)
|
|
||||||
if boundaryInCTM && parentCTM != nil {
|
|
||||||
matrix = parentCTM.Multiply(matrix)
|
|
||||||
}
|
|
||||||
geometry, err := r.Geometry()
|
|
||||||
if err != nil {
|
|
||||||
r.renderError = err
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
path, err := geometry.Clip(r, clips, matrix, nil)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
r.renderError = err
|
r.renderError = err
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -15,9 +15,6 @@
|
|||||||
package ofdgo
|
package ofdgo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"path"
|
|
||||||
|
|
||||||
"github.com/tdewolff/canvas"
|
"github.com/tdewolff/canvas"
|
||||||
"github.com/tdewolff/font"
|
"github.com/tdewolff/font"
|
||||||
)
|
)
|
||||||
@@ -25,54 +22,8 @@ import (
|
|||||||
// ResolveFont 按需加载字体资源,保留内嵌数据及外部字体的名称、样式和集合索引匹配
|
// ResolveFont 按需加载字体资源,保留内嵌数据及外部字体的名称、样式和集合索引匹配
|
||||||
// 入参: r 渲染器, id 字体标识, exact 是否禁止无关回退
|
// 入参: r 渲染器, id 字体标识, exact 是否禁止无关回退
|
||||||
// 返回: ResolvedFont 字体与来源, error 字体不可用错误
|
// 返回: ResolvedFont 字体与来源, error 字体不可用错误
|
||||||
func (CanvasBackend) ResolveFont(r *Renderer, id string, exact bool) (ResolvedFont, error) {
|
func (b CanvasBackend) ResolveFont(r *Renderer, id string, exact bool) (ResolvedFont, error) {
|
||||||
definition := r.Reader.fontCache[id]
|
return r.resolveFontSource(b, id, exact)
|
||||||
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{}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// OpenFont 解析独立SFNT数据,度量与原有编辑排版保持一致
|
// OpenFont 解析独立SFNT数据,度量与原有编辑排版保持一致
|
||||||
@@ -97,8 +48,13 @@ func (f canvasFontMetrics) Write() []byte {
|
|||||||
return fontSFNTData(f.SFNT)
|
return fontSFNTData(f.SFNT)
|
||||||
}
|
}
|
||||||
|
|
||||||
// initCanvasFonts 初始化Canvas默认字体
|
// GlyphOutline 返回基线原点、纵轴向下的字形轮廓
|
||||||
func (r *Renderer) initCanvasFonts() {
|
// 入参: glyph 字形编号, size 字号,单位为毫米
|
||||||
r.canvasState().fontFamily = canvas.NewFontFamily("default")
|
// 返回: GeometryPath 字形路径, error 字形解析错误
|
||||||
r.canvasState().defaultFontLoaded = r.loadDefaultFonts()
|
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
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -14,51 +14,7 @@
|
|||||||
|
|
||||||
package ofdgo
|
package ofdgo
|
||||||
|
|
||||||
import (
|
import "github.com/tdewolff/canvas"
|
||||||
"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
|
|
||||||
}
|
|
||||||
|
|
||||||
// loadFont 加载字体
|
// loadFont 加载字体
|
||||||
// 入参: fontID 字体ID
|
// 入参: fontID 字体ID
|
||||||
@@ -86,9 +42,7 @@ func (r *Renderer) loadFont(fontID string) *canvas.FontFamily {
|
|||||||
r.renderError = err
|
r.renderError = err
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if r.Reader.fontCache[fontID] != nil {
|
|
||||||
r.canvasState().fontMap[fontID] = ff
|
r.canvasState().fontMap[fontID] = ff
|
||||||
}
|
|
||||||
return ff
|
return ff
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,211 +59,3 @@ func canvasFontStyle(font *Font) canvas.FontStyle {
|
|||||||
}
|
}
|
||||||
return style
|
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
|
|
||||||
}
|
|
||||||
@@ -21,6 +21,28 @@ import (
|
|||||||
"github.com/tdewolff/canvas"
|
"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描边样式,虚线由调用方传入
|
// canvasStrokeOptions 转换Canvas描边样式,虚线由调用方传入
|
||||||
// 入参: width 描边宽度, cap 线帽, join 连接, tolerance 展开误差
|
// 入参: width 描边宽度, cap 线帽, join 连接, tolerance 展开误差
|
||||||
// 返回: StrokeOptions 公共描边样式
|
// 返回: StrokeOptions 公共描边样式
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -22,7 +22,7 @@ import (
|
|||||||
"github.com/gogpu/gg"
|
"github.com/gogpu/gg"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GGBackend 不注册全局GPU设备或回退到其他绘图库的CPU后端
|
// GGBackend 提供字体、页面编译和CPU绘制,不注册全局GPU设备或隐式切换后端
|
||||||
type GGBackend struct{}
|
type GGBackend struct{}
|
||||||
|
|
||||||
// Name 返回后端标识
|
// Name 返回后端标识
|
||||||
@@ -43,7 +43,9 @@ func (GGBackend) Render(page *RasterPage) (image.Image, error) {
|
|||||||
for i, cmd := range page.Commands {
|
for i, cmd := range page.Commands {
|
||||||
m := page.PixelTransform(cmd.Transform, h)
|
m := page.PixelTransform(cmd.Transform, h)
|
||||||
if cmd.Image != nil {
|
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
|
continue
|
||||||
}
|
}
|
||||||
path := gg.NewPath()
|
path := gg.NewPath()
|
||||||
|
|||||||
@@ -121,30 +121,11 @@ func (r *Renderer) canvasEncodedImage(img image.Image) image.Image {
|
|||||||
// 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) {
|
||||||
border := obj.Border
|
border := imageBorderObject(obj, box)
|
||||||
width := defaultPathLineWidth
|
if border.LineWidth == 0 {
|
||||||
if border.LineWidth != nil {
|
|
||||||
width = *border.LineWidth
|
|
||||||
}
|
|
||||||
if width == 0 {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w, h := box.W, box.H
|
r.renderPath(ctx, border, pageH, nil, parentCTM, boundaryInCTM, clipPath)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// imageWithClip 应用图片裁剪区域
|
// imageWithClip 应用图片裁剪区域
|
||||||
|
|||||||
@@ -173,6 +173,22 @@ func (r *Renderer) renderPath(ctx *canvas.Context, obj PathObject, pageH float64
|
|||||||
if r.textOnly {
|
if r.textOnly {
|
||||||
return
|
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 {
|
if obj.Visible != nil && !*obj.Visible {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -280,8 +296,7 @@ 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)
|
||||||
_, nativeGeometry := r.backends.Geometry.(CanvasBackend)
|
if strokeClip != nil || strokeView != canvas.Identity || repeat || style.strokePattern != nil || !canvasNativeStroke(r.backends.Geometry) {
|
||||||
if strokeClip != nil || strokeView != canvas.Identity || repeat || style.strokePattern != nil || !nativeGeometry {
|
|
||||||
sp = sp.Copy()
|
sp = sp.Copy()
|
||||||
sp = r.strokeCanvasPath(sp, style.lineWidth, style.lineCap, style.lineJoin)
|
sp = r.strokeCanvasPath(sp, style.lineWidth, style.lineCap, style.lineJoin)
|
||||||
sp = applyClipPath(sp, strokeClip)
|
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坐标
|
// 入参: obj 路径对象, pageH 页面高度, ctm 变换矩阵, bx 边界X坐标, by 边界Y坐标
|
||||||
// 返回: *canvas.Path 路径对象
|
// 返回: *canvas.Path 路径对象
|
||||||
func (r *Renderer) buildTinyFillRectPath(obj PathObject, pageH float64, ctm Matrix, bx, by float64) *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
|
return nil
|
||||||
}
|
}
|
||||||
box, err := ParseBox(obj.Boundary)
|
p, err := geometryToCanvasPath(&path)
|
||||||
if err != nil || box.W <= 0 || box.H <= 0 || box.W > 0.6 || box.H > 0.6 {
|
if err != nil {
|
||||||
|
r.renderError = err
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
tokens := strings.Fields(obj.AbbreviatedData)
|
return p.Translate(0, pageH)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
@@ -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))})
|
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
|
|
||||||
}
|
|
||||||
@@ -23,15 +23,8 @@ import (
|
|||||||
// canvasBackendState 保存Canvas字体、字形和编码图片适配缓存
|
// canvasBackendState 保存Canvas字体、字形和编码图片适配缓存
|
||||||
type canvasBackendState struct {
|
type canvasBackendState struct {
|
||||||
images map[*EncodedImage]image.Image
|
images map[*EncodedImage]image.Image
|
||||||
fontFamily *canvas.FontFamily
|
|
||||||
defaultFontLoaded bool
|
|
||||||
fontMap map[string]*canvas.FontFamily
|
fontMap map[string]*canvas.FontFamily
|
||||||
fontCache map[fontCacheKey]*canvas.FontFamily
|
|
||||||
svgFontCache map[*canvas.Font]SVGFont
|
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
|
textGlyphPathCache map[textGlyphPathCacheKey]textGlyphPathCacheValue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,15 +37,15 @@ func (r *Renderer) canvasState() *canvasBackendState {
|
|||||||
}
|
}
|
||||||
state := &canvasBackendState{
|
state := &canvasBackendState{
|
||||||
images: make(map[*EncodedImage]image.Image),
|
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),
|
|
||||||
}
|
}
|
||||||
|
state.resetFonts()
|
||||||
r.backendStates[key] = state
|
r.backendStates[key] = state
|
||||||
return 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)
|
||||||
|
}
|
||||||
Reference in new issue
Block a user