mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 16:59:27 +08:00
This commit is contained in:
1 parent
3ba9317cf3
commit
81e6657c65
8 files changed
+323
-66
No files matched your search
@@ -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`;
|
||||||
|
|||||||
+43
-12
@@ -20,6 +20,7 @@ import (
|
|||||||
"image"
|
"image"
|
||||||
"io"
|
"io"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ErrBackendUnavailable 表示未配置所需后端,不会自动切换其他实现
|
// ErrBackendUnavailable 表示未配置所需后端,不会自动切换其他实现
|
||||||
@@ -105,6 +106,7 @@ type EPSBackend interface {
|
|||||||
// RenderBackends 按职责组合后端,nil表示明确禁用,不隐式回退
|
// RenderBackends 按职责组合后端,nil表示明确禁用,不隐式回退
|
||||||
// 实例及其Renderer需串行使用,第三方实现无需引用任何内置绘图库
|
// 实例及其Renderer需串行使用,第三方实现无需引用任何内置绘图库
|
||||||
type RenderBackends struct {
|
type RenderBackends struct {
|
||||||
|
Resources FontResourceBackend
|
||||||
Fonts FontBackend
|
Fonts FontBackend
|
||||||
Geometry GeometryBackend
|
Geometry GeometryBackend
|
||||||
Compiler PageCompiler
|
Compiler PageCompiler
|
||||||
@@ -116,6 +118,7 @@ type RenderBackends struct {
|
|||||||
|
|
||||||
// BackendInfo 描述各项能力实际使用的后端,空标识表示未配置
|
// BackendInfo 描述各项能力实际使用的后端,空标识表示未配置
|
||||||
type BackendInfo struct {
|
type BackendInfo struct {
|
||||||
|
Resources string `json:"resources"`
|
||||||
Fonts string `json:"fonts"`
|
Fonts string `json:"fonts"`
|
||||||
Geometry string `json:"geometry"`
|
Geometry string `json:"geometry"`
|
||||||
Compiler string `json:"compiler"`
|
Compiler string `json:"compiler"`
|
||||||
@@ -125,33 +128,61 @@ type BackendInfo struct {
|
|||||||
EPS string `json:"eps"`
|
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
@@ -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
@@ -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
@@ -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条目直接复制到新包,逐项写入改动,不持有原资源解压副本
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 ""
|
||||||
|
}
|
||||||
@@ -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" }
|
||||||
|
|||||||
Reference in new issue
Block a user