feat(加强兼容): 支持多文档
build.yaml / build (push) Successful in 44s

This commit is contained in:
xiaoqidun committed 2026-10-03 13:42:29 +08:00
1 parent 8de701dbd4
commit d37f41cd90
21 files changed
+280 -57

No files matched your search

+1
View File
@@ -597,6 +597,7 @@
<button id="infoButton" class="small-button" type="button" aria-label="修改文档信息" aria-haspopup="dialog" aria-controls="infoPanel" hidden>修改</button> <button id="infoButton" class="small-button" type="button" aria-label="修改文档信息" aria-haspopup="dialog" aria-controls="infoPanel" hidden>修改</button>
</div> </div>
<div id="editNotice" class="font-permission-hint" role="status" hidden></div> <div id="editNotice" class="font-permission-hint" role="status" hidden></div>
<select id="documentSelect" class="document-select" aria-label="选择文档" title="切换文档" hidden></select>
<dl class="meta-list"> <dl class="meta-list">
<div> <div>
<dt>文件</dt> <dt>文件</dt>
+9
View File
@@ -1516,6 +1516,15 @@ body[data-hide-meta] .meta-panel {
white-space: nowrap; white-space: nowrap;
} }
.document-select {
width: 100%;
min-width: 0;
height: 28px;
margin-bottom: 10px;
padding: 0 6px;
font-size: 12px;
}
.app-panel { .app-panel {
margin-bottom: 12px; margin-bottom: 12px;
padding-bottom: 12px; padding-bottom: 12px;
+54 -2
View File
@@ -51,6 +51,7 @@ const state = {
editorInfo: null, editorInfo: null,
editorViews: new Map(), editorViews: new Map(),
savedRevision: null, savedRevision: null,
packageDirty: false,
encryptionDirty: false, encryptionDirty: false,
insertObject: null, insertObject: null,
importPageCount: 0, importPageCount: 0,
@@ -231,6 +232,7 @@ const el = {
textFontAdd: document.querySelector("#textFontAdd"), textFontAdd: document.querySelector("#textFontAdd"),
paragraphButton: document.querySelector("#paragraphButton"), paragraphButton: document.querySelector("#paragraphButton"),
infoButton: document.querySelector("#infoButton"), infoButton: document.querySelector("#infoButton"),
documentSelect: document.querySelector("#documentSelect"),
infoPanel: document.querySelector("#infoPanel"), infoPanel: document.querySelector("#infoPanel"),
infoForm: document.querySelector("#infoForm"), infoForm: document.querySelector("#infoForm"),
infoTitle: document.querySelector("#infoTitle"), infoTitle: document.querySelector("#infoTitle"),
@@ -611,10 +613,12 @@ const canvasEditor = new CanvasEditor(el.viewerPanel, {
}); });
function updatePendingChanges() { function updatePendingChanges() {
setDirty(state.encryptionDirty || Boolean(state.editorInfo) && (state.editorInfo.revision !== state.savedRevision setDirty(state.packageDirty || state.encryptionDirty || Boolean(state.editorInfo) && (state.editorInfo.revision !== state.savedRevision
|| canvasEditor.textChanged() || canvasEditor.cropChanged() || canvasEditor.nudgeChanged())); || canvasEditor.textChanged() || canvasEditor.cropChanged() || canvasEditor.nudgeChanged()));
} }
el.documentSelect.addEventListener("change", () => switchDocument(Number(el.documentSelect.value)));
el.editorTools.addEventListener("pointerdown", (event) => { el.editorTools.addEventListener("pointerdown", (event) => {
if ((canvasEditor.input || canvasEditor.nudge) && event.target.closest("button")) event.preventDefault(); if ((canvasEditor.input || canvasEditor.nudge) && event.target.closest("button")) event.preventDefault();
}); });
@@ -1929,7 +1933,40 @@ function discardChanges() {
function setEditorInfo(doc) { function setEditorInfo(doc) {
state.editorInfo = { revision: doc.revision, canUndo: doc.canUndo, canRedo: doc.canRedo, state.editorInfo = { revision: doc.revision, canUndo: doc.canUndo, canRedo: doc.canRedo,
pageCapabilities: doc.pageCapabilities, editWarnings: doc.editWarnings || [] }; pageCapabilities: doc.pageCapabilities, editWarnings: doc.editWarnings || [] };
setDirty(state.encryptionDirty || doc.revision !== state.savedRevision); setDirty(state.packageDirty || state.encryptionDirty || doc.revision !== state.savedRevision);
}
async function switchDocument(index) {
if (!state.doc || document.body.hasAttribute("aria-busy") || state.exporting || formDialogOpen()) return;
const previous = state.doc.documentIndex || 0;
if (index === previous) return;
if (!await canvasEditor.commitNudge() || !await canvasEditor.commitText() || !await canvasEditor.commitCrop()) {
el.documentSelect.value = String(previous);
return;
}
const openSeq = ++state.openSeq;
setBusy(true, "正在切换文档");
try {
const doc = await callWASM("ofdgoSelectDocument", index);
if (openSeq !== state.openSeq) return;
state.packageDirty ||= state.dirty;
state.savedRevision = doc.revision;
state.editorViews.clear();
state.objectClipboard = state.styleClipboard = null;
resetCompositeScope();
canvasEditor.clear();
canvasEditor.setTool("");
state.ofdBytes = null;
setEditorInfo(doc);
await openDocument({ doc, openSeq, skipAutoFonts: true, resetScroll: true, clearSelection: true, fitMode: state.fitMode, scale: state.scale });
} catch (error) {
if (openSeq === state.openSeq) {
el.documentSelect.value = String(previous);
showError(error, false);
}
} finally {
if (openSeq === state.openSeq) setBusy(false);
}
} }
function editorLocation() { function editorLocation() {
@@ -2110,6 +2147,7 @@ async function createDocument(event) {
state.fileName = `${title.replace(/[\\/:*?"<>|]+/g, "_").replace(/\.ofd$/i, "")}.ofd`; state.fileName = `${title.replace(/[\\/:*?"<>|]+/g, "_").replace(/\.ofd$/i, "")}.ofd`;
state.conversionWarnings = []; state.conversionWarnings = [];
state.savedRevision = null; state.savedRevision = null;
state.packageDirty = false;
state.encryptionDirty = false; state.encryptionDirty = false;
setEditorInfo(doc); setEditorInfo(doc);
el.createPanel.close(); el.createPanel.close();
@@ -3616,6 +3654,7 @@ async function openOFD(file, remote = null) {
state.editorViews.clear(); state.editorViews.clear();
state.fontRenderPending = false; state.fontRenderPending = false;
state.savedRevision = null; state.savedRevision = null;
state.packageDirty = false;
state.encryptionDirty = false; state.encryptionDirty = false;
canvasEditor.clear(); canvasEditor.clear();
setDirty(false); setDirty(false);
@@ -5299,6 +5338,7 @@ async function exportFile(whole, indices = null, value = el.exportFormat.value)
} }
if (saving && indices === null && state.editorInfo) { if (saving && indices === null && state.editorInfo) {
state.savedRevision = state.editorInfo.revision; state.savedRevision = state.editorInfo.revision;
state.packageDirty = false;
state.encryptionDirty = false; state.encryptionDirty = false;
setDirty(false); setDirty(false);
} }
@@ -7383,6 +7423,16 @@ function renderMeta(keepDetails = false) {
})); }));
}); });
const doc = state.doc || {}; const doc = state.doc || {};
el.documentSelect.hidden = (doc.documents?.length || 0) < 2;
renderMetaContent(el.documentSelect, doc.documents || [], () => {
el.documentSelect.replaceChildren(...(doc.documents || []).map(entry => {
const option = document.createElement("option");
option.value = String(entry.index);
option.textContent = `${entry.index + 1} · ${entry.title || "未命名"}`;
return option;
}));
});
el.documentSelect.value = String(doc.documentIndex || 0);
el.metaPanel.setAttribute("aria-busy", String(!!doc.detailsPending)); el.metaPanel.setAttribute("aria-busy", String(!!doc.detailsPending));
document.title = `OFDGo WebUI - ${state.fileName}`; document.title = `OFDGo WebUI - ${state.fileName}`;
el.metaFile.textContent = state.fileName; el.metaFile.textContent = state.fileName;
@@ -8200,6 +8250,7 @@ function updateControls() {
el.attachmentAdd.disabled = !state.editing || !state.ready || state.exporting; el.attachmentAdd.disabled = !state.editing || !state.ready || state.exporting;
renderMetaContent(el.attachmentList, [state.doc?.attachments, state.doc?.attachmentError, state.editing], renderAttachments); renderMetaContent(el.attachmentList, [state.doc?.attachments, state.doc?.attachmentError, state.editing], renderAttachments);
const hasDoc = Boolean(state.doc); const hasDoc = Boolean(state.doc);
el.documentSelect.disabled = !hasDoc || !state.ready || state.exporting || document.body.hasAttribute("aria-busy");
updateDisplayControls(); updateDisplayControls();
updateEditorTools(); updateEditorTools();
const pageCount = state.doc ? state.doc.pageCount : 0; const pageCount = state.doc ? state.doc.pageCount : 0;
@@ -8403,6 +8454,7 @@ function showError(err, empty = !state.doc) {
function setBusy(busy, text = "", percent = 0, status = "") { function setBusy(busy, text = "", percent = 0, status = "") {
document.body.toggleAttribute("aria-busy", busy); document.body.toggleAttribute("aria-busy", busy);
el.documentSelect.disabled = busy || !state.doc || !state.ready || state.exporting;
renderSecurity(); renderSecurity();
updateDisplayControls(); updateDisplayControls();
el.editButton.disabled = busy || Boolean(sealPreview) || !state.doc || !state.ready || state.exporting; el.editButton.disabled = busy || Boolean(sealPreview) || !state.doc || !state.ready || state.exporting;
+6
View File
@@ -536,6 +536,12 @@ export class CanvasEditor {
} }
start(event) { start(event) {
if (event.target === this.viewer) {
const rect = this.viewer.getBoundingClientRect();
const left = rect.left + (this.viewer.clientLeft || 0), top = rect.top + (this.viewer.clientTop || 0);
if (event.clientX < left || event.clientX >= left + this.viewer.clientWidth
|| event.clientY < top || event.clientY >= top + this.viewer.clientHeight) return;
}
if (this.nudge) { if (this.nudge) {
this.commitNudge(); this.commitNudge();
return; return;
+24 -1
View File
@@ -184,6 +184,7 @@ func (r *browserReaderAt) ReadAt(data []byte, offset int64) (int, error) {
// RunWASM 注册浏览器WASM接口并阻塞运行 // RunWASM 注册浏览器WASM接口并阻塞运行
func RunWASM() { func RunWASM() {
registerCallback("ofdgoOpen", openDocument) registerCallback("ofdgoOpen", openDocument)
registerCallback("ofdgoSelectDocument", selectDocument)
registerAsyncCallback("ofdgoConvertPDF", convertPDFDocument) registerAsyncCallback("ofdgoConvertPDF", convertPDFDocument)
registerAsyncCallback("ofdgoConvertFile", convertFile) registerAsyncCallback("ofdgoConvertFile", convertFile)
registerAsyncCallback("ofdgoPackFiles", packFiles) registerAsyncCallback("ofdgoPackFiles", packFiles)
@@ -3359,6 +3360,27 @@ func changeAttachment(args []js.Value) (any, error) {
}) })
} }
// selectDocument 切换包内文档,保留编辑快照并清除跨文档剪贴板
// 入参: args 文档索引
// 返回: any 文档信息, error 错误信息
func selectDocument(args []js.Value) (any, error) {
if currentSession == nil || len(args) == 0 {
return nil, fmt.Errorf("ofd document is not opened")
}
session, err := currentSession.Document(args[0].Int())
if err != nil {
return nil, err
}
_ = currentSession.Close()
currentSession, currentEditor = session, session.editor
if pendingImport != nil {
_ = pendingImport.Close()
pendingImport = nil
}
copiedObjects, copiedStyle = nil, nil
return editorSummary(), nil
}
// editDocument 将已打开文档接入编辑器,沿用页面、资源及字体配置 // editDocument 将已打开文档接入编辑器,沿用页面、资源及字体配置
// 入参: args 当前页索引,省略时使用首页 // 入参: args 当前页索引,省略时使用首页
// 返回: any 编辑状态, error 错误信息 // 返回: any 编辑状态, error 错误信息
@@ -3367,6 +3389,7 @@ func editDocument(args []js.Value) (any, error) {
return nil, fmt.Errorf("ofd document is not opened") return nil, fmt.Errorf("ofd document is not opened")
} }
if currentEditor != nil { if currentEditor != nil {
currentSession.editing = true
return editorSummary(), nil return editorSummary(), nil
} }
editor, err := currentSession.Reader.Editor() editor, err := currentSession.Reader.Editor()
@@ -3952,7 +3975,7 @@ func verifySignatures(args []js.Value) (any, error) {
Certificates: append(files["certificates"], files["roots"]...), Required: policy.RequireRevocation, VerifyTime: &now, Certificates: append(files["certificates"], files["roots"]...), Required: policy.RequireRevocation, VerifyTime: &now,
})) }))
} }
reports, err := currentSession.Reader.VerifySignatures(options...) reports, err := currentSession.Reader.VerifyDocumentSignatures(currentSession.Reader.DocumentIndex(), options...)
if err != nil { if err != nil {
return nil, err return nil, err
} }
+52 -1
View File
@@ -72,6 +72,8 @@ type pageBoxInfo struct {
// DocumentInfo 文档信息 // DocumentInfo 文档信息
type DocumentInfo struct { type DocumentInfo struct {
DocumentIndex int `json:"documentIndex"`
Documents []DocumentEntry `json:"documents,omitempty"`
Encryption EncryptionInfo `json:"encryption"` Encryption EncryptionInfo `json:"encryption"`
Version string `json:"version"` Version string `json:"version"`
DocType string `json:"docType"` DocType string `json:"docType"`
@@ -96,6 +98,12 @@ type DocumentInfo struct {
DetailsPending bool `json:"detailsPending,omitempty"` DetailsPending bool `json:"detailsPending,omitempty"`
} }
// DocumentEntry 包内文档入口,不预先解析其他文档的页面和资源
type DocumentEntry struct {
Index int `json:"index"`
Title string `json:"title"`
}
// DocumentDetails 不含页面数组的文档补充信息 // DocumentDetails 不含页面数组的文档补充信息
type DocumentDetails struct { type DocumentDetails struct {
FontCount int `json:"fontCount"` FontCount int `json:"fontCount"`
@@ -287,6 +295,45 @@ func (s *Session) Close() error {
return nil return nil
} }
// Document 创建指定文档的会话,保留编辑内容和字体配置,不复用页面缓存
// 不关闭原会话,输入文件的生命周期由原会话管理
// 入参: index 文档索引,从0开始
// 返回: *Session 文档会话, error 错误信息
func (s *Session) Document(index int) (*Session, error) {
reader := s.Reader
if s.editor != nil {
var err error
reader, err = s.editor.Reader()
if err != nil {
return nil, err
}
}
reader, err := reader.Document(index)
if err != nil {
return nil, err
}
next, err := newSession(reader, OpenOptions{RenderAnnotations: s.Renderer.RenderAnnotations})
if err != nil {
return nil, err
}
ofdgo.WithRenderBackends(s.Renderer.Backends())(next.Renderer)
next.fontFS = s.fontFS
if next.fontFS != nil {
next.Renderer.SetFontFS(next.fontFS)
}
next.editor, err = reader.Editor()
if err != nil {
return nil, err
}
next.editor.SetHistoryLimit(100)
if next.fontFS != nil {
next.editor.SetFontFS(next.fontFS)
}
next.editor.SetRenderBackends(next.Renderer.Backends())
next.editing = true
return next, nil
}
// SetFonts 更新字体配置并保留文档、页面和验签结果 // SetFonts 更新字体配置并保留文档、页面和验签结果
// 入参: fonts 字体文件列表 // 入参: fonts 字体文件列表
// 返回: error 错误信息 // 返回: error 错误信息
@@ -376,6 +423,7 @@ func (s *Session) SearchPage(index int, query string) ([]ofdgo.TextMatch, error)
// 返回: DocumentInfo 文档信息 // 返回: DocumentInfo 文档信息
func (s *Session) Summary() DocumentInfo { func (s *Session) Summary() DocumentInfo {
info := DocumentInfo{ info := DocumentInfo{
DocumentIndex: s.Reader.DocumentIndex(),
Encryption: s.encryptionInfo(), Encryption: s.encryptionInfo(),
Version: s.Reader.Version(), Version: s.Reader.Version(),
DocType: s.Reader.DocType(), DocType: s.Reader.DocType(),
@@ -384,6 +432,9 @@ func (s *Session) Summary() DocumentInfo {
Outlines: s.doc.OutlineInfos(), Outlines: s.doc.OutlineInfos(),
DetailsPending: true, DetailsPending: true,
} }
for index, body := range s.Reader.OFD.DocBody {
info.Documents = append(info.Documents, DocumentEntry{Index: index, Title: body.DocInfo.Title})
}
if docInfo, err := s.Reader.DocInfo(); err == nil && docInfo != nil { if docInfo, err := s.Reader.DocInfo(); err == nil && docInfo != nil {
info.Title = docInfo.Title info.Title = docInfo.Title
info.Author = docInfo.Author info.Author = docInfo.Author
@@ -788,7 +839,7 @@ func (s *Session) signatureInfos() ([]SignatureInfo, error) {
return s.signatures, s.signatureError return s.signatures, s.signatureError
} }
s.signaturesRead = true s.signaturesRead = true
reports, err := s.Reader.VerifySignatures() reports, err := s.Reader.VerifyDocumentSignatures(s.Reader.DocumentIndex())
if err != nil { if err != nil {
s.signatureError = err s.signatureError = err
return nil, err return nil, err
+1 -1
View File
@@ -265,7 +265,7 @@ func (e *Editor) editAnnotations(index int, ids []string, edit func([]byte, *edi
if len(patches) != 0 { if len(patches) != 0 {
parts[name] = editorPatchXML(data, patches) parts[name] = editorPatchXML(data, patches)
if remaining == 0 { if remaining == 0 {
docName, err := editorPageLocation(reader, nil, "", "/"+cleanPackagePath(reader.OFD.DocBody[0].DocRoot)) docName, err := editorPageLocation(reader, nil, "", "/"+cleanPackagePath(reader.OFD.DocBody[reader.documentIndex].DocRoot))
if err != nil { if err != nil {
return err return err
} }
+2 -2
View File
@@ -313,7 +313,7 @@ func (e *Editor) appendAnnotations(index int, annotations []byte) error {
} }
parts := map[string][]byte{name: data, file: content} parts := map[string][]byte{name: data, file: content}
if base.document.Annotations == "" { if base.document.Annotations == "" {
docName := cleanPackagePath(reader.OFD.DocBody[0].DocRoot) docName := cleanPackagePath(reader.OFD.DocBody[reader.documentIndex].DocRoot)
docData, err := reader.readFile(docName) docData, err := reader.readFile(docName)
if err != nil { if err != nil {
return err return err
@@ -391,7 +391,7 @@ func (e *Editor) commitAnnotationParts(base *editorSource, parts map[string][]by
} }
files[name] = data files[name] = data
} }
reader := &Reader{Zip: base.reader.Zip, files: files} reader := &Reader{Zip: base.reader.Zip, files: files, encryption: base.reader.encryption, documentIndex: base.reader.documentIndex}
if err := reader.initRoot(); err != nil { if err := reader.initRoot(); err != nil {
return err return err
} }
+1 -1
View File
@@ -140,7 +140,7 @@ func (e *Editor) PasteAnnotations(page int, selection *AnnotationSelection, dx,
edit.source.annotationStates = states edit.source.annotationStates = states
if len(selection.resources) != 0 { if len(selection.resources) != 0 {
reader := edit.source.reader reader := edit.source.reader
name := cleanPackagePath(reader.OFD.DocBody[0].DocRoot) name := cleanPackagePath(reader.OFD.DocBody[reader.documentIndex].DocRoot)
data, err := reader.readFile(name) data, err := reader.readFile(name)
if err != nil { if err != nil {
return err return err
+2 -2
View File
@@ -65,7 +65,7 @@ func (e *Editor) AddAttachment(name string, data []byte) (string, error) {
return "", err return "", err
} }
entry = bytes.TrimPrefix(entry, []byte(xml.Header)) entry = bytes.TrimPrefix(entry, []byte(xml.Header))
docName := cleanPackagePath(base.reader.OFD.DocBody[0].DocRoot) docName := cleanPackagePath(base.reader.OFD.DocBody[base.reader.documentIndex].DocRoot)
docData, err := base.reader.readFile(docName) docData, err := base.reader.readFile(docName)
if err != nil { if err != nil {
return "", err return "", err
@@ -190,7 +190,7 @@ func (e *Editor) editAttachment(id string, change func([]byte, *editorXML, map[s
if base == nil { if base == nil {
return fmt.Errorf("attachment not found: %s", id) return fmt.Errorf("attachment not found: %s", id)
} }
name := cleanPackagePath(base.reader.OFD.DocBody[0].DocRoot) name := cleanPackagePath(base.reader.OFD.DocBody[base.reader.documentIndex].DocRoot)
external := strings.TrimSpace(base.document.Attachments.Path) != "" external := strings.TrimSpace(base.document.Attachments.Path) != ""
if external { if external {
name = base.reader.ResPath(base.document.Attachments.Path) name = base.reader.ResPath(base.document.Attachments.Path)
+7 -7
View File
@@ -224,7 +224,7 @@ func (e *Editor) ImportObjects(source *Editor, sourcePage int, ids []string, pag
return copied, err return copied, err
} }
// ImportPages 将来源主文档中的指定页面插入目标位置,保持输入顺序并作为一次撤销操作 // ImportPages 将来源当前文档中的指定页面插入目标位置,保持输入顺序并作为一次撤销操作
// 页面索引从0开始,at可等于当前页数;复制关联模板、注释、签章外观和实际引用的资源 // 页面索引从0开始,at可等于当前页数;复制关联模板、注释、签章外观和实际引用的资源
// 不导入来源元数据和目录,指向未选页面的跳转被移除;签名数据仅保留原始凭据,不代表合并后文档有效 // 不导入来源元数据和目录,指向未选页面的跳转被移除;签名数据仅保留原始凭据,不代表合并后文档有效
// 返回后可关闭来源Reader,目标原Reader的生命周期要求不变 // 返回后可关闭来源Reader,目标原Reader的生命周期要求不变
@@ -450,7 +450,7 @@ func (e *Editor) prepareImport(source *Reader, doc *Document, selected map[strin
for _, resource := range e.resources { for _, resource := range e.resources {
m.files[resource.name] = nil m.files[resource.name] = nil
} }
m.documentXML, err = m.readFile(source.ResPath(source.OFD.DocBody[0].DocRoot)) m.documentXML, err = m.readFile(source.ResPath(source.OFD.DocBody[source.documentIndex].DocRoot))
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@@ -530,7 +530,7 @@ func (m *editorPageImport) readFile(name string) ([]byte, error) {
// outlines 筛选指向导入页面的目录树并复用标准动作迁移 // outlines 筛选指向导入页面的目录树并复用标准动作迁移
// 返回: []byte 目录片段, error 错误信息 // 返回: []byte 目录片段, error 错误信息
func (m *editorPageImport) outlines() ([]byte, error) { func (m *editorPageImport) outlines() ([]byte, error) {
name := m.reader.ResPath(m.reader.OFD.DocBody[0].DocRoot) name := m.reader.ResPath(m.reader.OFD.DocBody[m.reader.documentIndex].DocRoot)
data, err := m.readFile(name) data, err := m.readFile(name)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -1132,7 +1132,7 @@ func (m *editorPageImport) attachment(id string) (string, error) {
if _, exists := m.attachments[id]; exists { if _, exists := m.attachments[id]; exists {
return m.id(id), nil return m.id(id), nil
} }
name := m.reader.ResPath(m.reader.OFD.DocBody[0].DocRoot) name := m.reader.ResPath(m.reader.OFD.DocBody[m.reader.documentIndex].DocRoot)
if m.doc.Attachments.Path != "" { if m.doc.Attachments.Path != "" {
name = m.reader.ResPath(m.doc.Attachments.Path) name = m.reader.ResPath(m.doc.Attachments.Path)
} }
@@ -1215,7 +1215,7 @@ func (m *editorPageImport) merge(base *editorSource, refs []Page, annotations, s
files = make(map[string][]byte) files = make(map[string][]byte)
} }
maps.Copy(files, m.files) maps.Copy(files, m.files)
name := base.reader.ResPath(base.reader.OFD.DocBody[0].DocRoot) name := base.reader.ResPath(base.reader.OFD.DocBody[base.reader.documentIndex].DocRoot)
if file, ok := base.reader.packageFile(name); ok { if file, ok := base.reader.packageFile(name); ok {
name = cleanPackagePath(file.Name) name = cleanPackagePath(file.Name)
} }
@@ -1318,10 +1318,10 @@ func (m *editorPageImport) merge(base *editorSource, refs []Page, annotations, s
if file, ok := base.reader.packageFile(name); ok { if file, ok := base.reader.packageFile(name); ok {
name = cleanPackagePath(file.Name) name = cleanPackagePath(file.Name)
} }
files[name] = editorXMLSetText(ofd, root.child("DocBody"), [][2]string{{"Signatures", loc}}) files[name] = editorXMLSetText(ofd, root.childAt("DocBody", base.reader.documentIndex), [][2]string{{"Signatures", loc}})
} }
} }
reader := &Reader{Zip: base.reader.Zip, files: files} reader := &Reader{Zip: base.reader.Zip, files: files, encryption: base.reader.encryption, documentIndex: base.reader.documentIndex}
if err := reader.initRoot(); err != nil { if err := reader.initRoot(); err != nil {
return nil, err return nil, err
} }
+10 -10
View File
@@ -29,7 +29,7 @@ func (e *Editor) sourceInfoXML() ([]byte, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
data, info, err := editorDocInfoXML(data, e.source.fallbackDocID) data, info, err := editorDocInfoXML(data, e.source.fallbackDocID, e.source.reader.documentIndex)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -50,16 +50,16 @@ func (e *Editor) sourceInfoXML() ([]byte, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
return editorCustomDatas(data, root.child("DocBody").child("DocInfo"), after.CustomDatas) return editorCustomDatas(data, root.childAt("DocBody", e.source.reader.documentIndex).child("DocInfo"), after.CustomDatas)
} }
return data, nil return data, nil
} }
// editorCreatorXML 更新首份文档的制作软件,移除原软件版本,保留其余根索引内容 // editorCreatorXML 更新指定文档的制作软件,移除原软件版本,保留其余根索引内容
// 入参: data 当前根索引XML, docID 缺少文档描述时使用的标识 // 入参: data 当前根索引XML, docID 缺少文档描述时使用的标识, index 文档索引
// 返回: []byte 修改后的XML, error 解析错误 // 返回: []byte 修改后的XML, error 解析错误
func editorCreatorXML(data []byte, docID string) ([]byte, error) { func editorCreatorXML(data []byte, docID string, index int) ([]byte, error) {
data, info, err := editorDocInfoXML(data, docID) data, info, err := editorDocInfoXML(data, docID, index)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -67,14 +67,14 @@ func editorCreatorXML(data []byte, docID string) ([]byte, error) {
} }
// editorDocInfoXML 获取文档描述,修改缺少描述的源文件时补建标准容器及标识 // editorDocInfoXML 获取文档描述,修改缺少描述的源文件时补建标准容器及标识
// 入参: data 根索引XML, docID 缺少文档描述时使用的标识 // 入参: data 根索引XML, docID 缺少文档描述时使用的标识, index 文档索引
// 返回: []byte 根索引XML, *editorXML 描述节点, error 解析错误 // 返回: []byte 根索引XML, *editorXML 描述节点, error 解析错误
func editorDocInfoXML(data []byte, docID string) ([]byte, *editorXML, error) { func editorDocInfoXML(data []byte, docID string, index int) ([]byte, *editorXML, error) {
root, err := parseEditorXML(data) root, err := parseEditorXML(data)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
body := root.child("DocBody") body := root.childAt("DocBody", index)
if body == nil { if body == nil {
return nil, nil, fmt.Errorf("document has no DocBody") return nil, nil, fmt.Errorf("document has no DocBody")
} }
@@ -90,7 +90,7 @@ func editorDocInfoXML(data []byte, docID string) ([]byte, *editorXML, error) {
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
return data, root.child("DocBody").child("DocInfo"), nil return data, root.childAt("DocBody", index).child("DocInfo"), nil
} }
// editorCustomDatas 更新自定义字段,保留原字段及容器的扩展内容 // editorCustomDatas 更新自定义字段,保留原字段及容器的扩展内容
+2 -2
View File
@@ -108,7 +108,7 @@ type PageCapabilities struct {
Resize bool Resize bool
} }
// Editor 打开主文档进行保真编辑,不修改输入包,不以权限声明或签名限制编辑 // Editor 打开当前文档进行保真编辑,不修改输入包,不以权限声明或签名限制编辑
// 原权限和签名数据保留,修改受保护内容会使原签名失效,不重新签名 // 原权限和签名数据保留,修改受保护内容会使原签名失效,不重新签名
// 页面按需解析,未修改条目直接保留;编辑器及其Reader快照使用期间不得关闭输入Reader // 页面按需解析,未修改条目直接保留;编辑器及其Reader快照使用期间不得关闭输入Reader
// 返回: *Editor 编辑器, error 错误信息 // 返回: *Editor 编辑器, error 错误信息
@@ -205,7 +205,7 @@ func (c ObjectCapabilities) editError() error {
} }
// cloneEditorReader 复用已验证的只读包索引,独立保存可变文档与读取缓存 // cloneEditorReader 复用已验证的只读包索引,独立保存可变文档与读取缓存
// 入参: reader 已加载主文档的阅读器 // 入参: reader 已加载当前文档的阅读器
// 返回: *Reader 不拥有输入文件的独立阅读器 // 返回: *Reader 不拥有输入文件的独立阅读器
func cloneEditorReader(reader *Reader) *Reader { func cloneEditorReader(reader *Reader) *Reader {
next := *reader next := *reader
+1 -1
View File
@@ -264,7 +264,7 @@ func (e *Editor) outlineXML() ([]byte, error) {
} }
if e.source != nil { if e.source != nil {
reader := e.source.reader reader := e.source.reader
data, err := reader.readFile(reader.ResPath(reader.OFD.DocBody[0].DocRoot)) data, err := reader.readFile(reader.ResPath(reader.OFD.DocBody[reader.documentIndex].DocRoot))
if err != nil { if err != nil {
return nil, err return nil, err
} }
+4 -4
View File
@@ -99,7 +99,7 @@ func (e *Editor) sourceParts(progress editorProgress) (map[string][]byte, error)
} }
pagesChanged := !slices.Equal(pageRefs, source.document.Pages.Page) pagesChanged := !slices.Equal(pageRefs, source.document.Pages.Page)
if pagesChanged || len(resourceFiles) != 0 || len(parts) != 0 && e.maxID != source.document.CommonData.MaxUnitID { if pagesChanged || len(resourceFiles) != 0 || len(parts) != 0 && e.maxID != source.document.CommonData.MaxUnitID {
name := reader.ResPath(reader.OFD.DocBody[0].DocRoot) name := reader.ResPath(reader.OFD.DocBody[reader.documentIndex].DocRoot)
data, err := reader.readFile(name) data, err := reader.readFile(name)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -199,7 +199,7 @@ func (e *Editor) sourceParts(progress editorProgress) (map[string][]byte, error)
parts["OFD.xml"] = data parts["OFD.xml"] = data
} }
if e.outlines != nil { if e.outlines != nil {
name := reader.ResPath(reader.OFD.DocBody[0].DocRoot) name := reader.ResPath(reader.OFD.DocBody[reader.documentIndex].DocRoot)
data, ok := parts[name] data, ok := parts[name]
if !ok { if !ok {
var err error var err error
@@ -468,7 +468,7 @@ func (e *Editor) writeSource(writer io.Writer, fonts map[string][]byte, progress
return 0, err return 0, err
} }
} }
data, err = editorCreatorXML(data, e.source.fallbackDocID) data, err = editorCreatorXML(data, e.source.fallbackDocID, reader.documentIndex)
if err != nil { if err != nil {
return 0, err return 0, err
} }
@@ -592,7 +592,7 @@ func (e *Editor) sourceReader(progress editorProgress) (*Reader, error) {
files = make(map[string][]byte) files = make(map[string][]byte)
} }
maps.Copy(files, parts) maps.Copy(files, parts)
reader := &Reader{Zip: e.source.reader.Zip, files: files, encryption: e.encryption} reader := &Reader{Zip: e.source.reader.Zip, files: files, encryption: e.encryption, documentIndex: e.source.reader.documentIndex}
if err := reader.initRoot(); err != nil { if err := reader.initRoot(); err != nil {
return nil, err return nil, err
} }
+2 -2
View File
@@ -21,7 +21,7 @@ import (
) )
// prunePageReferences 在输出快照中删除已移除页面的标准引用,不修改历史快照或其他文档 // prunePageReferences 在输出快照中删除已移除页面的标准引用,不修改历史快照或其他文档
// 沿主文档、页面、模板、资源、注释和签名索引检查,不进入附件、私有扩展和签名原始凭据 // 沿当前文档、页面、模板、资源、注释和签名索引检查,不进入附件、私有扩展和签名原始凭据
// 入参: parts 输出覆盖条目 // 入参: parts 输出覆盖条目
// 返回: error 读取或解析错误 // 返回: error 读取或解析错误
func (e *Editor) prunePageReferences(parts map[string][]byte) error { func (e *Editor) prunePageReferences(parts map[string][]byte) error {
@@ -48,7 +48,7 @@ func (e *Editor) prunePageReferences(parts map[string][]byte) error {
bookmarks[bookmark.Name] = true bookmarks[bookmark.Name] = true
} }
} }
queue := []string{reader.ResPath(reader.OFD.DocBody[0].DocRoot)} queue := []string{reader.ResPath(reader.OFD.DocBody[reader.documentIndex].DocRoot)}
if name := e.source.document.Signatures; name != "" { if name := e.source.document.Signatures; name != "" {
queue = append(queue, reader.ResPath(name)) queue = append(queue, reader.ResPath(name))
} }
+2 -1
View File
@@ -37,10 +37,11 @@ type editorResourceRefs struct {
// compactSourceResources 在保存副本中清理无引用字体、图片和已删除附件,并裁剪可完整确认用字的原有字体 // compactSourceResources 在保存副本中清理无引用字体、图片和已删除附件,并裁剪可完整确认用字的原有字体
// 按GB/T33190-2016附录A检查标识和路径引用,包括模板、注释、底纹、裁剪、版本和其他文档 // 按GB/T33190-2016附录A检查标识和路径引用,包括模板、注释、底纹、裁剪、版本和其他文档
// 全包检查保留孤立XML中的引用;未知命名空间、扩展数据或无法解析的XML使本次清理跳过 // 全包检查保留孤立XML中的引用;未知命名空间、扩展数据或无法解析的XML使本次清理跳过
// 多文档包保留原资源,避免独立文档的同号资源和共享文件被误删或裁剪
// 入参: parts 已修改和新增的包内条目, progress 保存进度回调 // 入参: parts 已修改和新增的包内条目, progress 保存进度回调
// 返回: map[string]bool 可移除的二进制条目, error 读取错误 // 返回: map[string]bool 可移除的二进制条目, error 读取错误
func (e *Editor) compactSourceResources(parts map[string][]byte, progress editorProgress) (map[string]bool, error) { func (e *Editor) compactSourceResources(parts map[string][]byte, progress editorProgress) (map[string]bool, error) {
if e.output != nil && e.output.protected { if e.source.reader.DocumentCount() > 1 || e.output != nil && e.output.protected {
return nil, nil return nil, nil
} }
if len(parts) == 0 && e.revision == 0 && (e.output == nil || e.output.options.Mode == CompressionUnchanged) { if len(parts) == 0 && e.revision == 0 && (e.output == nil || e.output.options.Mode == CompressionUnchanged) {
+11 -1
View File
@@ -84,9 +84,19 @@ func parseEditorXML(data []byte) (*editorXML, error) {
// 入参: name 节点名称 // 入参: name 节点名称
// 返回: *editorXML 节点,不存在时为nil // 返回: *editorXML 节点,不存在时为nil
func (n *editorXML) child(name string) *editorXML { func (n *editorXML) child(name string) *editorXML {
return n.childAt(name, 0)
}
// childAt 查找指定序号的OFD直接子节点
// 入参: name 节点名称, index 从0开始的节点序号
// 返回: *editorXML 节点,不存在时为nil
func (n *editorXML) childAt(name string, index int) *editorXML {
for _, child := range n.children { for _, child := range n.children {
if child.name.Local == name && (child.name.Space == n.name.Space || child.name.Space == ofdNamespace || child.name.Space == "") { if child.name.Local == name && (child.name.Space == n.name.Space || child.name.Space == ofdNamespace || child.name.Space == "") {
return child if index == 0 {
return child
}
index--
} }
} }
return nil return nil
+39 -5
View File
@@ -46,6 +46,7 @@ type Reader struct {
compositeGraphicUnitCache map[string]*CompositeGraphicUnit compositeGraphicUnitCache map[string]*CompositeGraphicUnit
pageHeaderCache map[string]PageContent pageHeaderCache map[string]PageContent
doc *Document doc *Document
documentIndex int
Stamps map[string][]Stamp Stamps map[string][]Stamp
Annots map[string][]Annotation Annots map[string][]Annotation
annotationFiles map[string][]string annotationFiles map[string][]string
@@ -67,16 +68,49 @@ func (r *Reader) Close() error {
return nil return nil
} }
// Doc 获取主文档结构 // DocumentCount 获取包内文档数量
// 返回: int 文档数量
func (r *Reader) DocumentCount() int {
if r.OFD == nil {
return 0
}
return len(r.OFD.DocBody)
}
// DocumentIndex 获取当前文档索引,从0开始
// 返回: int 文档索引
func (r *Reader) DocumentIndex() int {
return r.documentIndex
}
// Document 创建指定文档的独立阅读器,不改变当前文档或混用资源缓存
// 返回的阅读器不关闭输入文件,使用期间不得关闭持有输入文件的原阅读器
// 入参: index 文档索引,从0开始
// 返回: *Reader 文档阅读器, error 错误信息
func (r *Reader) Document(index int) (*Reader, error) {
if index < 0 || index >= r.DocumentCount() {
return nil, fmt.Errorf("document index %d out of range", index)
}
next := &Reader{Path: r.Path, Zip: r.Zip, files: r.files, encryption: r.encryption, documentIndex: index}
if err := next.initRoot(); err != nil {
return nil, err
}
if _, err := next.Doc(); err != nil {
return nil, err
}
return next, nil
}
// Doc 获取当前文档结构
// 返回: *Document 文档结构, error 错误信息 // 返回: *Document 文档结构, error 错误信息
func (r *Reader) Doc() (*Document, error) { func (r *Reader) Doc() (*Document, error) {
if r.doc != nil { if r.doc != nil {
return r.doc, nil return r.doc, nil
} }
if r.OFD == nil || len(r.OFD.DocBody) == 0 { if r.OFD == nil || r.documentIndex < 0 || r.documentIndex >= len(r.OFD.DocBody) {
return nil, fmt.Errorf("no docbody found") return nil, fmt.Errorf("no docbody found")
} }
docAttr := r.OFD.DocBody[0] docAttr := r.OFD.DocBody[r.documentIndex]
docRootPath := docAttr.DocRoot docRootPath := docAttr.DocRoot
r.RootDir = path.Dir(docRootPath) r.RootDir = path.Dir(docRootPath)
data, err := r.readFile(docRootPath) data, err := r.readFile(docRootPath)
@@ -197,10 +231,10 @@ func (r *Reader) DocType() string {
// DocInfo 获取文档元数据 // DocInfo 获取文档元数据
// 返回: *DocInfo 元数据, error 错误信息 // 返回: *DocInfo 元数据, error 错误信息
func (r *Reader) DocInfo() (*DocInfo, error) { func (r *Reader) DocInfo() (*DocInfo, error) {
if r.OFD == nil || len(r.OFD.DocBody) == 0 { if r.OFD == nil || r.documentIndex < 0 || r.documentIndex >= len(r.OFD.DocBody) {
return nil, fmt.Errorf("no docbody found") return nil, fmt.Errorf("no docbody found")
} }
return &r.OFD.DocBody[0].DocInfo, nil return &r.OFD.DocBody[r.documentIndex].DocInfo, nil
} }
// Permissions 获取文档权限信息 // Permissions 获取文档权限信息
+5 -2
View File
@@ -299,7 +299,7 @@ func (r *Reader) VerifyDocumentSignatures(index int, opts ...SignatureVerifyOpti
} }
body := r.OFD.DocBody[index] body := r.OFD.DocBody[index]
options.DocIndex, options.DocRoot = index, r.signatureCoveragePath(body.DocRoot) options.DocIndex, options.DocRoot = index, r.signatureCoveragePath(body.DocRoot)
if index == 0 && r.doc != nil && r.doc.Signatures == "" { if index == r.documentIndex && r.doc != nil && r.doc.Signatures == "" {
return nil, nil return nil, nil
} }
data, err := r.readFile(body.DocRoot) data, err := r.readFile(body.DocRoot)
@@ -316,7 +316,10 @@ func (r *Reader) VerifyDocumentSignatures(index int, opts ...SignatureVerifyOpti
if doc.Signatures == "" { if doc.Signatures == "" {
return nil, nil return nil, nil
} }
view := *r view := &Reader{Zip: r.Zip, files: r.files, encryption: r.encryption, documentIndex: index}
if err := view.initRoot(); err != nil {
return nil, err
}
view.doc, view.RootDir = &doc, path.Dir(body.DocRoot) view.doc, view.RootDir = &doc, path.Dir(body.DocRoot)
sigListPath := view.ResPath(doc.Signatures) sigListPath := view.ResPath(doc.Signatures)
data, err = view.readFile(sigListPath) data, err = view.readFile(sigListPath)
+45 -12
View File
@@ -109,6 +109,12 @@ func SignEditorTo(writer io.Writer, editor *Editor, options SignatureWriteOption
if err != nil { if err != nil {
return 0, err return 0, err
} }
if editor.source != nil {
r, err = r.Document(editor.source.reader.DocumentIndex())
if err != nil {
return 0, err
}
}
r.encryption = editor.encryption r.encryption = editor.encryption
options.progress = editorProgress(editor.OnWriteProgress) options.progress = editorProgress(editor.OnWriteProgress)
return r.SignTo(writer, options) return r.SignTo(writer, options)
@@ -167,7 +173,7 @@ func signatureWriteOutput(r *Reader, options SignatureWriteOptions) ([]byte, err
if err := options.progress.report("sign", 0, 1); err != nil { if err := options.progress.report("sign", 0, 1); err != nil {
return nil, err return nil, err
} }
data, err := signatureWritePackage(parts, options) data, err := signatureWritePackage(parts, options, r.DocumentIndex())
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -213,27 +219,49 @@ func signatureWriteParts(r *Reader) (map[string][]byte, error) {
// signatureWritePackage 完成输出优化后计算签名,不改写已有签名保护的资源 // signatureWritePackage 完成输出优化后计算签名,不改写已有签名保护的资源
// 签名目录采用Sign_N兼容OFDRW容器,标识使用独立的sN签章域 // 签名目录采用Sign_N兼容OFDRW容器,标识使用独立的sN签章域
// 入参: parts 独立包文件, options 签署选项 // 入参: parts 独立包文件, options 签署选项, index 文档索引
// 返回: []byte 完整包, error 错误信息 // 返回: []byte 完整包, error 错误信息
func signatureWritePackage(parts map[string][]byte, options SignatureWriteOptions) ([]byte, error) { func signatureWritePackage(parts map[string][]byte, options SignatureWriteOptions, index int) ([]byte, error) {
if err := options.Compression.Validate(); err != nil { if err := options.Compression.Validate(); err != nil {
return nil, err return nil, err
} }
if options.Mode != SignatureAppend && options.Mode != SignatureReplace { if options.Mode != SignatureAppend && options.Mode != SignatureReplace {
return nil, fmt.Errorf("unsupported signature write mode") return nil, fmt.Errorf("unsupported signature write mode")
} }
r := &Reader{files: parts} r := &Reader{files: parts, documentIndex: index}
if err := r.initRoot(); err != nil { if err := r.initRoot(); err != nil {
return nil, err return nil, err
} }
if len(r.OFD.DocBody) != 1 {
return nil, fmt.Errorf("signature writing requires exactly one document")
}
doc, err := r.Doc() doc, err := r.Doc()
if err != nil { if err != nil {
return nil, err return nil, err
} }
if options.Compression.Mode != CompressionUnchanged && doc.Signatures == "" { protected := make(map[string][]byte)
for other := range r.OFD.DocBody {
if other == index {
continue
}
view, err := r.Document(other)
if err != nil {
return nil, err
}
if view.doc.Signatures == "" {
continue
}
reports, err := r.VerifyDocumentSignatures(other)
if err != nil {
return nil, err
}
for _, report := range reports {
if report.Error != "" {
return nil, fmt.Errorf("cannot preserve document %d signature: %s", other, report.Error)
}
for _, ref := range report.References {
protected[ref.Path] = bytes.Clone(parts[ref.Path])
}
}
}
if options.Compression.Mode != CompressionUnchanged && doc.Signatures == "" && len(protected) == 0 {
images, err := r.Images(context.Background()) images, err := r.Images(context.Background())
if err != nil { if err != nil {
return nil, err return nil, err
@@ -271,7 +299,7 @@ func signatureWritePackage(parts map[string][]byte, options SignatureWriteOption
delete(parts, name) delete(parts, name)
} }
} }
r = &Reader{files: parts} r = &Reader{files: parts, documentIndex: index}
if err := r.initRoot(); err != nil { if err := r.initRoot(); err != nil {
return nil, err return nil, err
} }
@@ -307,7 +335,7 @@ func signatureWritePackage(parts map[string][]byte, options SignatureWriteOption
verifyOptions := append([]SignatureVerifyOption(nil), options.ExistingVerifyOptions...) verifyOptions := append([]SignatureVerifyOption(nil), options.ExistingVerifyOptions...)
verifyOptions = append(verifyOptions, WithSignatureCert(identity.cert.Raw)) verifyOptions = append(verifyOptions, WithSignatureCert(identity.cert.Raw))
if options.Mode == SignatureAppend && oldCount > 0 { if options.Mode == SignatureAppend && oldCount > 0 {
reports, err := r.VerifySignatures(verifyOptions...) reports, err := r.VerifyDocumentSignatures(index, verifyOptions...)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -330,7 +358,7 @@ func signatureWritePackage(parts map[string][]byte, options SignatureWriteOption
if err != nil { if err != nil {
return nil, err return nil, err
} }
body := root.child("DocBody") body := root.childAt("DocBody", index)
if body == nil { if body == nil {
return nil, fmt.Errorf("missing document body") return nil, fmt.Errorf("missing document body")
} }
@@ -411,6 +439,11 @@ func signatureWritePackage(parts map[string][]byte, options SignatureWriteOption
if err != nil { if err != nil {
return nil, err return nil, err
} }
for name, before := range protected {
if !bytes.Equal(before, parts[name]) {
return nil, fmt.Errorf("another document signature protects %s", name)
}
}
output, err := signatureWriteZIP(parts, options.Compression) output, err := signatureWriteZIP(parts, options.Compression)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -419,7 +452,7 @@ func signatureWritePackage(parts map[string][]byte, options SignatureWriteOption
if err != nil { if err != nil {
return nil, err return nil, err
} }
reports, err := check.VerifySignatures(verifyOptions...) reports, err := check.VerifyDocumentSignatures(index, verifyOptions...)
if err != nil { if err != nil {
return nil, err return nil, err
} }