mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 15:06:46 +08:00
1 parent
81e6657c65
commit
0508029387
13 files changed
+1121
-1065
No files matched your search
@@ -26,6 +26,7 @@ let wasmRequestID = 0;
|
|||||||
let wasmRecoveryTimer = 0;
|
let wasmRecoveryTimer = 0;
|
||||||
let textMeasure = null;
|
let textMeasure = null;
|
||||||
let credentialRequest = null;
|
let credentialRequest = null;
|
||||||
|
let cancelPageTouch = null;
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
composite: null,
|
composite: null,
|
||||||
@@ -5570,8 +5571,6 @@ el.finishPageSelection.addEventListener("click", () => {
|
|||||||
updateControls();
|
updateControls();
|
||||||
});
|
});
|
||||||
|
|
||||||
let cancelPageTouch = null;
|
|
||||||
|
|
||||||
el.pageList.addEventListener("pointerdown", event => {
|
el.pageList.addEventListener("pointerdown", event => {
|
||||||
cancelPageTouch?.();
|
cancelPageTouch?.();
|
||||||
state.pageTouchPointer = null;
|
state.pageTouchPointer = null;
|
||||||
|
|||||||
+410
-410
@@ -45,6 +45,79 @@ type exportWriter struct {
|
|||||||
write js.Value
|
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 将数据块交给浏览器保存
|
// Write 将数据块交给浏览器保存
|
||||||
// 入参: data 导出数据
|
// 入参: data 导出数据
|
||||||
// 返回: int 写入长度, error 错误信息
|
// 返回: int 写入长度, error 错误信息
|
||||||
@@ -80,25 +153,6 @@ func awaitExport(fn js.Value, args ...any) error {
|
|||||||
return <-done
|
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接口并阻塞运行
|
// RunWASM 注册浏览器WASM接口并阻塞运行
|
||||||
func RunWASM() {
|
func RunWASM() {
|
||||||
registerCallback("ofdgoOpen", openDocument)
|
registerCallback("ofdgoOpen", openDocument)
|
||||||
@@ -297,45 +351,6 @@ func openDocument(args []js.Value) (any, error) {
|
|||||||
return currentSession.Summary(), nil
|
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 更新当前会话的字体和注解设置
|
// configureDocument 更新当前会话的字体和注解设置
|
||||||
// 入参: args 浏览器参数
|
// 入参: args 浏览器参数
|
||||||
// 返回: any 文档信息, error 错误信息
|
// 返回: any 文档信息, error 错误信息
|
||||||
@@ -378,70 +393,6 @@ func documentInfo(args []js.Value) (any, error) {
|
|||||||
return currentSession.Info(), nil
|
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 分块导出附件
|
// exportAttachment 分块导出附件
|
||||||
// 入参: args 浏览器参数
|
// 入参: args 浏览器参数
|
||||||
// 返回: any 附件数据, error 错误信息
|
// 返回: any 附件数据, error 错误信息
|
||||||
@@ -820,42 +771,6 @@ func encodeResult(result apiResult) string {
|
|||||||
return string(data)
|
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 获取当前编辑文档状态
|
// editorSummary 获取当前编辑文档状态
|
||||||
// 返回: editorInfo 文档信息
|
// 返回: editorInfo 文档信息
|
||||||
func 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 获取编辑面板所需的注解内容,不修改文件
|
// readAnnotation 获取编辑面板所需的注解内容,不修改文件
|
||||||
// 入参: args 页面索引与注解标识
|
// 入参: args 页面索引与注解标识
|
||||||
// 返回: any 面板数据, error 错误信息
|
// 返回: 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
|
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 替换图片资源并更新预览
|
// replaceImage 替换图片资源并更新预览
|
||||||
// 入参: args 页码、对象标识、图片数据和适应方式
|
// 入参: args 页码、对象标识、图片数据和适应方式
|
||||||
// 返回: any 文档信息, error 错误信息
|
// 返回: any 文档信息, error 错误信息
|
||||||
@@ -2651,12 +2781,6 @@ func batchPages(args []js.Value) (any, error) {
|
|||||||
return editorPageInfo{info.(editorInfo), index}, nil
|
return editorPageInfo{info.(editorInfo), index}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// editorOutlineInfo 目录操作后的位置
|
|
||||||
type editorOutlineInfo struct {
|
|
||||||
editorInfo
|
|
||||||
OutlinePath []int `json:"outlinePath"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// indexesFromJS 读取整数索引列表
|
// indexesFromJS 读取整数索引列表
|
||||||
// 入参: value JavaScript数组
|
// 入参: value JavaScript数组
|
||||||
// 返回: []int 索引列表
|
// 返回: []int 索引列表
|
||||||
@@ -3007,155 +3131,6 @@ func previewEditor(editor *ofdgo.Editor, annotations bool) (editorInfo, error) {
|
|||||||
return editorSummary(), nil
|
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纯色,保留透明度并替换原颜色空间和索引
|
// setEditorColor 写入当前文档的RGB纯色,保留透明度并替换原颜色空间和索引
|
||||||
// 入参: fill 颜色, value 十六进制色值
|
// 入参: fill 颜色, value 十六进制色值
|
||||||
// 返回: error 错误信息
|
// 返回: 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 将库层毫米边界传递给画布交互层
|
// editorBox 将库层毫米边界传递给画布交互层
|
||||||
// 入参: box 毫米坐标中的矩形范围
|
// 入参: box 毫米坐标中的矩形范围
|
||||||
// 返回: map[string]any 可直接传递给JavaScript的横纵坐标和宽高
|
// 返回: map[string]any 可直接传递给JavaScript的横纵坐标和宽高
|
||||||
@@ -3631,14 +3528,107 @@ func editorOperationProgress(callback js.Value) func(string, int, int) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// saveDocument 分块写出当前编辑文档或指定页面
|
// credentialsFromJS 解析本次打开的口令或本机证书私钥
|
||||||
// 入参: args 数据写出回调、可选准备进度回调和可选页面索引
|
// 入参: value 解锁表单数据
|
||||||
// 返回: any 保存结果, error 错误信息
|
// 返回: ofdgo.Credentials 会话凭据, error 错误信息
|
||||||
func saveDocument(args []js.Value) (any, error) {
|
func credentialsFromJS(value js.Value) (ofdgo.Credentials, error) {
|
||||||
if currentEditor == nil {
|
credentials := ofdgo.Credentials{UserName: value.Get("userName").String()}
|
||||||
return nil, fmt.Errorf("no document is being edited")
|
if keyValue := value.Get("key"); !keyValue.IsNull() && !keyValue.IsUndefined() {
|
||||||
|
key, err := bytesFromJS(keyValue)
|
||||||
|
if err != nil {
|
||||||
|
return credentials, err
|
||||||
}
|
}
|
||||||
return saveEditor(currentEditor, args)
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 以独立快照加密另存,不修改当前文档的输出策略
|
// saveEncrypted 以独立快照加密另存,不修改当前文档的输出策略
|
||||||
@@ -3790,6 +3780,16 @@ func saveSigned(args []js.Value) (any, error) {
|
|||||||
return successResult(map[string]any{"mime": "application/ofd", "label": "OFD"}), nil
|
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 统一分块写出及进度取消,加密策略由库层执行
|
// saveEditor 统一分块写出及进度取消,加密策略由库层执行
|
||||||
// 入参: editor 待写出文档, args 写出回调、进度回调和可选页面索引
|
// 入参: editor 待写出文档, args 写出回调、进度回调和可选页面索引
|
||||||
// 返回: any 保存结果, error 错误信息
|
// 返回: any 保存结果, error 错误信息
|
||||||
|
|||||||
+191
-191
@@ -39,6 +39,97 @@ type FontFile = ofdgo.FontFile
|
|||||||
// FontInfo OFD字体信息
|
// FontInfo OFD字体信息
|
||||||
type FontInfo = ofdgo.FontInfo
|
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 签名验证信息
|
// SignatureInfo 签名验证信息
|
||||||
type SignatureInfo struct {
|
type SignatureInfo struct {
|
||||||
DocIndex int `json:"docIndex"`
|
DocIndex int `json:"docIndex"`
|
||||||
@@ -144,55 +235,6 @@ type SignatureStampInfo struct {
|
|||||||
Height float64 `json:"height,omitempty"`
|
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 仅向界面传递非敏感加密状态
|
// EncryptionInfo 仅向界面传递非敏感加密状态
|
||||||
type EncryptionInfo struct {
|
type EncryptionInfo struct {
|
||||||
Encrypted bool `json:"encrypted"`
|
Encrypted bool `json:"encrypted"`
|
||||||
@@ -201,55 +243,6 @@ type EncryptionInfo struct {
|
|||||||
Layers int `json:"layers,omitempty"`
|
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 导出格式列表
|
// supportedExportFormats 导出格式列表
|
||||||
var supportedExportFormats = []ExportFormat{
|
var supportedExportFormats = []ExportFormat{
|
||||||
{Value: "pdf", Label: "PDF", Extension: "pdf", MIME: "application/pdf"},
|
{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"},
|
{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文档
|
// Open 打开浏览器内存中的OFD文档
|
||||||
// 入参: data OFD文件数据, opts 打开选项
|
// 入参: data OFD文件数据, opts 打开选项
|
||||||
// 返回: *Session 文档会话, error 错误信息
|
// 返回: *Session 文档会话, error 错误信息
|
||||||
@@ -647,6 +616,64 @@ func (s *Session) SVGFontData(name string) ([]byte, error) {
|
|||||||
return data, nil
|
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 导出单页
|
// ExportPage 导出单页
|
||||||
// 入参: index 页面索引, value 导出格式, dpi 图片DPI, writer 输出流
|
// 入参: index 页面索引, value 导出格式, dpi 图片DPI, writer 输出流
|
||||||
// 返回: ExportFormat 导出格式, error 错误信息
|
// 返回: 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)
|
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 获取签名验证信息
|
// signatureInfos 获取签名验证信息
|
||||||
// 返回: []SignatureInfo 签名验证信息, error 错误信息
|
// 返回: []SignatureInfo 签名验证信息, error 错误信息
|
||||||
func (s *Session) signatureInfos() ([]SignatureInfo, error) {
|
func (s *Session) signatureInfos() ([]SignatureInfo, error) {
|
||||||
@@ -855,72 +924,3 @@ func signatureStampInfos(positions []ofdgo.SignatureStampPosition) []SignatureSt
|
|||||||
}
|
}
|
||||||
return infos
|
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
|
|
||||||
}
|
|
||||||
+3
-55
@@ -20,7 +20,6 @@ import (
|
|||||||
"image"
|
"image"
|
||||||
"io"
|
"io"
|
||||||
"reflect"
|
"reflect"
|
||||||
"sync"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// ErrBackendUnavailable 表示未配置所需后端,不会自动切换其他实现
|
// ErrBackendUnavailable 表示未配置所需后端,不会自动切换其他实现
|
||||||
@@ -106,7 +105,7 @@ type EPSBackend interface {
|
|||||||
// RenderBackends 按职责组合后端,nil表示明确禁用,不隐式回退
|
// RenderBackends 按职责组合后端,nil表示明确禁用,不隐式回退
|
||||||
// 实例及其Renderer需串行使用,第三方实现无需引用任何内置绘图库
|
// 实例及其Renderer需串行使用,第三方实现无需引用任何内置绘图库
|
||||||
type RenderBackends struct {
|
type RenderBackends struct {
|
||||||
Resources FontResourceBackend
|
FontResources FontResourceBackend
|
||||||
Fonts FontBackend
|
Fonts FontBackend
|
||||||
Geometry GeometryBackend
|
Geometry GeometryBackend
|
||||||
Compiler PageCompiler
|
Compiler PageCompiler
|
||||||
@@ -118,7 +117,7 @@ type RenderBackends struct {
|
|||||||
|
|
||||||
// BackendInfo 描述各项能力实际使用的后端,空标识表示未配置
|
// BackendInfo 描述各项能力实际使用的后端,空标识表示未配置
|
||||||
type BackendInfo struct {
|
type BackendInfo struct {
|
||||||
Resources string `json:"resources"`
|
FontResources string `json:"fontResources"`
|
||||||
Fonts string `json:"fonts"`
|
Fonts string `json:"fonts"`
|
||||||
Geometry string `json:"geometry"`
|
Geometry string `json:"geometry"`
|
||||||
Compiler string `json:"compiler"`
|
Compiler string `json:"compiler"`
|
||||||
@@ -128,61 +127,10 @@ type BackendInfo struct {
|
|||||||
EPS string `json:"eps"`
|
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// Info 返回配置中各能力的提供者,不依赖WebUI推断
|
// Info 返回配置中各能力的提供者,不依赖WebUI推断
|
||||||
// 返回: BackendInfo 后端能力
|
// 返回: BackendInfo 后端能力
|
||||||
func (b RenderBackends) Info() 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 获取已配置的后端标识
|
// backendName 获取已配置的后端标识
|
||||||
|
|||||||
@@ -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
@@ -85,7 +85,7 @@ 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) {
|
if !sameBackend(e.backends.FontResources, backends.FontResources) {
|
||||||
e.fonts = make(map[string]*FontResource)
|
e.fonts = make(map[string]*FontResource)
|
||||||
for i := range e.resources {
|
for i := range e.resources {
|
||||||
e.resources[i].subset = nil
|
e.resources[i].subset = nil
|
||||||
@@ -565,40 +565,6 @@ func (e *Editor) nextID() string {
|
|||||||
return strconv.Itoa(e.maxID)
|
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图片,重复资源复用标识,引用后写入文档
|
// AddImage 注册PNG或JPEG图片,重复资源复用标识,引用后写入文档
|
||||||
// 不透明纯黑白PNG仅在无损JBIG2编码更小时转换,其他图片保留原始编码
|
// 不透明纯黑白PNG仅在无损JBIG2编码更小时转换,其他图片保留原始编码
|
||||||
// 入参: data 图片数据
|
// 入参: data 图片数据
|
||||||
|
|||||||
+49
-151
@@ -16,14 +16,12 @@ package ofdgo
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/binary"
|
"crypto/sha256"
|
||||||
"fmt"
|
"fmt"
|
||||||
"maps"
|
"maps"
|
||||||
"slices"
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/tdewolff/font"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// editorFontUsage 汇总原文档字体的字符、显式字形及无法确定的引用
|
// editorFontUsage 汇总原文档字体的字符、显式字形及无法确定的引用
|
||||||
@@ -33,6 +31,47 @@ type editorFontUsage struct {
|
|||||||
unsafe bool
|
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 创建字体用字记录
|
// newEditorFontUsage 创建字体用字记录
|
||||||
// 返回: *editorFontUsage 用字记录
|
// 返回: *editorFontUsage 用字记录
|
||||||
func 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 收集新增字体实际使用的字形,仅裁剪保存结果,保留完整编辑字体
|
// subsetFonts 收集新增字体实际使用的字形,仅裁剪保存结果,保留完整编辑字体
|
||||||
// 入参: 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 {
|
if e.backends.FontResources == nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
used := make(map[string]map[uint16]bool)
|
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 {
|
if resource.font == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
sfnt := e.fonts[resource.font.ID]
|
resourceFont := e.fonts[resource.font.ID]
|
||||||
if sfnt == nil {
|
if resourceFont == nil {
|
||||||
var err error
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
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)
|
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)
|
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 := 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 {
|
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)
|
||||||
}
|
}
|
||||||
@@ -338,45 +278,3 @@ func (e *Editor) subsetFonts(progress editorProgress) (map[string][]byte, error)
|
|||||||
}
|
}
|
||||||
return result, nil
|
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)
|
|
||||||
}
|
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -421,133 +421,6 @@ func (e *Editor) sourceNewPageXML(page PageContent) ([]byte, error) {
|
|||||||
return editorXMLContainer("Page", nil, append(area, content...))
|
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条目直接复制到新包,逐项写入改动,不持有原资源解压副本
|
// writeSource 将未修改ZIP条目直接复制到新包,逐项写入改动,不持有原资源解压副本
|
||||||
// 入参: writer 输出流, fonts 新增字体子集, progress 保存进度回调
|
// 入参: writer 输出流, fonts 新增字体子集, progress 保存进度回调
|
||||||
// 返回: int64 写入字节数, error 错误信息
|
// 返回: int64 写入字节数, error 错误信息
|
||||||
|
|||||||
@@ -173,14 +173,14 @@ 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 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
|
continue
|
||||||
}
|
}
|
||||||
data, err := reader.readFile(name)
|
data, err := reader.readFile(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("subset font %s: %w", name, err)
|
return nil, fmt.Errorf("subset font %s: %w", name, err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,13 +14,6 @@
|
|||||||
|
|
||||||
package ofdgo
|
package ofdgo
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"slices"
|
|
||||||
|
|
||||||
"github.com/tdewolff/font"
|
|
||||||
)
|
|
||||||
|
|
||||||
// FontResource 保存独立字体数据、标准元信息与只读度量,不包含绘图库类型
|
// FontResource 保存独立字体数据、标准元信息与只读度量,不包含绘图库类型
|
||||||
type FontResource struct {
|
type FontResource struct {
|
||||||
FontMetrics
|
FontMetrics
|
||||||
@@ -45,87 +38,3 @@ type FontResourceBackend interface {
|
|||||||
SubsetFont(data []byte, glyphs []uint16, preserve bool) ([]byte, error)
|
SubsetFont(data []byte, glyphs []uint16, preserve bool) ([]byte, error)
|
||||||
SubsetSourceFont(data []byte, usage FontUsage) ([]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 ""
|
|
||||||
}
|
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -40,7 +40,7 @@ func init() {
|
|||||||
// 返回: RenderBackends 默认后端组合
|
// 返回: RenderBackends 默认后端组合
|
||||||
func defaultRenderBackends() RenderBackends {
|
func defaultRenderBackends() RenderBackends {
|
||||||
backend := CanvasBackend{}
|
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 返回后端标识
|
// Name 返回后端标识
|
||||||
|
|||||||
Reference in new issue
Block a user