feat(后端架构): 完善后端扩展与字体资源解耦,修复元数据保存和缩略图底边
build.yaml / build (push) Successful in 41s

This commit is contained in:
xiaoqidun committed 2026-09-24 10:13:54 +08:00
1 parent 3ba9317cf3
commit 81e6657c65
8 files changed
+337 -80

No files matched your search

+1 -1
View File
@@ -5714,7 +5714,7 @@ function layoutThumbnails() {
} }
view.offsets = offsets; view.offsets = offsets;
view.gap = 12; view.gap = 12;
view.total = Math.max(0, offsets.at(-1) - 12); view.total = Math.ceil(offsets.at(-1));
for (const [index, button] of view.nodes) { for (const [index, button] of view.nodes) {
layoutThumbnail(button, state.doc.pages[index]); layoutThumbnail(button, state.doc.pages[index]);
button.style.height = `${offsets[index + 1] - offsets[index] - 12}px`; button.style.height = `${offsets[index + 1] - offsets[index] - 12}px`;
+57 -26
View File
@@ -20,6 +20,7 @@ import (
"image" "image"
"io" "io"
"reflect" "reflect"
"sync"
) )
// ErrBackendUnavailable 表示未配置所需后端,不会自动切换其他实现 // ErrBackendUnavailable 表示未配置所需后端,不会自动切换其他实现
@@ -105,53 +106,83 @@ type EPSBackend interface {
// RenderBackends 按职责组合后端,nil表示明确禁用,不隐式回退 // RenderBackends 按职责组合后端,nil表示明确禁用,不隐式回退
// 实例及其Renderer需串行使用,第三方实现无需引用任何内置绘图库 // 实例及其Renderer需串行使用,第三方实现无需引用任何内置绘图库
type RenderBackends struct { type RenderBackends struct {
Fonts FontBackend Resources FontResourceBackend
Geometry GeometryBackend Fonts FontBackend
Compiler PageCompiler Geometry GeometryBackend
Raster RasterBackend Compiler PageCompiler
SVG SVGBackend Raster RasterBackend
PDF PDFBackend SVG SVGBackend
EPS EPSBackend PDF PDFBackend
EPS EPSBackend
} }
// BackendInfo 描述各项能力实际使用的后端,空标识表示未配置 // BackendInfo 描述各项能力实际使用的后端,空标识表示未配置
type BackendInfo struct { type BackendInfo struct {
Fonts string `json:"fonts"` Resources string `json:"resources"`
Geometry string `json:"geometry"` Fonts string `json:"fonts"`
Compiler string `json:"compiler"` Geometry string `json:"geometry"`
Raster string `json:"raster"` Compiler string `json:"compiler"`
SVG string `json:"svg"` Raster string `json:"raster"`
PDF string `json:"pdf"` SVG string `json:"svg"`
EPS string `json:"eps"` PDF string `json:"pdf"`
EPS string `json:"eps"`
} }
// NewRenderBackends 创建内置后端组合,各项能力通过Info报告实际提供者 // NewRenderBackends 创建已注册后端组合,各项能力通过Info报告实际提供者
// 入参: name 内置后端标识 // 入参: name 已注册后端标识
// 返回: RenderBackends 后端组合, error 未知后端错误 // 返回: RenderBackends 后端组合, error 未知后端错误
func NewRenderBackends(name string) (RenderBackends, error) { func NewRenderBackends(name string) (RenderBackends, error) {
if name != "canvas" { backendRegistry.RLock()
factory := backendRegistry.factories[name]
backendRegistry.RUnlock()
if factory == nil {
return RenderBackends{}, fmt.Errorf("unknown render backend %q", name) return RenderBackends{}, fmt.Errorf("unknown render backend %q", name)
} }
return defaultRenderBackends(), nil return factory(), nil
} }
// RenderBackendInfos 列出内置组合及其实际能力,供原生程序和WASM共用 // RenderBackendInfos 列出已注册组合及其实际能力,供原生程序和WASM共用
// 返回: map[string]BackendInfo 后端能力 // 返回: map[string]BackendInfo 后端能力
func RenderBackendInfos() map[string]BackendInfo { func RenderBackendInfos() map[string]BackendInfo {
return map[string]BackendInfo{"canvas": defaultRenderBackends().Info()} backendRegistry.RLock()
factories := make(map[string]func() RenderBackends, len(backendRegistry.factories))
for name, factory := range backendRegistry.factories {
factories[name] = factory
}
backendRegistry.RUnlock()
result := make(map[string]BackendInfo, len(factories))
for name, factory := range factories {
result[name] = factory().Info()
}
return result
} }
// defaultRenderBackends 创建独立的默认配置 // backendRegistry 保存后端工厂,不共享有状态的后端实例
// 返回: RenderBackends 默认后端组合 var backendRegistry = struct {
func defaultRenderBackends() RenderBackends { sync.RWMutex
backend := CanvasBackend{} factories map[string]func() RenderBackends
return RenderBackends{Fonts: backend, Geometry: backend, Compiler: backend, Raster: backend, SVG: backend, PDF: backend, EPS: backend} }{factories: make(map[string]func() RenderBackends)}
// RegisterRenderBackend 注册后端组合,工厂应返回独立实例,重复标识不会覆盖已有实现
// 入参: name 后端标识, factory 后端工厂
// 返回: error 无效或重复注册错误
func RegisterRenderBackend(name string, factory func() RenderBackends) error {
if name == "" || factory == nil {
return fmt.Errorf("backend name and factory are required")
}
backendRegistry.Lock()
defer backendRegistry.Unlock()
if _, exists := backendRegistry.factories[name]; exists {
return fmt.Errorf("render backend %q already registered", name)
}
backendRegistry.factories[name] = factory
return nil
} }
// Info 返回配置中各能力的提供者,不依赖WebUI推断 // Info 返回配置中各能力的提供者,不依赖WebUI推断
// 返回: BackendInfo 后端能力 // 返回: BackendInfo 后端能力
func (b RenderBackends) Info() BackendInfo { func (b RenderBackends) Info() BackendInfo {
return BackendInfo{Fonts: backendName(b.Fonts), Geometry: backendName(b.Geometry), Compiler: backendName(b.Compiler), Raster: backendName(b.Raster), SVG: backendName(b.SVG), PDF: backendName(b.PDF), EPS: backendName(b.EPS)} return BackendInfo{Resources: backendName(b.Resources), Fonts: backendName(b.Fonts), Geometry: backendName(b.Geometry), Compiler: backendName(b.Compiler), Raster: backendName(b.Raster), SVG: backendName(b.SVG), PDF: backendName(b.PDF), EPS: backendName(b.EPS)}
} }
// backendName 获取已配置的后端标识 // backendName 获取已配置的后端标识
+19 -45
View File
@@ -31,7 +31,6 @@ import (
"time" "time"
"unicode/utf8" "unicode/utf8"
"github.com/tdewolff/font"
"github.com/xiaoqidun/jbig2" "github.com/xiaoqidun/jbig2"
) )
@@ -45,7 +44,7 @@ type Editor struct {
Info DocInfo Info DocInfo
pages []PageContent pages []PageContent
resources []editorResource resources []editorResource
fonts map[string]*font.SFNT fonts map[string]*FontResource
fontFS []fs.FS fontFS []fs.FS
fontRenderer *Renderer fontRenderer *Renderer
fontMetrics map[string]FontMetrics fontMetrics map[string]FontMetrics
@@ -86,6 +85,12 @@ func (e *Editor) SetPageCompiler(compiler PageCompiler) {
// 入参: backends 后端组合,未配置的能力不会沿用默认实现 // 入参: backends 后端组合,未配置的能力不会沿用默认实现
func (e *Editor) SetRenderBackends(backends RenderBackends) { func (e *Editor) SetRenderBackends(backends RenderBackends) {
fontsChanged := !sameBackend(e.backends.Fonts, backends.Fonts) fontsChanged := !sameBackend(e.backends.Fonts, backends.Fonts)
if !sameBackend(e.backends.Resources, backends.Resources) {
e.fonts = make(map[string]*FontResource)
for i := range e.resources {
e.resources[i].subset = nil
}
}
e.backends = backends e.backends = backends
if e.fontRenderer != nil { if e.fontRenderer != nil {
WithRenderBackends(backends)(e.fontRenderer) WithRenderBackends(backends)(e.fontRenderer)
@@ -150,7 +155,7 @@ func NewEditor() *Editor {
DocID: hex.EncodeToString(id[:]), DocID: hex.EncodeToString(id[:]),
CreationDate: time.Now().Format("2006-01-02"), CreationDate: time.Now().Format("2006-01-02"),
}, },
fonts: make(map[string]*font.SFNT), fonts: make(map[string]*FontResource),
images: make(map[string]image.Point), images: make(map[string]image.Point),
resourceID: make(map[editorResourceKey]string), resourceID: make(map[editorResourceKey]string),
fontMetrics: make(map[string]FontMetrics), fontMetrics: make(map[string]FontMetrics),
@@ -564,67 +569,36 @@ func (e *Editor) nextID() string {
// 入参: file 字体文件, index 集合内字体索引,单字体为0 // 入参: file 字体文件, index 集合内字体索引,单字体为0
// 返回: string 字体资源标识, error 错误信息 // 返回: string 字体资源标识, error 错误信息
func (e *Editor) AddFont(file FontFile, index int) (string, error) { func (e *Editor) AddFont(file FontFile, index int) (string, error) {
data, err := font.ToSFNT(file.Data) if e.backends.Resources == nil {
return "", fmt.Errorf("font resources: %w", ErrBackendUnavailable)
}
parsed, err := e.backends.Resources.OpenFontResource(file, index)
if err != nil { if err != nil {
return "", err return "", err
} }
key := editorResourceKey{checksum: sha256.Sum256(data), index: index} data := parsed.Data
key := editorResourceKey{checksum: sha256.Sum256(data)}
if id, ok := e.resourceID[key]; ok { if id, ok := e.resourceID[key]; ok {
return id, nil return id, nil
} }
data, err = (FontFile{Data: data}).Face(index)
if err != nil {
return "", err
}
sfnt, err := font.ParseSFNT(data, 0)
if err != nil {
return "", err
}
name := editorFontName(sfnt, font.NamePostScript, font.NameFull, font.NameFontFamily)
if name == "" {
return "", fmt.Errorf("font has no name")
}
if err := e.prepareSourceIDs(); err != nil { if err := e.prepareSourceIDs(); err != nil {
return "", err return "", err
} }
id := e.nextID() id := e.nextID()
extension := ".ttf" definition := parsed.Font
if sfnt.IsCFF { definition.ID = id
extension = ".otf"
}
resource := editorResource{ resource := editorResource{
name: e.packageName("Res/Fonts/Font_" + id + extension), name: e.packageName("Res/Fonts/Font_" + id + parsed.Extension),
data: data, data: data,
font: &Font{ font: &definition,
ID: id, FontName: name,
FamilyName: editorFontName(sfnt, font.NamePreferredFamily, font.NameFontFamily),
Charset: "unicode",
Bold: sfnt.Head.MacStyle[0],
Italic: sfnt.Head.MacStyle[1],
FixedWidth: sfnt.Post.IsFixedPitch != 0,
},
} }
resource.font.FontFile = "/" + resource.name resource.font.FontFile = "/" + resource.name
e.resources = append(e.resources, resource) e.resources = append(e.resources, resource)
e.fonts[id] = sfnt e.fonts[id] = parsed
e.resourceID[key] = id e.resourceID[key] = id
return id, nil return id, nil
} }
// editorFontName 获取字体中的名称
// 入参: sfnt 字体, names 名称类型,按顺序查找
// 返回: string 字体名称
func editorFontName(sfnt *font.SFNT, names ...font.NameID) string {
for _, name := range names {
for _, record := range sfnt.Name.Get(name) {
if value := record.String(); value != "" {
return value
}
}
}
return ""
}
// AddImage 注册PNG或JPEG图片,重复资源复用标识,引用后写入文档 // AddImage 注册PNG或JPEG图片,重复资源复用标识,引用后写入文档
// 不透明纯黑白PNG仅在无损JBIG2编码更小时转换,其他图片保留原始编码 // 不透明纯黑白PNG仅在无损JBIG2编码更小时转换,其他图片保留原始编码
// 入参: data 图片数据 // 入参: data 图片数据
+13 -2
View File
@@ -187,6 +187,9 @@ type editorFontSubset struct {
// 入参: progress 保存进度回调 // 入参: progress 保存进度回调
// 返回: map[string][]byte 包内字体子集, error 错误信息 // 返回: map[string][]byte 包内字体子集, error 错误信息
func (e *Editor) subsetFonts(progress editorProgress) (map[string][]byte, error) { func (e *Editor) subsetFonts(progress editorProgress) (map[string][]byte, error) {
if e.backends.Resources == nil {
return nil, nil
}
used := make(map[string]map[uint16]bool) used := make(map[string]map[uint16]bool)
mapped := make(map[string]bool) mapped := make(map[string]bool)
for _, resource := range e.resources { for _, resource := range e.resources {
@@ -194,7 +197,15 @@ func (e *Editor) subsetFonts(progress editorProgress) (map[string][]byte, error)
continue continue
} }
sfnt := e.fonts[resource.font.ID] sfnt := e.fonts[resource.font.ID]
if sfnt.IsTrueType && !slices.ContainsFunc([]string{"fvar", "COLR", "CBDT", "sbix", "SVG "}, func(tag string) bool { return sfnt.Tables[tag] != nil }) { if sfnt == nil {
var err error
sfnt, err = e.backends.Resources.OpenFontResource(FontFile{Data: resource.data}, 0)
if err != nil {
return nil, err
}
e.fonts[resource.font.ID] = sfnt
}
if sfnt.CanSubset {
used[resource.font.ID] = make(map[uint16]bool) used[resource.font.ID] = make(map[uint16]bool)
} }
} }
@@ -309,7 +320,7 @@ func (e *Editor) subsetFonts(progress editorProgress) (map[string][]byte, error)
} }
slices.Sort(ids) slices.Sort(ids)
if resource.subset == nil || resource.subset.mapped != mapped[resource.font.ID] || !slices.Equal(resource.subset.glyphs, ids) { if resource.subset == nil || resource.subset.mapped != mapped[resource.font.ID] || !slices.Equal(resource.subset.glyphs, ids) {
data, err := subsetEditorFont(resource.data, ids, mapped[resource.font.ID]) data, err := e.backends.Resources.SubsetFont(resource.data, ids, mapped[resource.font.ID])
if err != nil { if err != nil {
return nil, fmt.Errorf("subset font %s: %w", resource.font.FontName, err) return nil, fmt.Errorf("subset font %s: %w", resource.font.FontName, err)
} }
+95 -4
View File
@@ -441,9 +441,6 @@ func (e *Editor) sourceInfoXML() ([]byte, error) {
return nil, fmt.Errorf("document has no DocInfo") return nil, fmt.Errorf("document has no DocInfo")
} }
before, after := e.source.info, e.Info before, after := e.source.info, e.Info
if !reflect.DeepEqual(before.CustomDatas, after.CustomDatas) {
return nil, fmt.Errorf("editing imported custom metadata is not supported")
}
var values [][2]string var values [][2]string
for _, field := range [][3]string{ for _, field := range [][3]string{
{"DocID", before.DocID, after.DocID}, {"Title", before.Title, after.Title}, {"Author", before.Author, after.Author}, {"DocID", before.DocID, after.DocID}, {"Title", before.Title, after.Title}, {"Author", before.Author, after.Author},
@@ -454,7 +451,101 @@ func (e *Editor) sourceInfoXML() ([]byte, error) {
values = append(values, [2]string{field[0], field[2]}) values = append(values, [2]string{field[0], field[2]})
} }
} }
return editorXMLSetText(data, info, values), nil data = editorXMLSetText(data, info, values)
if !reflect.DeepEqual(before.CustomDatas, after.CustomDatas) {
root, err = parseEditorXML(data)
if err != nil {
return nil, err
}
return editorCustomDatas(data, root.child("DocBody").child("DocInfo"), after.CustomDatas)
}
return data, nil
}
// editorCustomDatas 更新自定义字段,保留容器扩展与同名字段的未知属性和子节点
// 入参: data 原XML, info 文档信息节点, values 新字段
// 返回: []byte 修改后的XML, error 编码错误
func editorCustomDatas(data []byte, info *editorXML, values *CustomDatas) ([]byte, error) {
container := info.child("CustomDatas")
var items []CustomData
if values != nil {
items = values.CustomData
}
byName := make(map[string][]*editorXML)
var nodes []*editorXML
if container != nil {
for _, node := range container.children {
if node.name.Local == "CustomData" && (node.name.Space == ofdNamespace || node.name.Space == container.name.Space) {
nodes = append(nodes, node)
byName[node.attr("Name")] = append(byName[node.attr("Name")], node)
}
}
}
var encoded [][]byte
for _, item := range items {
matches := byName[item.Name]
if len(matches) == 0 {
entry, err := xml.Marshal(struct {
XMLName xml.Name
CustomData
}{xml.Name{Space: ofdNamespace, Local: "CustomData"}, item})
if err != nil {
return nil, err
}
encoded = append(encoded, entry)
continue
}
node := matches[0]
byName[item.Name] = matches[1:]
var original CustomData
if err := xml.Unmarshal(data[node.start:node.end], &original); err != nil {
return nil, err
}
if original.Value == item.Value {
encoded = append(encoded, data[node.start:node.end])
continue
}
var content bytes.Buffer
if err := xml.EscapeText(&content, []byte(item.Value)); err != nil {
return nil, err
}
for _, child := range node.children {
content.Write(data[child.start:child.end])
}
patch := editorXMLContent(data, node, content.Bytes())
entry := append(bytes.Clone(data[node.start:patch.start]), patch.data...)
entry = append(entry, data[patch.end:node.end]...)
encoded = append(encoded, entry)
}
if container == nil {
if len(encoded) == 0 {
return data, nil
}
content := []byte("<ofd:CustomDatas xmlns:ofd=\"" + ofdNamespace + "\">")
content = append(content, bytes.Join(encoded, nil)...)
content = append(content, []byte("</ofd:CustomDatas>")...)
if info.open == info.end {
return editorPatchXML(data, []editorXMLPatch{editorXMLContent(data, info, content)}), nil
}
return editorPatchXML(data, []editorXMLPatch{{info.close, info.close, content}}), nil
}
var patches []editorXMLPatch
for i, node := range nodes {
var entry []byte
if i < len(encoded) {
entry = encoded[i]
}
patches = append(patches, editorXMLPatch{node.start, node.end, entry})
}
if len(encoded) > len(nodes) {
added := bytes.Join(encoded[len(nodes):], nil)
if container.open == container.end {
patches = append(patches, editorXMLContent(data, container, added))
} else {
patches = append(patches, editorXMLPatch{container.close, container.close, added})
}
}
return editorPatchXML(data, patches), nil
} }
// writeSource 将未修改ZIP条目直接复制到新包,逐项写入改动,不持有原资源解压副本 // writeSource 将未修改ZIP条目直接复制到新包,逐项写入改动,不持有原资源解压副本
+7 -2
View File
@@ -18,6 +18,7 @@ import (
"bufio" "bufio"
"bytes" "bytes"
"encoding/xml" "encoding/xml"
"fmt"
"io" "io"
"maps" "maps"
"path" "path"
@@ -172,14 +173,18 @@ func (e *Editor) compactSourceResources(parts map[string][]byte, progress editor
} }
key := strings.ToLower(cleanPackagePath(name)) key := strings.ToLower(cleanPackagePath(name))
usage := fontFiles[key] usage := fontFiles[key]
if usage == nil || usage.unsafe || refs.files[key] || removed[key] || parts[name] != nil { if e.backends.Resources == nil || usage == nil || usage.unsafe || refs.files[key] || removed[key] || parts[name] != nil {
continue continue
} }
data, err := reader.readFile(name) data, err := reader.readFile(name)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if subset := subsetSourceFont(data, usage); subset != nil { subset, err := e.backends.Resources.SubsetSourceFont(data, FontUsage{Characters: slices.Sorted(maps.Keys(usage.chars)), Glyphs: slices.Sorted(maps.Keys(usage.glyphs)), Unsafe: usage.unsafe})
if err != nil {
return nil, fmt.Errorf("subset font %s: %w", name, err)
}
if subset != nil {
updates[name] = subset updates[name] = subset
} }
} }
+131
View File
@@ -0,0 +1,131 @@
// 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"
"slices"
"github.com/tdewolff/font"
)
// FontResource 保存独立字体数据、标准元信息与只读度量,不包含绘图库类型
type FontResource struct {
FontMetrics
Data []byte
Font Font
Extension string
CanSubset bool
}
// FontUsage 描述全包已确认的字体引用,Unsafe禁止裁剪
type FontUsage struct {
Characters []rune
Glyphs []uint16
Unsafe bool
}
// FontResourceBackend 解析和裁剪保存资源,独立于显示字体后端
// SubsetFont的preserve为true时不得改变字形编号,SubsetSourceFont无安全收益时返回nil
type FontResourceBackend interface {
Backend
OpenFontResource(file FontFile, index int) (*FontResource, error)
SubsetFont(data []byte, glyphs []uint16, preserve bool) ([]byte, error)
SubsetSourceFont(data []byte, usage FontUsage) ([]byte, error)
}
// SFNTBackend 提供默认字体资源处理,不依赖Canvas绘图库
type SFNTBackend struct{}
// Name 返回资源后端标识
// 返回: string 后端标识
func (SFNTBackend) Name() string { return "sfnt" }
// OpenFontResource 提取独立字体并读取名称、样式和裁剪能力
// 入参: file 字体文件, index 集合索引
// 返回: *FontResource 字体资源, error 解析错误
func (SFNTBackend) OpenFontResource(file FontFile, index int) (*FontResource, error) {
data, err := file.Face(index)
if err != nil {
return nil, err
}
sfnt, err := font.ParseSFNT(data, 0)
if err != nil {
return nil, err
}
name := editorFontName(sfnt, font.NamePostScript, font.NameFull, font.NameFontFamily)
if name == "" {
return nil, fmt.Errorf("font has no name")
}
extension := ".ttf"
if sfnt.IsCFF {
extension = ".otf"
}
return &FontResource{
FontMetrics: sfntResourceMetrics{sfnt}, Data: data, Extension: extension,
Font: Font{FontName: name, FamilyName: editorFontName(sfnt, font.NamePreferredFamily, font.NameFontFamily), Charset: "unicode", Bold: sfnt.Head.MacStyle[0], Italic: sfnt.Head.MacStyle[1], FixedWidth: sfnt.Post.IsFixedPitch != 0},
CanSubset: sfntSubsettable(sfnt),
}, nil
}
// sfntSubsettable 判断默认资源后端能否无损裁剪该字体
// 入参: sfnt 已解析字体
// 返回: bool 是否支持裁剪
func sfntSubsettable(sfnt *font.SFNT) bool {
return sfnt.IsTrueType && !slices.ContainsFunc([]string{"fvar", "COLR", "CBDT", "sbix", "SVG "}, func(tag string) bool { return sfnt.Tables[tag] != nil })
}
// sfntResourceMetrics 隔离资源解析器的度量实现
type sfntResourceMetrics struct{ *font.SFNT }
// Write 返回未改动元信息的字体数据
// 返回: []byte 字体数据
func (f sfntResourceMetrics) Write() []byte { return fontSFNTData(f.SFNT) }
// SubsetFont 裁剪新建字体并按要求保留显式字形编号
// 入参: data 字体数据, glyphs 用字, preserve 是否保留编号
// 返回: []byte 子集, error 裁剪错误
func (SFNTBackend) SubsetFont(data []byte, glyphs []uint16, preserve bool) ([]byte, error) {
return subsetEditorFont(data, glyphs, preserve)
}
// SubsetSourceFont 按全包引用裁剪原字体,不确定时保留原资源
// 入参: data 字体数据, usage 用字信息
// 返回: []byte 子集, error 裁剪错误
func (SFNTBackend) SubsetSourceFont(data []byte, usage FontUsage) ([]byte, error) {
u := newEditorFontUsage()
u.unsafe = usage.Unsafe
for _, char := range usage.Characters {
u.chars[char] = true
}
for _, glyph := range usage.Glyphs {
u.glyphs[glyph] = true
}
return subsetSourceFont(data, u), nil
}
// editorFontName 获取字体中的名称
// 入参: sfnt 字体, names 名称类型,按顺序查找
// 返回: string 字体名称
func editorFontName(sfnt *font.SFNT, names ...font.NameID) string {
for _, name := range names {
for _, record := range sfnt.Name.Get(name) {
if value := record.String(); value != "" {
return value
}
}
}
return ""
}
+14
View File
@@ -29,6 +29,20 @@ type CanvasBackend struct {
ColorSpace canvas.ColorSpace ColorSpace canvas.ColorSpace
} }
// init 注册Canvas适配器和默认组合
func init() {
if err := RegisterRenderBackend("canvas", defaultRenderBackends); err != nil {
panic(err)
}
}
// defaultRenderBackends 创建默认组合,调用方可按职责替换任意后端
// 返回: RenderBackends 默认后端组合
func defaultRenderBackends() RenderBackends {
backend := CanvasBackend{}
return RenderBackends{Resources: SFNTBackend{}, Fonts: backend, Geometry: backend, Compiler: backend, Raster: backend, SVG: backend, PDF: backend, EPS: backend}
}
// Name 返回后端标识 // Name 返回后端标识
// 返回: string 后端标识 // 返回: string 后端标识
func (CanvasBackend) Name() string { return "canvas" } func (CanvasBackend) Name() string { return "canvas" }