style(代码风格): 整理代码归属与命名
build.yaml / build (push) Successful in 38s

This commit is contained in:
xiaoqidun committed 2026-09-24 10:30:57 +08:00
1 parent 81e6657c65
commit 0508029387
13 files changed
+1135 -1079

No files matched your search

+1 -2
View File
@@ -26,6 +26,7 @@ let wasmRequestID = 0;
let wasmRecoveryTimer = 0;
let textMeasure = null;
let credentialRequest = null;
let cancelPageTouch = null;
const state = {
composite: null,
@@ -5570,8 +5571,6 @@ el.finishPageSelection.addEventListener("click", () => {
updateControls();
});
let cancelPageTouch = null;
el.pageList.addEventListener("pointerdown", event => {
cancelPageTouch?.();
state.pageTouchPointer = null;
+410 -410
View File
@@ -45,6 +45,79 @@ type exportWriter struct {
write js.Value
}
// apiResult 浏览器接口返回结果
type apiResult struct {
OK bool `json:"ok"`
Code string `json:"code,omitempty"`
Error string `json:"error,omitempty"`
Data any `json:"data,omitempty"`
MissingGlyphs *ofdgo.MissingGlyphError `json:"missingGlyphs,omitempty"`
ReasonCode ofdgo.EditReason `json:"reasonCode,omitempty"`
}
// editorClipboard 当前编辑文档中的对象快照与剪贴板标识
type editorClipboard struct {
token string
objects []ofdgo.GraphicObject
composite *ofdgo.CompositeSelection
annotations *ofdgo.AnnotationSelection
}
// editorInfo 编辑文档信息与操作状态
type editorInfo struct {
DocumentInfo
Revision uint64 `json:"revision"`
CanUndo bool `json:"canUndo"`
CanRedo bool `json:"canRedo"`
PageCapabilities []uint `json:"pageCapabilities"`
EditWarnings []string `json:"editWarnings,omitempty"`
}
// editorPageInfo 页面操作结果与目标页面
type editorPageInfo struct {
editorInfo
PageIndex int `json:"pageIndex"`
}
// editorSelectionInfo 对象复制结果及新选区
type editorSelectionInfo struct {
editorInfo
SelectedIDs []string `json:"selectedIDs"`
}
// editorOutlineInfo 目录操作后的位置
type editorOutlineInfo struct {
editorInfo
OutlinePath []int `json:"outlinePath"`
}
// annotationOptions 保存注解面板的输入,资源字节单独传输
type annotationOptions struct {
Kind, ID, Text, Old, Creator, Pages, URI, Color string
Base string
X, Y, Width, Height, Size, Angle float64
Alpha int
Tile bool
Keep bool
Target int
Area *ofdgo.Box
}
// currentSession 当前WebUI文档会话
var currentSession *Session
// currentEditor 当前编辑文档
var currentEditor *ofdgo.Editor
// pendingImport 待插页文档,仅保留文件数据与索引,不创建渲染会话
var pendingImport *ofdgo.Reader
// copiedObjects 当前对象剪贴板,不保存字体或图片的重复数据
var copiedObjects *editorClipboard
// copiedStyle 当前文档中的独立样式快照
var copiedStyle *ofdgo.GraphicObject
// Write 将数据块交给浏览器保存
// 入参: data 导出数据
// 返回: int 写入长度, error 错误信息
@@ -80,25 +153,6 @@ func awaitExport(fn js.Value, args ...any) error {
return <-done
}
// currentSession 当前WebUI文档会话
var currentSession *Session
// pendingImport 待插页文档,仅保留文件数据与索引,不创建渲染会话
var pendingImport *ofdgo.Reader
// copiedStyle 当前文档中的独立样式快照
var copiedStyle *ofdgo.GraphicObject
// apiResult 浏览器接口返回结果
type apiResult struct {
OK bool `json:"ok"`
Code string `json:"code,omitempty"`
Error string `json:"error,omitempty"`
Data any `json:"data,omitempty"`
MissingGlyphs *ofdgo.MissingGlyphError `json:"missingGlyphs,omitempty"`
ReasonCode ofdgo.EditReason `json:"reasonCode,omitempty"`
}
// RunWASM 注册浏览器WASM接口并阻塞运行
func RunWASM() {
registerCallback("ofdgoOpen", openDocument)
@@ -297,45 +351,6 @@ func openDocument(args []js.Value) (any, error) {
return currentSession.Summary(), nil
}
// credentialsFromJS 解析本次打开的口令或本机证书私钥
// 入参: value 解锁表单数据
// 返回: ofdgo.Credentials 会话凭据, error 错误信息
func credentialsFromJS(value js.Value) (ofdgo.Credentials, error) {
credentials := ofdgo.Credentials{UserName: value.Get("userName").String()}
if keyValue := value.Get("key"); !keyValue.IsNull() && !keyValue.IsUndefined() {
key, err := bytesFromJS(keyValue)
if err != nil {
return credentials, err
}
defer clear(key)
certificate, err := bytesFromJS(value.Get("certificate"))
if err != nil {
return credentials, err
}
var password []byte
if field := value.Get("keyPassword"); !field.IsUndefined() && !field.IsNull() {
password, err = bytesFromJS(field)
if err != nil {
return credentials, err
}
defer clear(password)
}
signer, certificate, err := ofdgo.ParseSignatureIdentity(certificate, key, password)
if err != nil {
return credentials, ofdgo.ErrInvalidCredentials
}
decrypter, ok := signer.(crypto.Decrypter)
if !ok {
return credentials, ofdgo.ErrInvalidCredentials
}
credentials.Certificate, credentials.Decrypter = certificate, decrypter
return credentials, nil
}
var err error
credentials.Password, err = bytesFromJS(value.Get("password"))
return credentials, err
}
// configureDocument 更新当前会话的字体和注解设置
// 入参: args 浏览器参数
// 返回: any 文档信息, error 错误信息
@@ -378,70 +393,6 @@ func documentInfo(args []js.Value) (any, error) {
return currentSession.Info(), nil
}
// securityFilesFromJS 读取会话内的本机证据文件
// 入参: value 二进制数组列表
// 返回: [][]byte 文件内容, error 错误信息
func securityFilesFromJS(value js.Value) ([][]byte, error) {
if value.IsUndefined() || value.IsNull() {
return nil, nil
}
files := make([][]byte, value.Length())
for i := range files {
var err error
files[i], err = bytesFromJS(value.Index(i))
if err != nil {
return nil, err
}
}
return files, nil
}
// verifySignatures 按本机信任锚和离线证据重新验签,不设置联网回调
// 入参: args 信任根、辅助证书、时间戳及撤销证据
// 返回: any 分项报告, error 错误信息
func verifySignatures(args []js.Value) (any, error) {
if currentSession == nil || len(args) == 0 {
return nil, fmt.Errorf("missing signature verification arguments")
}
input := args[0]
files := make(map[string][][]byte)
for _, name := range []string{"roots", "certificates", "timestampRoots", "tokens", "crls", "ocsp"} {
data, err := securityFilesFromJS(input.Get(name))
if err != nil {
return nil, err
}
files[name] = data
}
now := time.Now()
policy := ofdgo.SignaturePolicy{RejectWeakAlgorithms: true, RequireDocumentCoverage: true,
RequireTimestamp: input.Get("requireTimestamp").Bool(), RequireRevocation: input.Get("requireRevocation").Bool()}
options := []ofdgo.SignatureVerifyOption{
ofdgo.WithSignatureVerifyTime(now), ofdgo.WithSignaturePolicy(policy),
ofdgo.WithSignatureTrustCerts(files["roots"]...), ofdgo.WithSignatureCerts(files["certificates"]...),
}
if policy.RequireTimestamp || len(files["timestampRoots"])+len(files["tokens"]) > 0 {
options = append(options, ofdgo.WithSignatureTimestamp(ofdgo.SignatureTimestampOptions{
TrustCerts: files["timestampRoots"], Certificates: files["certificates"], Tokens: files["tokens"], Required: policy.RequireTimestamp, VerifyTime: &now,
}))
}
if policy.RequireRevocation || len(files["crls"])+len(files["ocsp"]) > 0 {
options = append(options, ofdgo.WithSignatureRevocation(ofdgo.SignatureRevocationOptions{
SignatureRevocationEvidence: ofdgo.SignatureRevocationEvidence{CRLs: files["crls"], OCSPResponses: files["ocsp"]},
Certificates: append(files["certificates"], files["roots"]...), Required: policy.RequireRevocation, VerifyTime: &now,
}))
}
reports, err := currentSession.Reader.VerifySignatures(options...)
if err != nil {
return nil, err
}
infos := make([]SignatureInfo, len(reports))
for i, report := range reports {
infos[i] = signatureInfo(report)
}
currentSession.signatures, currentSession.signatureError, currentSession.signaturesRead = infos, nil, true
return map[string]any{"signatures": infos, "signatureCount": len(infos), "signatureError": ""}, nil
}
// exportAttachment 分块导出附件
// 入参: args 浏览器参数
// 返回: any 附件数据, error 错误信息
@@ -820,42 +771,6 @@ func encodeResult(result apiResult) string {
return string(data)
}
// currentEditor 当前编辑文档
var currentEditor *ofdgo.Editor
// editorClipboard 当前编辑文档中的对象快照与剪贴板标识
type editorClipboard struct {
token string
objects []ofdgo.GraphicObject
composite *ofdgo.CompositeSelection
annotations *ofdgo.AnnotationSelection
}
// copiedObjects 当前对象剪贴板,不保存字体或图片的重复数据
var copiedObjects *editorClipboard
// editorInfo 编辑文档信息与操作状态
type editorInfo struct {
DocumentInfo
Revision uint64 `json:"revision"`
CanUndo bool `json:"canUndo"`
CanRedo bool `json:"canRedo"`
PageCapabilities []uint `json:"pageCapabilities"`
EditWarnings []string `json:"editWarnings,omitempty"`
}
// editorPageInfo 页面操作结果与目标页面
type editorPageInfo struct {
editorInfo
PageIndex int `json:"pageIndex"`
}
// editorSelectionInfo 对象复制结果及新选区
type editorSelectionInfo struct {
editorInfo
SelectedIDs []string `json:"selectedIDs"`
}
// editorSummary 获取当前编辑文档状态
// 返回: editorInfo 文档信息
func editorSummary() editorInfo {
@@ -1119,18 +1034,6 @@ func changeAnnotations(args []js.Value) (any, error) {
})
}
// annotationOptions 保存注解面板的输入,资源字节单独传输
type annotationOptions struct {
Kind, ID, Text, Old, Creator, Pages, URI, Color string
Base string
X, Y, Width, Height, Size, Angle float64
Alpha int
Tile bool
Keep bool
Target int
Area *ofdgo.Box
}
// readAnnotation 获取编辑面板所需的注解内容,不修改文件
// 入参: args 页面索引与注解标识
// 返回: any 面板数据, error 错误信息
@@ -2277,6 +2180,233 @@ func previewText(args []js.Value) (any, error) {
return map[string]any{"runs": runs, "scale": scale, "weight": text.Weight, "italic": text.Italic}, nil
}
// updateText 修改文字,null内容保留定位且允许替换字体,null字体数据和颜色沿用对象属性
// 入参: args 页码、对象标识、内容、字体数据、字号和颜色
// 返回: any 文档信息, error 错误信息
func updateText(args []js.Value) (any, error) {
if currentEditor == nil {
return nil, fmt.Errorf("no document is being edited")
}
page, id := args[0].Int(), args[1].String()
object, err := currentEditor.Object(page, id)
if err != nil {
return nil, err
}
if object.Type != "TextObject" {
return nil, fmt.Errorf("object %q is not text", id)
}
if !args[3].IsNull() {
data, err := bytesFromJS(args[3])
if err != nil {
return nil, err
}
value := object.TextObject.Text()
if !args[2].IsNull() {
value = args[2].String()
}
if err := checkFontGlyphs(data, value); err != nil {
return nil, err
}
object.TextObject.Font, err = currentEditor.AddFont(FontFile{Data: data}, 0)
if err != nil {
return nil, err
}
}
if !args[2].IsNull() {
if args[4].Float() == object.TextObject.Size {
style := ofdgo.TextStyle{Font: object.TextObject.Font}
if !args[5].IsNull() {
var fill ofdgo.FillColor
if err := setEditorColor(&fill, args[5].String()); err != nil {
return nil, err
}
style.Color = fill.Value
}
revision := currentEditor.Revision()
if err := currentEditor.UpdateTextContent(page, id, args[2].String(), style); err != nil {
return nil, err
}
if currentEditor.Revision() == revision {
return editorSummary(), nil
}
return previewEditor(currentEditor, currentSession.Renderer.RenderAnnotations)
}
object.TextObject.Size = args[4].Float()
_, layout := object.TextObject.TextLayout()
if err := currentEditor.LayoutText(&object.TextObject, args[2].String(), layout); err != nil {
return nil, err
}
}
if !args[5].IsNull() {
if err := setTextColor(&object.TextObject, args[5].String()); err != nil {
return nil, err
}
}
revision := currentEditor.Revision()
if err := currentEditor.UpdateObject(page, id, object); err != nil {
return nil, err
}
if currentEditor.Revision() == revision {
return editorSummary(), nil
}
return previewEditor(currentEditor, currentSession.Renderer.RenderAnnotations)
}
// checkFontGlyphs 只检查候选字体,不注册资源或改变文档
// 入参: data 字体数据, value 待替换文字
// 返回: error 缺字或字体格式错误
func checkFontGlyphs(data []byte, value string) error {
missing, err := (FontFile{Data: data}).MissingGlyphs(0, value)
if err != nil {
return err
}
if missing != "" {
return &ofdgo.MissingGlyphError{Characters: missing}
}
return nil
}
// checkTextFont 为画布输入预检候选字体
// 入参: args 字体数据和输入文字
// 返回: any 空结果, error 缺字或字体格式错误
func checkTextFont(args []js.Value) (any, error) {
data, err := bytesFromJS(args[0])
if err != nil {
return nil, err
}
return nil, checkFontGlyphs(data, args[1].String())
}
// styleText 批量修改文字样式,字体预检通过后交由库原子提交
// 入参: args 页码、对象标识、字体数据、字号和颜色,null字体与颜色保持原值
// 返回: any 文档信息, error 错误信息
func styleText(args []js.Value) (any, error) {
return changeObjects(func() error {
page, ids := args[0].Int(), stringsFromJS(args[1])
style := ofdgo.TextStyle{Size: args[3].Float()}
if !args[2].IsNull() {
data, err := bytesFromJS(args[2])
if err != nil {
return err
}
var value strings.Builder
for _, id := range ids {
object, err := currentEditor.Object(page, id)
if err != nil {
return err
}
if object.Type != "TextObject" {
return fmt.Errorf("object %q is not text", id)
}
value.WriteString(object.TextObject.Text())
}
if err := checkFontGlyphs(data, value.String()); err != nil {
return err
}
style.Font, err = currentEditor.AddFont(FontFile{Data: data}, 0)
if err != nil {
return err
}
}
if !args[4].IsNull() {
var color ofdgo.FillColor
if err := setEditorColor(&color, args[4].String()); err != nil {
return err
}
style.Color = color.Value
}
return currentEditor.StyleText(page, ids, style)
})
}
// setTextColor 将浏览器RGB色值写入文字对象,保留颜色透明度
// 入参: object 文字对象, value 十六进制色值
// 返回: error 错误信息
func setTextColor(object *ofdgo.TextObject, value string) error {
if object.FillColor == nil {
object.FillColor = &ofdgo.FillColor{}
}
return setEditorColor(object.FillColor, value)
}
// insertText 使用选定字体创建文字对象
// 入参: args 页码、内容、字体数据、横纵坐标、字号、颜色、框宽、折行、对齐、行距、字距及左右和首行缩进
// 返回: any 文档信息, error 错误信息
func insertText(args []js.Value) (any, error) {
if currentEditor == nil {
return nil, fmt.Errorf("no document is being edited")
}
data, err := bytesFromJS(args[2])
if err != nil {
return nil, err
}
fontID, err := currentEditor.AddFont(FontFile{Data: data}, 0)
if err != nil {
return nil, err
}
page := args[0].Int()
content, err := currentSession.pageContent(page)
if err != nil {
return nil, err
}
box, err := currentSession.pageBox(page, content)
if err != nil {
return nil, err
}
x, y := args[3].Float(), args[4].Float()
width := args[7].Float()
if width == 0 {
width = 80
}
box = ofdgo.Box{X: x, Y: y, W: math.Min(width, box.W-x), H: box.H - y}
object := ofdgo.GraphicObject{Type: "TextObject", TextObject: ofdgo.TextObject{
Boundary: fmt.Sprintf("%g %g %g %g", box.X, box.Y, box.W, box.H),
Font: fontID, Size: args[5].Float(),
}}
if err := currentEditor.LayoutText(&object.TextObject, args[1].String(), ofdgo.TextLayout{Wrap: args[8].Bool(), Align: args[9].String(), LineHeight: args[10].Float(), LetterSpacing: args[11].Float(), LeftIndent: args[12].Float(), RightIndent: args[13].Float(), FirstLineIndent: args[14].Float()}); err != nil {
return nil, err
}
if err := setTextColor(&object.TextObject, args[6].String()); err != nil {
return nil, err
}
return insertEditorObject(page, object, args[15:])
}
// layoutText 调整文字框和段落排版,保留软换行前的原文
// 入参: args 页码、对象标识、本地左侧偏移、宽度、折行、对齐、行距、字距及左右和首行缩进
// 返回: any 文档信息, error 错误信息
func layoutText(args []js.Value) (any, error) {
if currentEditor == nil {
return nil, fmt.Errorf("no document is being edited")
}
page, id := args[0].Int(), args[1].String()
object, err := currentEditor.Object(page, id)
if err != nil {
return nil, err
}
if object.Type != "TextObject" {
return nil, fmt.Errorf("object is not text")
}
if !args[2].IsNull() {
object.TextObject, err = object.TextObject.ResizeTextFrame(args[2].Float(), args[3].Float())
if err != nil {
return nil, err
}
}
value, _ := object.TextObject.TextLayout()
if err := currentEditor.LayoutText(&object.TextObject, value, ofdgo.TextLayout{Wrap: args[4].Bool(), Align: args[5].String(), LineHeight: args[6].Float(), LetterSpacing: args[7].Float(), LeftIndent: args[8].Float(), RightIndent: args[9].Float(), FirstLineIndent: args[10].Float()}); err != nil {
return nil, err
}
revision := currentEditor.Revision()
if err := currentEditor.UpdateObject(page, id, object); err != nil {
return nil, err
}
if currentEditor.Revision() == revision {
return editorSummary(), nil
}
return previewEditor(currentEditor, currentSession.Renderer.RenderAnnotations)
}
// replaceImage 替换图片资源并更新预览
// 入参: args 页码、对象标识、图片数据和适应方式
// 返回: any 文档信息, error 错误信息
@@ -2651,12 +2781,6 @@ func batchPages(args []js.Value) (any, error) {
return editorPageInfo{info.(editorInfo), index}, nil
}
// editorOutlineInfo 目录操作后的位置
type editorOutlineInfo struct {
editorInfo
OutlinePath []int `json:"outlinePath"`
}
// indexesFromJS 读取整数索引列表
// 入参: value JavaScript数组
// 返回: []int 索引列表
@@ -3007,155 +3131,6 @@ func previewEditor(editor *ofdgo.Editor, annotations bool) (editorInfo, error) {
return editorSummary(), nil
}
// updateText 修改文字,null内容保留定位且允许替换字体,null字体数据和颜色沿用对象属性
// 入参: args 页码、对象标识、内容、字体数据、字号和颜色
// 返回: any 文档信息, error 错误信息
func updateText(args []js.Value) (any, error) {
if currentEditor == nil {
return nil, fmt.Errorf("no document is being edited")
}
page, id := args[0].Int(), args[1].String()
object, err := currentEditor.Object(page, id)
if err != nil {
return nil, err
}
if object.Type != "TextObject" {
return nil, fmt.Errorf("object %q is not text", id)
}
if !args[3].IsNull() {
data, err := bytesFromJS(args[3])
if err != nil {
return nil, err
}
value := object.TextObject.Text()
if !args[2].IsNull() {
value = args[2].String()
}
if err := checkFontGlyphs(data, value); err != nil {
return nil, err
}
object.TextObject.Font, err = currentEditor.AddFont(FontFile{Data: data}, 0)
if err != nil {
return nil, err
}
}
if !args[2].IsNull() {
if args[4].Float() == object.TextObject.Size {
style := ofdgo.TextStyle{Font: object.TextObject.Font}
if !args[5].IsNull() {
var fill ofdgo.FillColor
if err := setEditorColor(&fill, args[5].String()); err != nil {
return nil, err
}
style.Color = fill.Value
}
revision := currentEditor.Revision()
if err := currentEditor.UpdateTextContent(page, id, args[2].String(), style); err != nil {
return nil, err
}
if currentEditor.Revision() == revision {
return editorSummary(), nil
}
return previewEditor(currentEditor, currentSession.Renderer.RenderAnnotations)
}
object.TextObject.Size = args[4].Float()
_, layout := object.TextObject.TextLayout()
if err := currentEditor.LayoutText(&object.TextObject, args[2].String(), layout); err != nil {
return nil, err
}
}
if !args[5].IsNull() {
if err := setTextColor(&object.TextObject, args[5].String()); err != nil {
return nil, err
}
}
revision := currentEditor.Revision()
if err := currentEditor.UpdateObject(page, id, object); err != nil {
return nil, err
}
if currentEditor.Revision() == revision {
return editorSummary(), nil
}
return previewEditor(currentEditor, currentSession.Renderer.RenderAnnotations)
}
// checkFontGlyphs 只检查候选字体,不注册资源或改变文档
// 入参: data 字体数据, value 待替换文字
// 返回: error 缺字或字体格式错误
func checkFontGlyphs(data []byte, value string) error {
missing, err := (FontFile{Data: data}).MissingGlyphs(0, value)
if err != nil {
return err
}
if missing != "" {
return &ofdgo.MissingGlyphError{Characters: missing}
}
return nil
}
// checkTextFont 为画布输入预检候选字体
// 入参: args 字体数据和输入文字
// 返回: any 空结果, error 缺字或字体格式错误
func checkTextFont(args []js.Value) (any, error) {
data, err := bytesFromJS(args[0])
if err != nil {
return nil, err
}
return nil, checkFontGlyphs(data, args[1].String())
}
// styleText 批量修改文字样式,字体预检通过后交由库原子提交
// 入参: args 页码、对象标识、字体数据、字号和颜色,null字体与颜色保持原值
// 返回: any 文档信息, error 错误信息
func styleText(args []js.Value) (any, error) {
return changeObjects(func() error {
page, ids := args[0].Int(), stringsFromJS(args[1])
style := ofdgo.TextStyle{Size: args[3].Float()}
if !args[2].IsNull() {
data, err := bytesFromJS(args[2])
if err != nil {
return err
}
var value strings.Builder
for _, id := range ids {
object, err := currentEditor.Object(page, id)
if err != nil {
return err
}
if object.Type != "TextObject" {
return fmt.Errorf("object %q is not text", id)
}
value.WriteString(object.TextObject.Text())
}
if err := checkFontGlyphs(data, value.String()); err != nil {
return err
}
style.Font, err = currentEditor.AddFont(FontFile{Data: data}, 0)
if err != nil {
return err
}
}
if !args[4].IsNull() {
var color ofdgo.FillColor
if err := setEditorColor(&color, args[4].String()); err != nil {
return err
}
style.Color = color.Value
}
return currentEditor.StyleText(page, ids, style)
})
}
// setTextColor 将浏览器RGB色值写入文字对象,保留颜色透明度
// 入参: object 文字对象, value 十六进制色值
// 返回: error 错误信息
func setTextColor(object *ofdgo.TextObject, value string) error {
if object.FillColor == nil {
object.FillColor = &ofdgo.FillColor{}
}
return setEditorColor(object.FillColor, value)
}
// setEditorColor 写入当前文档的RGB纯色,保留透明度并替换原颜色空间和索引
// 入参: fill 颜色, value 十六进制色值
// 返回: error 错误信息
@@ -3294,84 +3269,6 @@ func updatePathStyle(args []js.Value) (any, error) {
})
}
// insertText 使用选定字体创建文字对象
// 入参: args 页码、内容、字体数据、横纵坐标、字号、颜色、框宽、折行、对齐、行距、字距及左右和首行缩进
// 返回: any 文档信息, error 错误信息
func insertText(args []js.Value) (any, error) {
if currentEditor == nil {
return nil, fmt.Errorf("no document is being edited")
}
data, err := bytesFromJS(args[2])
if err != nil {
return nil, err
}
fontID, err := currentEditor.AddFont(FontFile{Data: data}, 0)
if err != nil {
return nil, err
}
page := args[0].Int()
content, err := currentSession.pageContent(page)
if err != nil {
return nil, err
}
box, err := currentSession.pageBox(page, content)
if err != nil {
return nil, err
}
x, y := args[3].Float(), args[4].Float()
width := args[7].Float()
if width == 0 {
width = 80
}
box = ofdgo.Box{X: x, Y: y, W: math.Min(width, box.W-x), H: box.H - y}
object := ofdgo.GraphicObject{Type: "TextObject", TextObject: ofdgo.TextObject{
Boundary: fmt.Sprintf("%g %g %g %g", box.X, box.Y, box.W, box.H),
Font: fontID, Size: args[5].Float(),
}}
if err := currentEditor.LayoutText(&object.TextObject, args[1].String(), ofdgo.TextLayout{Wrap: args[8].Bool(), Align: args[9].String(), LineHeight: args[10].Float(), LetterSpacing: args[11].Float(), LeftIndent: args[12].Float(), RightIndent: args[13].Float(), FirstLineIndent: args[14].Float()}); err != nil {
return nil, err
}
if err := setTextColor(&object.TextObject, args[6].String()); err != nil {
return nil, err
}
return insertEditorObject(page, object, args[15:])
}
// layoutText 调整文字框和段落排版,保留软换行前的原文
// 入参: args 页码、对象标识、本地左侧偏移、宽度、折行、对齐、行距、字距及左右和首行缩进
// 返回: any 文档信息, error 错误信息
func layoutText(args []js.Value) (any, error) {
if currentEditor == nil {
return nil, fmt.Errorf("no document is being edited")
}
page, id := args[0].Int(), args[1].String()
object, err := currentEditor.Object(page, id)
if err != nil {
return nil, err
}
if object.Type != "TextObject" {
return nil, fmt.Errorf("object is not text")
}
if !args[2].IsNull() {
object.TextObject, err = object.TextObject.ResizeTextFrame(args[2].Float(), args[3].Float())
if err != nil {
return nil, err
}
}
value, _ := object.TextObject.TextLayout()
if err := currentEditor.LayoutText(&object.TextObject, value, ofdgo.TextLayout{Wrap: args[4].Bool(), Align: args[5].String(), LineHeight: args[6].Float(), LetterSpacing: args[7].Float(), LeftIndent: args[8].Float(), RightIndent: args[9].Float(), FirstLineIndent: args[10].Float()}); err != nil {
return nil, err
}
revision := currentEditor.Revision()
if err := currentEditor.UpdateObject(page, id, object); err != nil {
return nil, err
}
if currentEditor.Revision() == revision {
return editorSummary(), nil
}
return previewEditor(currentEditor, currentSession.Renderer.RenderAnnotations)
}
// editorBox 将库层毫米边界传递给画布交互层
// 入参: box 毫米坐标中的矩形范围
// 返回: map[string]any 可直接传递给JavaScript的横纵坐标和宽高
@@ -3631,14 +3528,107 @@ func editorOperationProgress(callback js.Value) func(string, int, int) error {
}
}
// saveDocument 分块写出当前编辑文档或指定页面
// 入参: args 数据写出回调、可选准备进度回调和可选页面索引
// 返回: any 保存结果, error 错误信息
func saveDocument(args []js.Value) (any, error) {
if currentEditor == nil {
return nil, fmt.Errorf("no document is being edited")
// credentialsFromJS 解析本次打开的口令或本机证书私钥
// 入参: value 解锁表单数据
// 返回: ofdgo.Credentials 会话凭据, error 错误信息
func credentialsFromJS(value js.Value) (ofdgo.Credentials, error) {
credentials := ofdgo.Credentials{UserName: value.Get("userName").String()}
if keyValue := value.Get("key"); !keyValue.IsNull() && !keyValue.IsUndefined() {
key, err := bytesFromJS(keyValue)
if err != nil {
return credentials, err
}
defer clear(key)
certificate, err := bytesFromJS(value.Get("certificate"))
if err != nil {
return credentials, err
}
var password []byte
if field := value.Get("keyPassword"); !field.IsUndefined() && !field.IsNull() {
password, err = bytesFromJS(field)
if err != nil {
return credentials, err
}
defer clear(password)
}
signer, certificate, err := ofdgo.ParseSignatureIdentity(certificate, key, password)
if err != nil {
return credentials, ofdgo.ErrInvalidCredentials
}
decrypter, ok := signer.(crypto.Decrypter)
if !ok {
return credentials, ofdgo.ErrInvalidCredentials
}
credentials.Certificate, credentials.Decrypter = certificate, decrypter
return credentials, nil
}
return saveEditor(currentEditor, args)
var err error
credentials.Password, err = bytesFromJS(value.Get("password"))
return credentials, err
}
// securityFilesFromJS 读取会话内的本机证据文件
// 入参: value 二进制数组列表
// 返回: [][]byte 文件内容, error 错误信息
func securityFilesFromJS(value js.Value) ([][]byte, error) {
if value.IsUndefined() || value.IsNull() {
return nil, nil
}
files := make([][]byte, value.Length())
for i := range files {
var err error
files[i], err = bytesFromJS(value.Index(i))
if err != nil {
return nil, err
}
}
return files, nil
}
// verifySignatures 按本机信任锚和离线证据重新验签,不设置联网回调
// 入参: args 信任根、辅助证书、时间戳及撤销证据
// 返回: any 分项报告, error 错误信息
func verifySignatures(args []js.Value) (any, error) {
if currentSession == nil || len(args) == 0 {
return nil, fmt.Errorf("missing signature verification arguments")
}
input := args[0]
files := make(map[string][][]byte)
for _, name := range []string{"roots", "certificates", "timestampRoots", "tokens", "crls", "ocsp"} {
data, err := securityFilesFromJS(input.Get(name))
if err != nil {
return nil, err
}
files[name] = data
}
now := time.Now()
policy := ofdgo.SignaturePolicy{RejectWeakAlgorithms: true, RequireDocumentCoverage: true,
RequireTimestamp: input.Get("requireTimestamp").Bool(), RequireRevocation: input.Get("requireRevocation").Bool()}
options := []ofdgo.SignatureVerifyOption{
ofdgo.WithSignatureVerifyTime(now), ofdgo.WithSignaturePolicy(policy),
ofdgo.WithSignatureTrustCerts(files["roots"]...), ofdgo.WithSignatureCerts(files["certificates"]...),
}
if policy.RequireTimestamp || len(files["timestampRoots"])+len(files["tokens"]) > 0 {
options = append(options, ofdgo.WithSignatureTimestamp(ofdgo.SignatureTimestampOptions{
TrustCerts: files["timestampRoots"], Certificates: files["certificates"], Tokens: files["tokens"], Required: policy.RequireTimestamp, VerifyTime: &now,
}))
}
if policy.RequireRevocation || len(files["crls"])+len(files["ocsp"]) > 0 {
options = append(options, ofdgo.WithSignatureRevocation(ofdgo.SignatureRevocationOptions{
SignatureRevocationEvidence: ofdgo.SignatureRevocationEvidence{CRLs: files["crls"], OCSPResponses: files["ocsp"]},
Certificates: append(files["certificates"], files["roots"]...), Required: policy.RequireRevocation, VerifyTime: &now,
}))
}
reports, err := currentSession.Reader.VerifySignatures(options...)
if err != nil {
return nil, err
}
infos := make([]SignatureInfo, len(reports))
for i, report := range reports {
infos[i] = signatureInfo(report)
}
currentSession.signatures, currentSession.signatureError, currentSession.signaturesRead = infos, nil, true
return map[string]any{"signatures": infos, "signatureCount": len(infos), "signatureError": ""}, nil
}
// saveEncrypted 以独立快照加密另存,不修改当前文档的输出策略
@@ -3790,6 +3780,16 @@ func saveSigned(args []js.Value) (any, error) {
return successResult(map[string]any{"mime": "application/ofd", "label": "OFD"}), nil
}
// saveDocument 分块写出当前编辑文档或指定页面
// 入参: args 数据写出回调、可选准备进度回调和可选页面索引
// 返回: any 保存结果, error 错误信息
func saveDocument(args []js.Value) (any, error) {
if currentEditor == nil {
return nil, fmt.Errorf("no document is being edited")
}
return saveEditor(currentEditor, args)
}
// saveEditor 统一分块写出及进度取消,加密策略由库层执行
// 入参: editor 待写出文档, args 写出回调、进度回调和可选页面索引
// 返回: any 保存结果, error 错误信息
+191 -191
View File
@@ -39,6 +39,97 @@ type FontFile = ofdgo.FontFile
// FontInfo OFD字体信息
type FontInfo = ofdgo.FontInfo
// Session WebUI文档会话
type Session struct {
Reader *ofdgo.Reader
Renderer *ofdgo.Renderer
fontFS *ofdgo.FontFS
doc *ofdgo.Document
pageCache map[int]*ofdgo.PageContent
boxCache []pageBoxInfo
textCache map[int]*ofdgo.PageText
svgFonts map[string][]byte
fontScan *ofdgo.FontInfoScanner
fontInfos []FontInfo
fontsRead bool
fontAnnotations bool
signatures []SignatureInfo
signatureError error
signaturesRead bool
editing bool
}
// pageBoxInfo 按页索引缓存真实区域,valid区分未读取与零值区域
type pageBoxInfo struct {
box ofdgo.Box
valid bool
}
// DocumentInfo 文档信息
type DocumentInfo struct {
Encryption EncryptionInfo `json:"encryption"`
Version string `json:"version"`
DocType string `json:"docType"`
Title string `json:"title"`
Author string `json:"author"`
Subject string `json:"subject"`
CreationDate string `json:"creationDate"`
ModDate string `json:"modDate"`
PageCount int `json:"pageCount"`
FontCount int `json:"fontCount"`
SignatureCount int `json:"signatureCount"`
SignatureError string `json:"signatureError,omitempty"`
AttachmentError string `json:"attachmentError,omitempty"`
Attachments []AttachmentInfo `json:"attachments,omitempty"`
Fonts []FontInfo `json:"fonts"`
Signatures []SignatureInfo `json:"signatures"`
Pages []PageInfo `json:"pages"`
Outlines []OutlineInfo `json:"outlines,omitempty"`
DetailsPending bool `json:"detailsPending,omitempty"`
}
// OutlineInfo 目录节点信息
type OutlineInfo = ofdgo.OutlineInfo
// AttachmentInfo 可见附件信息,Size单位为KB
type AttachmentInfo struct {
ID string `json:"id"`
Name string `json:"name"`
Format string `json:"format,omitempty"`
Size *float64 `json:"size,omitempty"`
FileName string `json:"fileName"`
}
// PageInfo 页面信息
type PageInfo struct {
Index int `json:"index"`
ID string `json:"id"`
Width float64 `json:"width"`
Height float64 `json:"height"`
}
// PageSVG 页面SVG结果
type PageSVG struct {
Index int `json:"index"`
Number int `json:"number"`
ID string `json:"id"`
Width float64 `json:"width"`
Height float64 `json:"height"`
SVG string `json:"svg"`
Links []ofdgo.PageLink `json:"-"`
Fonts []ofdgo.SVGFont `json:"-"`
Images []ofdgo.SVGImage `json:"-"`
Annotations []ofdgo.AnnotationInfo `json:"annotations,omitempty"`
}
// ExportFormat 导出格式
type ExportFormat struct {
Value string `json:"value"`
Label string `json:"label"`
Extension string `json:"extension"`
MIME string `json:"mime"`
}
// SignatureInfo 签名验证信息
type SignatureInfo struct {
DocIndex int `json:"docIndex"`
@@ -144,55 +235,6 @@ type SignatureStampInfo struct {
Height float64 `json:"height,omitempty"`
}
// Session WebUI文档会话
type Session struct {
Reader *ofdgo.Reader
Renderer *ofdgo.Renderer
fontFS *ofdgo.FontFS
doc *ofdgo.Document
pageCache map[int]*ofdgo.PageContent
boxCache []pageBoxInfo
textCache map[int]*ofdgo.PageText
svgFonts map[string][]byte
fontScan *ofdgo.FontInfoScanner
fontInfos []FontInfo
fontsRead bool
fontAnnotations bool
signatures []SignatureInfo
signatureError error
signaturesRead bool
editing bool
}
// pageBoxInfo 按页索引缓存真实区域,valid区分未读取与零值区域
type pageBoxInfo struct {
box ofdgo.Box
valid bool
}
// DocumentInfo 文档信息
type DocumentInfo struct {
Encryption EncryptionInfo `json:"encryption"`
Version string `json:"version"`
DocType string `json:"docType"`
Title string `json:"title"`
Author string `json:"author"`
Subject string `json:"subject"`
CreationDate string `json:"creationDate"`
ModDate string `json:"modDate"`
PageCount int `json:"pageCount"`
FontCount int `json:"fontCount"`
SignatureCount int `json:"signatureCount"`
SignatureError string `json:"signatureError,omitempty"`
AttachmentError string `json:"attachmentError,omitempty"`
Attachments []AttachmentInfo `json:"attachments,omitempty"`
Fonts []FontInfo `json:"fonts"`
Signatures []SignatureInfo `json:"signatures"`
Pages []PageInfo `json:"pages"`
Outlines []OutlineInfo `json:"outlines,omitempty"`
DetailsPending bool `json:"detailsPending,omitempty"`
}
// EncryptionInfo 仅向界面传递非敏感加密状态
type EncryptionInfo struct {
Encrypted bool `json:"encrypted"`
@@ -201,55 +243,6 @@ type EncryptionInfo struct {
Layers int `json:"layers,omitempty"`
}
// encryptionInfo 获取当前文档的加密来源,不包含凭据
// 返回: EncryptionInfo 加密状态
func (s *Session) encryptionInfo() EncryptionInfo {
info := s.Reader.Encryption()
return EncryptionInfo{Encrypted: info.Encrypted, Method: info.Method, Users: info.Users, Layers: info.Layers}
}
// OutlineInfo 目录节点信息
type OutlineInfo = ofdgo.OutlineInfo
// AttachmentInfo 可见附件信息,Size单位为KB
type AttachmentInfo struct {
ID string `json:"id"`
Name string `json:"name"`
Format string `json:"format,omitempty"`
Size *float64 `json:"size,omitempty"`
FileName string `json:"fileName"`
}
// PageInfo 页面信息
type PageInfo struct {
Index int `json:"index"`
ID string `json:"id"`
Width float64 `json:"width"`
Height float64 `json:"height"`
}
// PageSVG 页面SVG结果
type PageSVG struct {
Index int `json:"index"`
Number int `json:"number"`
ID string `json:"id"`
Width float64 `json:"width"`
Height float64 `json:"height"`
SVG string `json:"svg"`
Links []ofdgo.PageLink `json:"-"`
Fonts []ofdgo.SVGFont `json:"-"`
Images []ofdgo.SVGImage `json:"-"`
Annotations []ofdgo.AnnotationInfo `json:"annotations,omitempty"`
}
// ExportFormat 导出格式
type ExportFormat struct {
Value string `json:"value"`
Label string `json:"label"`
Extension string `json:"extension"`
MIME string `json:"mime"`
}
// supportedExportFormats 导出格式列表
var supportedExportFormats = []ExportFormat{
{Value: "pdf", Label: "PDF", Extension: "pdf", MIME: "application/pdf"},
@@ -260,30 +253,6 @@ var supportedExportFormats = []ExportFormat{
{Value: "txt", Label: "TXT", Extension: "txt", MIME: "text/plain"},
}
// ExportFormats 获取导出格式
// 返回: []ExportFormat 导出格式列表
func ExportFormats() []ExportFormat {
formats := make([]ExportFormat, len(supportedExportFormats))
copy(formats, supportedExportFormats)
return formats
}
// exportFormat 获取导出格式
// 入参: value 格式值
// 返回: ExportFormat 导出格式, bool 是否支持
func exportFormat(value string) (ExportFormat, bool) {
value = strings.ToLower(strings.TrimSpace(value))
if value == "jpeg" {
value = "jpg"
}
for _, format := range supportedExportFormats {
if format.Value == value {
return format, true
}
}
return ExportFormat{}, false
}
// Open 打开浏览器内存中的OFD文档
// 入参: data OFD文件数据, opts 打开选项
// 返回: *Session 文档会话, error 错误信息
@@ -647,6 +616,64 @@ func (s *Session) SVGFontData(name string) ([]byte, error) {
return data, nil
}
// pageContent 读取页面,复用最近一次解析结果
// 入参: index 页面索引
// 返回: *ofdgo.PageContent 页面内容, error 错误信息
func (s *Session) pageContent(index int) (*ofdgo.PageContent, error) {
if s == nil || s.Reader == nil || s.Renderer == nil || s.doc == nil {
return nil, fmt.Errorf("ofd document is not opened")
}
if page, ok := s.pageCache[index]; ok {
return page, nil
}
clear(s.pageCache)
page, err := s.Reader.PageContentByIndex(index)
if err != nil {
return nil, err
}
s.pageCache[index] = page
return page, nil
}
// pageBox 获取页面物理区域
// 入参: index 页面索引, page 页面内容
// 返回: ofdgo.Box 页面物理区域, error 错误信息
func (s *Session) pageBox(index int, page *ofdgo.PageContent) (ofdgo.Box, error) {
if cached := s.boxCache[index]; cached.valid {
return cached.box, nil
}
box, err := s.Renderer.GetPageBox(page)
if err != nil {
return ofdgo.Box{}, err
}
s.boxCache[index] = pageBoxInfo{box: box, valid: true}
return box, nil
}
// ExportFormats 获取导出格式
// 返回: []ExportFormat 导出格式列表
func ExportFormats() []ExportFormat {
formats := make([]ExportFormat, len(supportedExportFormats))
copy(formats, supportedExportFormats)
return formats
}
// exportFormat 获取导出格式
// 入参: value 格式值
// 返回: ExportFormat 导出格式, bool 是否支持
func exportFormat(value string) (ExportFormat, bool) {
value = strings.ToLower(strings.TrimSpace(value))
if value == "jpeg" {
value = "jpg"
}
for _, format := range supportedExportFormats {
if format.Value == value {
return format, true
}
}
return ExportFormat{}, false
}
// ExportPage 导出单页
// 入参: index 页面索引, value 导出格式, dpi 图片DPI, writer 输出流
// 返回: ExportFormat 导出格式, error 错误信息
@@ -666,6 +693,48 @@ func (s *Session) ExportPage(index int, value string, dpi float64, writer io.Wri
return format, renderer.RenderTo(page, writer, format.Value)
}
// ExportDocument 导出文档为PDF、TXT或逐页打包ZIP
// 入参: value 导出格式, dpi 图片DPI, writer 输出流, indices 零基页面索引,省略则全部
// 返回: ExportFormat 导出格式, error 错误信息
func (s *Session) ExportDocument(value string, dpi float64, writer io.Writer, indices ...int) (ExportFormat, error) {
format, ok := exportFormat(value)
if !ok {
return ExportFormat{}, fmt.Errorf("unsupported export format %s", value)
}
if format.Value == "pdf" {
return format, s.ExportPDF(writer, indices...)
}
if s == nil || s.Reader == nil || s.Renderer == nil || s.doc == nil {
return ExportFormat{}, fmt.Errorf("ofd document is not opened")
}
if format.Value == "txt" {
return format, s.Renderer.RenderToMultiPageText(writer, indices...)
}
renderer := *s.Renderer
if dpi > 0 && (format.Value == "png" || format.Value == "jpg") {
renderer.DPI = dpi
}
err := renderer.RenderToZIP(writer, format.Value, indices...)
return ExportFormat{Value: "zip", Label: "ZIP", Extension: "zip", MIME: "application/zip"}, err
}
// ExportPDF 导出文档为PDF
// 入参: writer 输出流, indices 零基页面索引,省略则全部
// 返回: error 错误信息
func (s *Session) ExportPDF(writer io.Writer, indices ...int) error {
if s == nil || s.Reader == nil || s.Renderer == nil || s.doc == nil {
return fmt.Errorf("ofd document is not opened")
}
return s.Renderer.RenderToMultiPagePDF(writer, indices...)
}
// encryptionInfo 获取当前文档的加密来源,不包含凭据
// 返回: EncryptionInfo 加密状态
func (s *Session) encryptionInfo() EncryptionInfo {
info := s.Reader.Encryption()
return EncryptionInfo{Encrypted: info.Encrypted, Method: info.Method, Users: info.Users, Layers: info.Layers}
}
// signatureInfos 获取签名验证信息
// 返回: []SignatureInfo 签名验证信息, error 错误信息
func (s *Session) signatureInfos() ([]SignatureInfo, error) {
@@ -855,72 +924,3 @@ func signatureStampInfos(positions []ofdgo.SignatureStampPosition) []SignatureSt
}
return infos
}
// ExportDocument 导出文档为PDF、TXT或逐页打包ZIP
// 入参: value 导出格式, dpi 图片DPI, writer 输出流, indices 零基页面索引,省略则全部
// 返回: ExportFormat 导出格式, error 错误信息
func (s *Session) ExportDocument(value string, dpi float64, writer io.Writer, indices ...int) (ExportFormat, error) {
format, ok := exportFormat(value)
if !ok {
return ExportFormat{}, fmt.Errorf("unsupported export format %s", value)
}
if format.Value == "pdf" {
return format, s.ExportPDF(writer, indices...)
}
if s == nil || s.Reader == nil || s.Renderer == nil || s.doc == nil {
return ExportFormat{}, fmt.Errorf("ofd document is not opened")
}
if format.Value == "txt" {
return format, s.Renderer.RenderToMultiPageText(writer, indices...)
}
renderer := *s.Renderer
if dpi > 0 && (format.Value == "png" || format.Value == "jpg") {
renderer.DPI = dpi
}
err := renderer.RenderToZIP(writer, format.Value, indices...)
return ExportFormat{Value: "zip", Label: "ZIP", Extension: "zip", MIME: "application/zip"}, err
}
// ExportPDF 导出文档为PDF
// 入参: writer 输出流, indices 零基页面索引,省略则全部
// 返回: error 错误信息
func (s *Session) ExportPDF(writer io.Writer, indices ...int) error {
if s == nil || s.Reader == nil || s.Renderer == nil || s.doc == nil {
return fmt.Errorf("ofd document is not opened")
}
return s.Renderer.RenderToMultiPagePDF(writer, indices...)
}
// pageContent 读取页面,复用最近一次解析结果
// 入参: index 页面索引
// 返回: *ofdgo.PageContent 页面内容, error 错误信息
func (s *Session) pageContent(index int) (*ofdgo.PageContent, error) {
if s == nil || s.Reader == nil || s.Renderer == nil || s.doc == nil {
return nil, fmt.Errorf("ofd document is not opened")
}
if page, ok := s.pageCache[index]; ok {
return page, nil
}
clear(s.pageCache)
page, err := s.Reader.PageContentByIndex(index)
if err != nil {
return nil, err
}
s.pageCache[index] = page
return page, nil
}
// pageBox 获取页面物理区域
// 入参: index 页面索引, page 页面内容
// 返回: ofdgo.Box 页面物理区域, error 错误信息
func (s *Session) pageBox(index int, page *ofdgo.PageContent) (ofdgo.Box, error) {
if cached := s.boxCache[index]; cached.valid {
return cached.box, nil
}
box, err := s.Renderer.GetPageBox(page)
if err != nil {
return ofdgo.Box{}, err
}
s.boxCache[index] = pageBoxInfo{box: box, valid: true}
return box, nil
}
+17 -69
View File
@@ -20,7 +20,6 @@ import (
"image"
"io"
"reflect"
"sync"
)
// ErrBackendUnavailable 表示未配置所需后端,不会自动切换其他实现
@@ -106,83 +105,32 @@ type EPSBackend interface {
// RenderBackends 按职责组合后端,nil表示明确禁用,不隐式回退
// 实例及其Renderer需串行使用,第三方实现无需引用任何内置绘图库
type RenderBackends struct {
Resources FontResourceBackend
Fonts FontBackend
Geometry GeometryBackend
Compiler PageCompiler
Raster RasterBackend
SVG SVGBackend
PDF PDFBackend
EPS EPSBackend
FontResources FontResourceBackend
Fonts FontBackend
Geometry GeometryBackend
Compiler PageCompiler
Raster RasterBackend
SVG SVGBackend
PDF PDFBackend
EPS EPSBackend
}
// BackendInfo 描述各项能力实际使用的后端,空标识表示未配置
type BackendInfo struct {
Resources string `json:"resources"`
Fonts string `json:"fonts"`
Geometry string `json:"geometry"`
Compiler string `json:"compiler"`
Raster string `json:"raster"`
SVG string `json:"svg"`
PDF string `json:"pdf"`
EPS string `json:"eps"`
}
// NewRenderBackends 创建已注册后端组合,各项能力通过Info报告实际提供者
// 入参: name 已注册后端标识
// 返回: RenderBackends 后端组合, error 未知后端错误
func NewRenderBackends(name string) (RenderBackends, error) {
backendRegistry.RLock()
factory := backendRegistry.factories[name]
backendRegistry.RUnlock()
if factory == nil {
return RenderBackends{}, fmt.Errorf("unknown render backend %q", name)
}
return factory(), nil
}
// RenderBackendInfos 列出已注册组合及其实际能力,供原生程序和WASM共用
// 返回: map[string]BackendInfo 后端能力
func RenderBackendInfos() map[string]BackendInfo {
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
}
// backendRegistry 保存后端工厂,不共享有状态的后端实例
var backendRegistry = struct {
sync.RWMutex
factories map[string]func() RenderBackends
}{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
FontResources string `json:"fontResources"`
Fonts string `json:"fonts"`
Geometry string `json:"geometry"`
Compiler string `json:"compiler"`
Raster string `json:"raster"`
SVG string `json:"svg"`
PDF string `json:"pdf"`
EPS string `json:"eps"`
}
// Info 返回配置中各能力的提供者,不依赖WebUI推断
// 返回: BackendInfo 后端能力
func (b RenderBackends) Info() BackendInfo {
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)}
return BackendInfo{FontResources: backendName(b.FontResources), 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 获取已配置的后端标识
+71
View File
@@ -0,0 +1,71 @@
// 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"
"sync"
)
// backendRegistry 保存后端工厂,不共享有状态的后端实例
var backendRegistry = struct {
sync.RWMutex
factories map[string]func() RenderBackends
}{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
}
// NewRenderBackends 创建已注册后端组合,各项能力通过Info报告实际提供者
// 入参: name 已注册后端标识
// 返回: RenderBackends 后端组合, error 未知后端错误
func NewRenderBackends(name string) (RenderBackends, error) {
backendRegistry.RLock()
factory := backendRegistry.factories[name]
backendRegistry.RUnlock()
if factory == nil {
return RenderBackends{}, fmt.Errorf("unknown render backend %q", name)
}
return factory(), nil
}
// RenderBackendInfos 列出已注册组合及其实际能力,供原生程序和WASM共用
// 返回: map[string]BackendInfo 后端能力
func RenderBackendInfos() map[string]BackendInfo {
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
}
+1 -35
View File
@@ -85,7 +85,7 @@ func (e *Editor) SetPageCompiler(compiler PageCompiler) {
// 入参: backends 后端组合,未配置的能力不会沿用默认实现
func (e *Editor) SetRenderBackends(backends RenderBackends) {
fontsChanged := !sameBackend(e.backends.Fonts, backends.Fonts)
if !sameBackend(e.backends.Resources, backends.Resources) {
if !sameBackend(e.backends.FontResources, backends.FontResources) {
e.fonts = make(map[string]*FontResource)
for i := range e.resources {
e.resources[i].subset = nil
@@ -565,40 +565,6 @@ func (e *Editor) nextID() string {
return strconv.Itoa(e.maxID)
}
// AddFont 注册OpenType字体,集合字体按索引提取,重复资源复用标识,引用后写入文档
// 入参: file 字体文件, index 集合内字体索引,单字体为0
// 返回: string 字体资源标识, error 错误信息
func (e *Editor) AddFont(file FontFile, index int) (string, error) {
if e.backends.Resources == nil {
return "", fmt.Errorf("font resources: %w", ErrBackendUnavailable)
}
parsed, err := e.backends.Resources.OpenFontResource(file, index)
if err != nil {
return "", err
}
data := parsed.Data
key := editorResourceKey{checksum: sha256.Sum256(data)}
if id, ok := e.resourceID[key]; ok {
return id, nil
}
if err := e.prepareSourceIDs(); err != nil {
return "", err
}
id := e.nextID()
definition := parsed.Font
definition.ID = id
resource := editorResource{
name: e.packageName("Res/Fonts/Font_" + id + parsed.Extension),
data: data,
font: &definition,
}
resource.font.FontFile = "/" + resource.name
e.resources = append(e.resources, resource)
e.fonts[id] = parsed
e.resourceID[key] = id
return id, nil
}
// AddImage 注册PNG或JPEG图片,重复资源复用标识,引用后写入文档
// 不透明纯黑白PNG仅在无损JBIG2编码更小时转换,其他图片保留原始编码
// 入参: data 图片数据
+49 -151
View File
@@ -16,14 +16,12 @@ package ofdgo
import (
"bytes"
"encoding/binary"
"crypto/sha256"
"fmt"
"maps"
"slices"
"strconv"
"strings"
"github.com/tdewolff/font"
)
// editorFontUsage 汇总原文档字体的字符、显式字形及无法确定的引用
@@ -33,6 +31,47 @@ type editorFontUsage struct {
unsafe bool
}
// editorFontSubset 保存单个新增字体最近一次裁剪结果,不保留历史字形集合
type editorFontSubset struct {
glyphs []uint16
data []byte
mapped bool
}
// AddFont 注册OpenType字体,集合字体按索引提取,重复资源复用标识,引用后写入文档
// 入参: file 字体文件, index 集合内字体索引,单字体为0
// 返回: string 字体资源标识, error 错误信息
func (e *Editor) AddFont(file FontFile, index int) (string, error) {
if e.backends.FontResources == nil {
return "", fmt.Errorf("font resources: %w", ErrBackendUnavailable)
}
parsed, err := e.backends.FontResources.OpenFontResource(file, index)
if err != nil {
return "", err
}
data := parsed.Data
key := editorResourceKey{checksum: sha256.Sum256(data)}
if id, ok := e.resourceID[key]; ok {
return id, nil
}
if err := e.prepareSourceIDs(); err != nil {
return "", err
}
id := e.nextID()
definition := parsed.Font
definition.ID = id
resource := editorResource{
name: e.packageName("Res/Fonts/Font_" + id + parsed.Extension),
data: data,
font: &definition,
}
resource.font.FontFile = "/" + resource.name
e.resources = append(e.resources, resource)
e.fonts[id] = parsed
e.resourceID[key] = id
return id, nil
}
// newEditorFontUsage 创建字体用字记录
// 返回: *editorFontUsage 用字记录
func newEditorFontUsage() *editorFontUsage {
@@ -84,110 +123,11 @@ func (u *editorFontUsage) merge(other *editorFontUsage, unsafe bool) {
}
}
// subsetSourceFont 裁剪静态TrueType原有字体,保留字形编号、复合依赖、度量及提示指令
// 集合、字形替换、可变、彩色和未知表保持原样,不对不完整的引用或异常字体猜测修复
// 入参: data 字体数据, usage 全包用字记录
// 返回: []byte 更小的字体子集,无确定收益时为空
func subsetSourceFont(data []byte, usage *editorFontUsage) []byte {
if usage.unsafe || len(usage.chars)+len(usage.glyphs) == 0 || bytes.HasPrefix(data, []byte("ttcf")) {
return nil
}
sfnt, err := font.ParseSFNT(data, 0)
if err != nil || !sfnt.IsTrueType {
return nil
}
for tag := range sfnt.Tables {
switch tag {
case "cmap", "head", "hhea", "hmtx", "maxp", "OS/2", "post", "name", "glyf", "loca", "cvt ", "fpgm", "prep", "gasp", "kern", "vhea", "vmtx", "hdmx", "LTSH", "VDMX", "GDEF", "GPOS", "DSIG", "FFTM":
default:
return nil
}
}
var unicodeTables []uint16
subtables := make(map[uint32]uint16)
for i, record := range sfnt.Cmap.EncodingRecords {
if record.Format == 14 {
return nil
}
offset := binary.BigEndian.Uint32(sfnt.Tables["cmap"][8+8*i:])
index, ok := subtables[offset]
if !ok {
index = uint16(len(subtables))
subtables[offset] = index
}
if record.PlatformID == 0 || record.PlatformID == 3 && (record.EncodingID == 1 || record.EncodingID == 10) {
unicodeTables = append(unicodeTables, index)
}
}
if len(unicodeTables) == 0 {
return nil
}
glyphs := maps.Clone(usage.glyphs)
glyphs[0] = true
mapping := make(map[rune]uint16, len(usage.chars))
for char := range usage.chars {
id := sfnt.GlyphIndex(char)
for _, index := range unicodeTables {
if alternate, ok := sfnt.Cmap.Subtables[index].Get(char); ok && alternate != id {
return nil
}
}
glyphs[id] = true
if id != 0 {
mapping[char] = id
}
}
for id := range glyphs {
if id >= sfnt.NumGlyphs() {
return nil
}
dependencies, err := sfnt.Glyf.Dependencies(id)
if err != nil {
return nil
}
for _, dependency := range dependencies {
glyphs[dependency] = true
}
}
var glyf, loca []byte
for id := 0; id <= int(sfnt.NumGlyphs()); id++ {
if sfnt.Head.IndexToLocFormat == 0 {
loca = binary.BigEndian.AppendUint16(loca, uint16(len(glyf)/2))
} else {
loca = binary.BigEndian.AppendUint32(loca, uint32(len(glyf)))
}
if id < int(sfnt.NumGlyphs()) && glyphs[uint16(id)] {
glyf = append(glyf, sfnt.Glyf.Get(uint16(id))...)
if len(glyf)%2 != 0 {
glyf = append(glyf, 0)
}
}
}
tables := maps.Clone(sfnt.Tables)
tables["glyf"], tables["loca"] = glyf, loca
tables["cmap"] = buildCmapTable(uint16(sfnt.NumGlyphs()), mapping)
tables["head"] = bytes.Clone(tables["head"])
clear(tables["head"][8:12])
delete(tables, "DSIG")
result, err := serializeOTF(tables)
if err != nil || len(result) >= len(data) {
return nil
}
return result
}
// editorFontSubset 保存单个新增字体最近一次裁剪结果,不保留历史字形集合
type editorFontSubset struct {
glyphs []uint16
data []byte
mapped bool
}
// subsetFonts 收集新增字体实际使用的字形,仅裁剪保存结果,保留完整编辑字体
// 入参: progress 保存进度回调
// 返回: map[string][]byte 包内字体子集, error 错误信息
func (e *Editor) subsetFonts(progress editorProgress) (map[string][]byte, error) {
if e.backends.Resources == nil {
if e.backends.FontResources == nil {
return nil, nil
}
used := make(map[string]map[uint16]bool)
@@ -196,16 +136,16 @@ func (e *Editor) subsetFonts(progress editorProgress) (map[string][]byte, error)
if resource.font == nil {
continue
}
sfnt := e.fonts[resource.font.ID]
if sfnt == nil {
resourceFont := e.fonts[resource.font.ID]
if resourceFont == nil {
var err error
sfnt, err = e.backends.Resources.OpenFontResource(FontFile{Data: resource.data}, 0)
resourceFont, err = e.backends.FontResources.OpenFontResource(FontFile{Data: resource.data}, 0)
if err != nil {
return nil, err
}
e.fonts[resource.font.ID] = sfnt
e.fonts[resource.font.ID] = resourceFont
}
if sfnt.CanSubset {
if resourceFont.CanSubset {
used[resource.font.ID] = make(map[uint16]bool)
}
}
@@ -320,7 +260,7 @@ func (e *Editor) subsetFonts(progress editorProgress) (map[string][]byte, error)
}
slices.Sort(ids)
if resource.subset == nil || resource.subset.mapped != mapped[resource.font.ID] || !slices.Equal(resource.subset.glyphs, ids) {
data, err := e.backends.Resources.SubsetFont(resource.data, ids, mapped[resource.font.ID])
data, err := e.backends.FontResources.SubsetFont(resource.data, ids, mapped[resource.font.ID])
if err != nil {
return nil, fmt.Errorf("subset font %s: %w", resource.font.FontName, err)
}
@@ -338,45 +278,3 @@ func (e *Editor) subsetFonts(progress editorProgress) (map[string][]byte, error)
}
return result, nil
}
// subsetEditorFont 裁剪TrueType字形及复合依赖,保留字符映射、名称、度量和提示指令
// 入参: data 字体数据, glyphs 已排序的字形编号,包含0, mapped 是否保留显式字形编号
// 返回: []byte 字体子集, error 错误信息
func subsetEditorFont(data []byte, glyphs []uint16, mapped bool) ([]byte, error) {
sfnt, err := font.ParseSFNT(bytes.Clone(data), 0)
if err != nil {
return nil, err
}
if mapped {
usage := newEditorFontUsage()
for _, id := range glyphs {
usage.glyphs[id] = true
for _, char := range sfnt.Cmap.ToUnicode(id) {
usage.chars[char] = true
}
}
tables := make(map[string][]byte)
for _, tag := range []string{"cmap", "head", "hhea", "hmtx", "maxp", "OS/2", "post", "name", "glyf", "loca", "cvt ", "fpgm", "prep", "gasp"} {
if table := sfnt.Tables[tag]; table != nil {
tables[tag] = table
}
}
fixed, err := serializeOTF(tables)
if err != nil {
return nil, err
}
if subset := subsetSourceFont(fixed, usage); subset != nil {
return subset, nil
}
return fixed, nil
}
subset, err := sfnt.Subset(glyphs, font.SubsetOptions{Tables: []string{
"cmap", "head", "hhea", "hmtx", "maxp", "OS/2", "post", "glyf", "loca", "cvt ", "fpgm", "prep", "gasp",
}})
if err != nil {
return nil, err
}
subset.Tables["name"] = sfnt.Tables["name"]
clear(subset.Tables["head"][8:12])
return serializeOTF(subset.Tables)
}
+149
View File
@@ -0,0 +1,149 @@
// 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"
"encoding/xml"
"fmt"
"reflect"
)
// sourceInfoXML 更新公开元数据,保留原创建程序和未识别字段
// 返回: []byte 根索引XML, error 错误信息
func (e *Editor) sourceInfoXML() ([]byte, error) {
data, err := e.source.reader.readFile("OFD.xml")
if err != nil {
return nil, err
}
root, err := parseEditorXML(data)
if err != nil {
return nil, err
}
body := root.child("DocBody")
if body == nil {
return nil, fmt.Errorf("document has no DocBody")
}
info := body.child("DocInfo")
if info == nil {
return nil, fmt.Errorf("document has no DocInfo")
}
before, after := e.source.info, e.Info
var values [][2]string
for _, field := range [][3]string{
{"DocID", before.DocID, after.DocID}, {"Title", before.Title, after.Title}, {"Author", before.Author, after.Author},
{"Subject", before.Subject, after.Subject}, {"Abstract", before.Abstract, after.Abstract},
{"CreationDate", before.CreationDate, after.CreationDate}, {"ModDate", before.ModDate, after.ModDate},
} {
if field[1] != field[2] {
values = append(values, [2]string{field[0], field[2]})
}
}
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
}
-127
View File
@@ -421,133 +421,6 @@ func (e *Editor) sourceNewPageXML(page PageContent) ([]byte, error) {
return editorXMLContainer("Page", nil, append(area, content...))
}
// sourceInfoXML 更新公开元数据,保留原创建程序和未识别字段
// 返回: []byte 根索引XML, error 错误信息
func (e *Editor) sourceInfoXML() ([]byte, error) {
data, err := e.source.reader.readFile("OFD.xml")
if err != nil {
return nil, err
}
root, err := parseEditorXML(data)
if err != nil {
return nil, err
}
body := root.child("DocBody")
if body == nil {
return nil, fmt.Errorf("document has no DocBody")
}
info := body.child("DocInfo")
if info == nil {
return nil, fmt.Errorf("document has no DocInfo")
}
before, after := e.source.info, e.Info
var values [][2]string
for _, field := range [][3]string{
{"DocID", before.DocID, after.DocID}, {"Title", before.Title, after.Title}, {"Author", before.Author, after.Author},
{"Subject", before.Subject, after.Subject}, {"Abstract", before.Abstract, after.Abstract},
{"CreationDate", before.CreationDate, after.CreationDate}, {"ModDate", before.ModDate, after.ModDate},
} {
if field[1] != field[2] {
values = append(values, [2]string{field[0], field[2]})
}
}
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条目直接复制到新包,逐项写入改动,不持有原资源解压副本
// 入参: writer 输出流, fonts 新增字体子集, progress 保存进度回调
// 返回: int64 写入字节数, error 错误信息
+2 -2
View File
@@ -173,14 +173,14 @@ func (e *Editor) compactSourceResources(parts map[string][]byte, progress editor
}
key := strings.ToLower(cleanPackagePath(name))
usage := fontFiles[key]
if e.backends.Resources == nil || usage == nil || usage.unsafe || refs.files[key] || removed[key] || parts[name] != nil {
if e.backends.FontResources == nil || usage == nil || usage.unsafe || refs.files[key] || removed[key] || parts[name] != nil {
continue
}
data, err := reader.readFile(name)
if err != nil {
return nil, err
}
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})
subset, err := e.backends.FontResources.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)
}
-91
View File
@@ -14,13 +14,6 @@
package ofdgo
import (
"fmt"
"slices"
"github.com/tdewolff/font"
)
// FontResource 保存独立字体数据、标准元信息与只读度量,不包含绘图库类型
type FontResource struct {
FontMetrics
@@ -45,87 +38,3 @@ type FontResourceBackend interface {
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 ""
}
+243
View File
@@ -0,0 +1,243 @@
// 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"
"encoding/binary"
"fmt"
"maps"
"slices"
"github.com/tdewolff/font"
)
// SFNTBackend 提供默认字体资源处理,不依赖Canvas绘图库
type SFNTBackend struct{}
// sfntResourceMetrics 隔离资源解析器的度量实现
type sfntResourceMetrics struct{ *font.SFNT }
// 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 := sfntFontName(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: sfntFontName(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
}
// SubsetFont 裁剪新建字体并按要求保留显式字形编号
// 入参: data 字体数据, glyphs 用字, preserve 是否保留编号
// 返回: []byte 子集, error 裁剪错误
func (SFNTBackend) SubsetFont(data []byte, glyphs []uint16, preserve bool) ([]byte, error) {
return subsetSFNTFont(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 subsetSourceSFNTFont(data, u), nil
}
// Write 返回未改动元信息的字体数据
// 返回: []byte 字体数据
func (f sfntResourceMetrics) Write() []byte { return fontSFNTData(f.SFNT) }
// 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 })
}
// sfntFontName 获取字体中的名称
// 入参: sfnt 字体, names 名称类型,按顺序查找
// 返回: string 字体名称
func sfntFontName(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 ""
}
// subsetSFNTFont 裁剪TrueType字形及复合依赖,保留字符映射、名称、度量和提示指令
// 入参: data 字体数据, glyphs 已排序的字形编号,包含0, mapped 是否保留显式字形编号
// 返回: []byte 字体子集, error 错误信息
func subsetSFNTFont(data []byte, glyphs []uint16, mapped bool) ([]byte, error) {
sfnt, err := font.ParseSFNT(bytes.Clone(data), 0)
if err != nil {
return nil, err
}
if mapped {
usage := newEditorFontUsage()
for _, id := range glyphs {
usage.glyphs[id] = true
for _, char := range sfnt.Cmap.ToUnicode(id) {
usage.chars[char] = true
}
}
tables := make(map[string][]byte)
for _, tag := range []string{"cmap", "head", "hhea", "hmtx", "maxp", "OS/2", "post", "name", "glyf", "loca", "cvt ", "fpgm", "prep", "gasp"} {
if table := sfnt.Tables[tag]; table != nil {
tables[tag] = table
}
}
fixed, err := serializeOTF(tables)
if err != nil {
return nil, err
}
if subset := subsetSourceSFNTFont(fixed, usage); subset != nil {
return subset, nil
}
return fixed, nil
}
subset, err := sfnt.Subset(glyphs, font.SubsetOptions{Tables: []string{
"cmap", "head", "hhea", "hmtx", "maxp", "OS/2", "post", "glyf", "loca", "cvt ", "fpgm", "prep", "gasp",
}})
if err != nil {
return nil, err
}
subset.Tables["name"] = sfnt.Tables["name"]
clear(subset.Tables["head"][8:12])
return serializeOTF(subset.Tables)
}
// subsetSourceSFNTFont 裁剪静态TrueType原有字体,保留字形编号、复合依赖、度量及提示指令
// 集合、字形替换、可变、彩色和未知表保持原样,不对不完整的引用或异常字体猜测修复
// 入参: data 字体数据, usage 全包用字记录
// 返回: []byte 更小的字体子集,无确定收益时为空
func subsetSourceSFNTFont(data []byte, usage *editorFontUsage) []byte {
if usage.unsafe || len(usage.chars)+len(usage.glyphs) == 0 || bytes.HasPrefix(data, []byte("ttcf")) {
return nil
}
sfnt, err := font.ParseSFNT(data, 0)
if err != nil || !sfnt.IsTrueType {
return nil
}
for tag := range sfnt.Tables {
switch tag {
case "cmap", "head", "hhea", "hmtx", "maxp", "OS/2", "post", "name", "glyf", "loca", "cvt ", "fpgm", "prep", "gasp", "kern", "vhea", "vmtx", "hdmx", "LTSH", "VDMX", "GDEF", "GPOS", "DSIG", "FFTM":
default:
return nil
}
}
var unicodeTables []uint16
subtables := make(map[uint32]uint16)
for i, record := range sfnt.Cmap.EncodingRecords {
if record.Format == 14 {
return nil
}
offset := binary.BigEndian.Uint32(sfnt.Tables["cmap"][8+8*i:])
index, ok := subtables[offset]
if !ok {
index = uint16(len(subtables))
subtables[offset] = index
}
if record.PlatformID == 0 || record.PlatformID == 3 && (record.EncodingID == 1 || record.EncodingID == 10) {
unicodeTables = append(unicodeTables, index)
}
}
if len(unicodeTables) == 0 {
return nil
}
glyphs := maps.Clone(usage.glyphs)
glyphs[0] = true
mapping := make(map[rune]uint16, len(usage.chars))
for char := range usage.chars {
id := sfnt.GlyphIndex(char)
for _, index := range unicodeTables {
if alternate, ok := sfnt.Cmap.Subtables[index].Get(char); ok && alternate != id {
return nil
}
}
glyphs[id] = true
if id != 0 {
mapping[char] = id
}
}
for id := range glyphs {
if id >= sfnt.NumGlyphs() {
return nil
}
dependencies, err := sfnt.Glyf.Dependencies(id)
if err != nil {
return nil
}
for _, dependency := range dependencies {
glyphs[dependency] = true
}
}
var glyf, loca []byte
for id := 0; id <= int(sfnt.NumGlyphs()); id++ {
if sfnt.Head.IndexToLocFormat == 0 {
loca = binary.BigEndian.AppendUint16(loca, uint16(len(glyf)/2))
} else {
loca = binary.BigEndian.AppendUint32(loca, uint32(len(glyf)))
}
if id < int(sfnt.NumGlyphs()) && glyphs[uint16(id)] {
glyf = append(glyf, sfnt.Glyf.Get(uint16(id))...)
if len(glyf)%2 != 0 {
glyf = append(glyf, 0)
}
}
}
tables := maps.Clone(sfnt.Tables)
tables["glyf"], tables["loca"] = glyf, loca
tables["cmap"] = buildCmapTable(uint16(sfnt.NumGlyphs()), mapping)
tables["head"] = bytes.Clone(tables["head"])
clear(tables["head"][8:12])
delete(tables, "DSIG")
result, err := serializeOTF(tables)
if err != nil || len(result) >= len(data) {
return nil
}
return result
}
+1 -1
View File
@@ -40,7 +40,7 @@ func init() {
// 返回: RenderBackends 默认后端组合
func defaultRenderBackends() RenderBackends {
backend := CanvasBackend{}
return RenderBackends{Resources: SFNTBackend{}, Fonts: backend, Geometry: backend, Compiler: backend, Raster: backend, SVG: backend, PDF: backend, EPS: backend}
return RenderBackends{FontResources: SFNTBackend{}, Fonts: backend, Geometry: backend, Compiler: backend, Raster: backend, SVG: backend, PDF: backend, EPS: backend}
}
// Name 返回后端标识