mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 17:27:08 +08:00
feat(附件管理): 完善附件管理功能
This commit is contained in:
1 parent
418a2fa0a7
commit
38004171cf
10 files changed
+254
-42
No files matched your search
+13
-1
@@ -489,6 +489,18 @@
|
||||
<div class="form-actions"><div id="credentialsStatus" class="form-status" role="status" aria-live="polite"></div><button id="credentialsCancel" class="button" type="button">取消</button><button class="button" type="submit">解锁</button></div>
|
||||
</form>
|
||||
</dialog>
|
||||
<dialog id="attachmentDialog" class="form-dialog" aria-labelledby="attachmentTitle">
|
||||
<form id="attachmentForm">
|
||||
<h2 id="attachmentTitle" class="form-title">添加附件</h2>
|
||||
<div id="attachmentFileRow" class="form-row"><label for="attachmentFile">文件</label><input id="attachmentFile" type="file" multiple required></div>
|
||||
<div id="attachmentNameRow" class="form-row" hidden><label for="attachmentName">名称</label><input id="attachmentName" type="text" autocomplete="off" spellcheck="false"></div>
|
||||
<div class="form-actions">
|
||||
<div id="attachmentStatus" class="form-status" role="status"></div>
|
||||
<button id="attachmentCancel" class="button" type="button">取消</button>
|
||||
<button id="attachmentSubmit" class="button" type="submit">添加</button>
|
||||
</div>
|
||||
</form>
|
||||
</dialog>
|
||||
<dialog id="encryptionPanel" class="form-dialog" aria-labelledby="encryptionTitle">
|
||||
<form id="encryptionForm">
|
||||
<h2 id="encryptionTitle" class="form-title">加密文档</h2>
|
||||
@@ -640,7 +652,7 @@
|
||||
<dl class="meta-list"><div><dt>加密</dt><dd id="securityEncryption">未加密</dd></div><div id="securitySchemeRow" hidden><dt>方式</dt><dd id="securityScheme"></dd></div><div id="securityMethodRow" hidden><dt>算法</dt><dd id="securityMethod"></dd></div></dl>
|
||||
</section>
|
||||
<section id="attachmentPanel" class="attachment-panel" aria-labelledby="attachmentPanelTitle" hidden>
|
||||
<div id="attachmentPanelTitle" class="panel-title">附件</div>
|
||||
<div class="panel-row"><div id="attachmentPanelTitle" class="panel-title compact">附件</div><div class="panel-actions"><button id="attachmentAdd" class="small-button" type="button" title="添加附件" aria-haspopup="dialog" aria-controls="attachmentDialog" hidden>添加</button></div></div>
|
||||
<div id="attachmentList" class="attachment-list"></div>
|
||||
</section>
|
||||
<section id="signaturePanel" class="signature-panel" aria-labelledby="signaturePanelTitle" hidden>
|
||||
|
||||
@@ -1904,6 +1904,7 @@ button.outline-link:hover {
|
||||
.empty-state[hidden],
|
||||
.editor-group > [hidden],
|
||||
.panel-row > [hidden],
|
||||
.panel-actions > [hidden],
|
||||
.meta-list > [hidden],
|
||||
.navigation-tabs[hidden],
|
||||
.page-selection-tools[hidden],
|
||||
@@ -2549,6 +2550,26 @@ button.outline-link:hover {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.attachment-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.attachment-row .attachment-item {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.attachment-actions {
|
||||
flex: 0 0 60px;
|
||||
width: 60px;
|
||||
border: 1px solid var(--line-strong);
|
||||
background: #ffffff;
|
||||
color: #1f2428;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.attachment-item {
|
||||
width: 100%;
|
||||
padding: 4px 0;
|
||||
|
||||
+93
-7
@@ -481,6 +481,17 @@ const el = {
|
||||
metaFonts: document.querySelector("#metaFonts"),
|
||||
attachmentPanel: document.querySelector("#attachmentPanel"),
|
||||
attachmentList: document.querySelector("#attachmentList"),
|
||||
attachmentAdd: document.querySelector("#attachmentAdd"),
|
||||
attachmentDialog: document.querySelector("#attachmentDialog"),
|
||||
attachmentForm: document.querySelector("#attachmentForm"),
|
||||
attachmentTitle: document.querySelector("#attachmentTitle"),
|
||||
attachmentFile: document.querySelector("#attachmentFile"),
|
||||
attachmentFileRow: document.querySelector("#attachmentFileRow"),
|
||||
attachmentName: document.querySelector("#attachmentName"),
|
||||
attachmentNameRow: document.querySelector("#attachmentNameRow"),
|
||||
attachmentStatus: document.querySelector("#attachmentStatus"),
|
||||
attachmentCancel: document.querySelector("#attachmentCancel"),
|
||||
attachmentSubmit: document.querySelector("#attachmentSubmit"),
|
||||
signaturePanel: document.querySelector("#signaturePanel"),
|
||||
signatureSummary: document.querySelector("#signatureSummary"),
|
||||
signatureList: document.querySelector("#signatureList"),
|
||||
@@ -2916,6 +2927,8 @@ async function changeDocument(name, item, ...args) {
|
||||
el.outlineStatus.textContent = err.message;
|
||||
} else if (el.infoPanel.open) {
|
||||
el.infoStatus.textContent = err.message;
|
||||
} else if (el.attachmentDialog.open) {
|
||||
el.attachmentStatus.textContent = err.message;
|
||||
} else if (el.pagePanel.open) {
|
||||
el.pageStatus.textContent = err.message;
|
||||
} else if (el.paragraphPanel.open) {
|
||||
@@ -4380,6 +4393,43 @@ function downloadAttachment(attachment) {
|
||||
return downloadResource(attachment, "ofdgoExportAttachment", "附件");
|
||||
}
|
||||
|
||||
editorClick(el.attachmentAdd, () => openAttachmentDialog("add"));
|
||||
el.attachmentCancel.addEventListener("click", () => el.attachmentDialog.close());
|
||||
el.attachmentDialog.addEventListener("cancel", event => { if (el.attachmentForm.inert) event.preventDefault(); });
|
||||
el.attachmentDialog.addEventListener("close", () => {
|
||||
el.attachmentForm.reset();
|
||||
delete el.attachmentDialog.attachmentEdit;
|
||||
});
|
||||
el.attachmentForm.addEventListener("submit", async event => {
|
||||
event.preventDefault();
|
||||
const edit = el.attachmentDialog.attachmentEdit;
|
||||
if (!edit || edit.openSeq !== state.openSeq || !el.attachmentForm.reportValidity()) return;
|
||||
const files = Array.from(el.attachmentFile.files);
|
||||
const names = edit.action === "rename" ? [el.attachmentName.value] : files.map(file => file.name);
|
||||
if (await changeDocument("ofdgoChangeAttachment", null, edit.action, edit.id, names, files)) {
|
||||
el.attachmentDialog.close();
|
||||
setStatus({add:"附件已添加", rename:"附件已改名", replace:"附件已替换"}[edit.action]);
|
||||
}
|
||||
});
|
||||
|
||||
function openAttachmentDialog(action, attachment) {
|
||||
if (!state.editing || !state.ready || state.exporting || document.body.hasAttribute("aria-busy")) return;
|
||||
const rename = action === "rename";
|
||||
el.attachmentForm.reset();
|
||||
el.attachmentDialog.attachmentEdit = {action, id: attachment?.id || "", openSeq: state.openSeq};
|
||||
el.attachmentTitle.textContent = {add:"添加附件", rename:"附件改名", replace:"替换附件"}[action];
|
||||
el.attachmentSubmit.textContent = {add:"添加", rename:"确定", replace:"替换"}[action];
|
||||
el.attachmentFileRow.hidden = rename;
|
||||
el.attachmentFile.required = !rename;
|
||||
el.attachmentFile.multiple = action === "add";
|
||||
el.attachmentNameRow.hidden = !rename;
|
||||
el.attachmentName.required = rename;
|
||||
el.attachmentName.value = attachment?.name || "";
|
||||
el.attachmentStatus.textContent = "";
|
||||
el.attachmentDialog.showModal();
|
||||
(rename ? el.attachmentName : el.attachmentFile).focus();
|
||||
}
|
||||
|
||||
async function saveDocumentImage(resource) {
|
||||
if (!resource || !state.doc || document.body.hasAttribute("aria-busy")) return;
|
||||
const seq = state.openSeq;
|
||||
@@ -7241,7 +7291,7 @@ function renderMeta(keepDetails = false) {
|
||||
el.pageTotal.textContent = String(doc.pageCount || 0);
|
||||
if (keepDetails && doc.detailsPending) return;
|
||||
el.metaSignatures.textContent = doc.detailsPending ? "正在检查签名" : doc.detailsError ? "读取失败" : String(doc.signatureCount || 0);
|
||||
renderMetaContent(el.attachmentList, [doc.attachments, doc.attachmentError], renderAttachments);
|
||||
renderMetaContent(el.attachmentList, [doc.attachments, doc.attachmentError, state.editing], renderAttachments);
|
||||
renderMetaContent(el.signatureList, [doc.signatures, doc.signatureError], renderSignatures);
|
||||
renderMetaContent(el.docFontList, doc.fonts || [], renderDocumentFonts);
|
||||
refreshEditorFonts();
|
||||
@@ -7256,8 +7306,9 @@ function renderMetaContent(node, value, render) {
|
||||
}
|
||||
|
||||
function renderAttachments() {
|
||||
const attachments = state.doc?.attachments || [];
|
||||
el.attachmentPanel.hidden = !attachments.length && !state.doc?.attachmentError;
|
||||
const attachments = (state.doc?.attachments || []).filter(item => state.editing || item.visible !== false);
|
||||
el.attachmentPanel.hidden = !state.doc || !state.editing && !attachments.length && !state.doc.attachmentError;
|
||||
el.attachmentAdd.hidden = !state.editing;
|
||||
el.attachmentList.replaceChildren();
|
||||
if (state.doc?.attachmentError) {
|
||||
const error = document.createElement("div");
|
||||
@@ -7267,17 +7318,25 @@ function renderAttachments() {
|
||||
el.attachmentList.append(error);
|
||||
return;
|
||||
}
|
||||
if (!attachments.length) {
|
||||
const empty = document.createElement("div");
|
||||
empty.className = "attachment-detail";
|
||||
empty.textContent = "暂无附件";
|
||||
el.attachmentList.append(empty);
|
||||
}
|
||||
for (const attachment of attachments) {
|
||||
const row = document.createElement("div");
|
||||
row.className = "attachment-row";
|
||||
const item = document.createElement("button");
|
||||
item.type = "button";
|
||||
item.className = "attachment-item";
|
||||
item.title = `下载 ${attachment.fileName}`;
|
||||
item.setAttribute("aria-label", `下载 ${attachment.name}`);
|
||||
item.title = `另存 ${attachment.fileName}`;
|
||||
item.setAttribute("aria-label", `另存 ${attachment.name}`);
|
||||
const name = document.createElement("span");
|
||||
name.className = "attachment-name";
|
||||
name.textContent = attachment.name;
|
||||
item.append(name);
|
||||
const detail = [attachment.format, attachment.size == null ? "" : formatBytes(attachment.size * 1024)].filter(Boolean).join(" · ");
|
||||
const detail = [attachment.format, attachment.size == null ? "" : formatBytes(attachment.size * 1024), attachment.visible === false ? "隐藏" : ""].filter(Boolean).join(" · ");
|
||||
if (detail) {
|
||||
const meta = document.createElement("span");
|
||||
meta.className = "attachment-detail";
|
||||
@@ -7285,7 +7344,30 @@ function renderAttachments() {
|
||||
item.append(meta);
|
||||
}
|
||||
item.addEventListener("click", () => downloadAttachment(attachment));
|
||||
el.attachmentList.append(item);
|
||||
row.append(item);
|
||||
if (state.editing) {
|
||||
const actions = document.createElement("select");
|
||||
actions.className = "small-button attachment-actions";
|
||||
actions.title = "附件操作";
|
||||
actions.setAttribute("aria-label", `${attachment.name} 操作`);
|
||||
for (const [value, text] of [["", "操作"], ["rename", "改名"], ["replace", "替换"], ["delete", "删除"]]) {
|
||||
const option = document.createElement("option");
|
||||
option.value = value;
|
||||
option.textContent = text;
|
||||
actions.append(option);
|
||||
}
|
||||
actions.addEventListener("change", async () => {
|
||||
const action = actions.value;
|
||||
actions.value = "";
|
||||
if ((canvasEditor.nudge || canvasEditor.nudgeCommit) && !await canvasEditor.commitNudge()) return;
|
||||
if (!await canvasEditor.commitText() || !await canvasEditor.commitCrop()) return;
|
||||
if (action === "delete") {
|
||||
if (window.confirm(`删除附件“${attachment.name}”?`) && await changeDocument("ofdgoChangeAttachment", null, action, attachment.id, [], [])) setStatus("附件已删除");
|
||||
} else if (action) openAttachmentDialog(action, attachment);
|
||||
});
|
||||
row.append(actions);
|
||||
}
|
||||
el.attachmentList.append(row);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8005,6 +8087,8 @@ function updateFitSpace() {
|
||||
|
||||
function updateControls() {
|
||||
renderSecurity();
|
||||
el.attachmentAdd.disabled = !state.editing || !state.ready || state.exporting;
|
||||
renderMetaContent(el.attachmentList, [state.doc?.attachments, state.doc?.attachmentError, state.editing], renderAttachments);
|
||||
const hasDoc = Boolean(state.doc);
|
||||
updateDisplayControls();
|
||||
updateEditorTools();
|
||||
@@ -8217,6 +8301,8 @@ function setBusy(busy, text = "", percent = 0, status = "") {
|
||||
el.pageForm.inert = busy;
|
||||
el.paragraphForm.inert = busy;
|
||||
el.infoForm.inert = busy;
|
||||
el.attachmentForm.inert = busy;
|
||||
el.attachmentPanel.inert = busy;
|
||||
el.annotationForm.inert = busy;
|
||||
el.importForm.inert = busy && !state.importing;
|
||||
for (const input of [el.importFile, el.importRange, el.importPosition, el.importOutlines]) input.disabled = busy;
|
||||
|
||||
@@ -173,6 +173,13 @@ async function handleMessage({ id, name, args }) {
|
||||
const password = converting ? args[2] : name === "ofdgoSetEncryption" ? args[0]?.password : name === "ofdgoOpen" ? args[3]?.password : name === "ofdgoLoadImport" ? args[1]?.password : null;
|
||||
try {
|
||||
signal?.throwIfAborted();
|
||||
if (name === "ofdgoChangeAttachment" && (args[0] === "add" || args[0] === "replace")) {
|
||||
const files = args[3], reader = new FileReaderSync();
|
||||
args[3] = index => {
|
||||
try { return { bytes: new Uint8Array(reader.readAsArrayBuffer(files[index])) }; }
|
||||
catch (err) { return { error: err.message }; }
|
||||
};
|
||||
}
|
||||
if (name === "ofdgoImportImages") {
|
||||
const files = args[0], reader = new FileReaderSync();
|
||||
args[0] = files.map(file => file.name);
|
||||
|
||||
+41
-2
@@ -30,7 +30,6 @@ import (
|
||||
"io"
|
||||
"io/fs"
|
||||
"math"
|
||||
"path"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strconv"
|
||||
@@ -212,6 +211,7 @@ func RunWASM() {
|
||||
registerAsyncCallback("ofdgoImportImages", importImages)
|
||||
registerCallback("ofdgoEditDocument", editDocument)
|
||||
registerCallback("ofdgoUpdateInfo", updateInfo)
|
||||
registerCallback("ofdgoChangeAttachment", changeAttachment)
|
||||
registerCallback("ofdgoChangePage", changePage)
|
||||
registerCallback("ofdgoBatchPages", batchPages)
|
||||
registerCallback("ofdgoChangeOutline", changeOutline)
|
||||
@@ -801,7 +801,7 @@ func renderPage(args []js.Value) (any, error) {
|
||||
return nil, err
|
||||
}
|
||||
for _, attachment := range attachments {
|
||||
attachmentNames[attachment.ID] = path.Base(currentSession.Reader.ResPath(attachment.FileLoc))
|
||||
attachmentNames[attachment.ID] = attachment.FileName()
|
||||
}
|
||||
break
|
||||
}
|
||||
@@ -3535,6 +3535,45 @@ func updateInfo(args []js.Value) (any, error) {
|
||||
})
|
||||
}
|
||||
|
||||
// changeAttachment 编辑附件,逐个读取文件并复用库层事务和历史
|
||||
// 入参: args 操作、附件标识、名称列表、文件读取回调
|
||||
// 返回: any 编辑状态, error 错误信息
|
||||
func changeAttachment(args []js.Value) (any, error) {
|
||||
return changeObjects(func() error {
|
||||
action, id := args[0].String(), args[1].String()
|
||||
switch action {
|
||||
case "rename":
|
||||
return currentEditor.RenameAttachment(id, args[2].Index(0).String())
|
||||
case "delete":
|
||||
return currentEditor.DeleteAttachment(id)
|
||||
case "add", "replace":
|
||||
count := args[2].Length()
|
||||
if count == 0 || action == "replace" && count != 1 {
|
||||
return fmt.Errorf("请选择附件")
|
||||
}
|
||||
for i := range count {
|
||||
result := args[3].Invoke(i)
|
||||
if message := result.Get("error"); message.Type() == js.TypeString {
|
||||
return fmt.Errorf("%s", message.String())
|
||||
}
|
||||
data, err := bytesFromJS(result.Get("bytes"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if action == "replace" {
|
||||
return currentEditor.ReplaceAttachment(id, data)
|
||||
}
|
||||
if _, err := currentEditor.AddAttachment(args[2].Index(i).String(), data); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("unsupported attachment action %q", action)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// editDocument 将已打开文档接入编辑器,沿用页面、资源及字体配置
|
||||
// 入参: args 当前页索引,省略时使用首页
|
||||
// 返回: any 编辑状态, error 错误信息
|
||||
|
||||
@@ -18,7 +18,6 @@ import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"path"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strings"
|
||||
@@ -111,13 +110,14 @@ type DocumentDetails struct {
|
||||
// OutlineInfo 目录节点信息
|
||||
type OutlineInfo = ofdgo.OutlineInfo
|
||||
|
||||
// AttachmentInfo 可见附件信息,Size单位为KB
|
||||
// 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"`
|
||||
Visible bool `json:"visible"`
|
||||
}
|
||||
|
||||
// PageInfo 页面信息
|
||||
@@ -419,13 +419,14 @@ func (s *Session) Details() DocumentDetails {
|
||||
info := DocumentDetails{}
|
||||
if attachments, err := s.Reader.Attachments(); err == nil {
|
||||
for _, attachment := range attachments {
|
||||
if attachment.Visible {
|
||||
if attachment.Visible || s.editing {
|
||||
info.Attachments = append(info.Attachments, AttachmentInfo{
|
||||
ID: attachment.ID,
|
||||
Name: attachment.Name,
|
||||
Format: attachment.Format,
|
||||
Size: attachment.Size,
|
||||
FileName: path.Base(s.Reader.ResPath(attachment.FileLoc)),
|
||||
FileName: attachment.FileName(),
|
||||
Visible: attachment.Visible,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
+24
-1
@@ -14,7 +14,11 @@
|
||||
|
||||
package ofdgo
|
||||
|
||||
import "encoding/xml"
|
||||
import (
|
||||
"encoding/xml"
|
||||
"path"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Document 文档结构
|
||||
type Document struct {
|
||||
@@ -226,6 +230,25 @@ func (doc *Document) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error
|
||||
return nil
|
||||
}
|
||||
|
||||
// FileName 获取另存文件名,优先使用带扩展名的显示名称,剔除目录及文件名非法字符
|
||||
// 返回: string 文件名
|
||||
func (a Attachment) FileName() string {
|
||||
name := path.Base(strings.ReplaceAll(a.Name, "\\", "/"))
|
||||
file := path.Base(strings.ReplaceAll(a.FileLoc, "\\", "/"))
|
||||
if name == "." || name == "/" || path.Ext(name) == "" && path.Ext(file) != "" {
|
||||
name = file
|
||||
}
|
||||
if name == "." || name == "/" {
|
||||
name = "attachment"
|
||||
}
|
||||
return strings.Map(func(r rune) rune {
|
||||
if r < 32 || strings.ContainsRune(`<>:"/\|?*`, r) {
|
||||
return '_'
|
||||
}
|
||||
return r
|
||||
}, name)
|
||||
}
|
||||
|
||||
// UnmarshalXML 解析附件并应用默认值
|
||||
// 入参: d XML解码器, start 起始节点
|
||||
// 返回: error 错误信息
|
||||
|
||||
+34
-15
@@ -18,9 +18,9 @@ import (
|
||||
"bytes"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"maps"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
@@ -130,7 +130,7 @@ func (e *Editor) RenameAttachment(id, name string) error {
|
||||
})
|
||||
}
|
||||
|
||||
// ReplaceAttachment 替换附件内容,使用独立资源避免影响共享引用,保留名称和格式
|
||||
// ReplaceAttachment 替换附件内容,使用独立资源避免影响共享引用,保留名称和格式,不新增日期
|
||||
// 入参: id 附件标识, data 文件数据,调用后可复用
|
||||
// 返回: error 附件结构或提交错误
|
||||
func (e *Editor) ReplaceAttachment(id string, data []byte) error {
|
||||
@@ -150,23 +150,28 @@ func (e *Editor) ReplaceAttachment(id string, data []byte) error {
|
||||
} else {
|
||||
fragment = editorPatchXML(fragment, []editorXMLPatch{{node.open, node.open, editorXMLText("FileLoc", "/"+file)}})
|
||||
}
|
||||
for _, attr := range []struct{ name, value string }{
|
||||
{"Size", ofdNumber(float64(len(data)) / 1024)}, {"ModDate", time.Now().UTC().Format(time.RFC3339)},
|
||||
} {
|
||||
root, err := parseEditorXML(fragment)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fragment, err = editorXMLAttribute(fragment, root, attr.name, attr.value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
root, err := parseEditorXML(fragment)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fragment, err = editorXMLAttribute(fragment, root, "Size", ofdNumber(float64(len(data))/1024))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
parts[file] = bytes.Clone(data)
|
||||
return fragment, nil
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteAttachment 删除附件声明,保存时清理可确认无引用的内容,支持撤销和重做
|
||||
// 入参: id 附件标识
|
||||
// 返回: error 附件结构或提交错误
|
||||
func (e *Editor) DeleteAttachment(id string) error {
|
||||
return e.editAttachment(id, func([]byte, *editorXML, map[string][]byte) ([]byte, error) {
|
||||
return nil, nil
|
||||
})
|
||||
}
|
||||
|
||||
// editAttachment 对单个附件片段执行原子编辑,兼容独立附件文件及内联声明
|
||||
// 入参: id 附件标识, change 片段及资源修改函数
|
||||
// 返回: error 结构、标识或提交错误
|
||||
@@ -194,7 +199,7 @@ func (e *Editor) editAttachment(id string, change func([]byte, *editorXML, map[s
|
||||
var found *editorXML
|
||||
if root != nil {
|
||||
for _, child := range root.children {
|
||||
if child.name.Local == "Attachment" && child.attr("ID") == id {
|
||||
if packageOFDNode(child, "Attachment") && child.attr("ID") == id {
|
||||
if found != nil {
|
||||
return fmt.Errorf("duplicate attachment ID: %s", id)
|
||||
}
|
||||
@@ -222,7 +227,21 @@ func (e *Editor) editAttachment(id string, change func([]byte, *editorXML, map[s
|
||||
return nil
|
||||
}
|
||||
parts[name] = editorPatchXML(data, []editorXMLPatch{{found.start, found.end, updated}})
|
||||
return e.commitAnnotationParts(base, parts)
|
||||
if err := e.commitAnnotationParts(base, parts); err != nil {
|
||||
return err
|
||||
}
|
||||
if file := node.child("FileLoc"); file != nil {
|
||||
location := strings.TrimSpace(editorImportText(fragment, file))
|
||||
if location != "" {
|
||||
retired := maps.Clone(base.retiredAttachments)
|
||||
if retired == nil {
|
||||
retired = make(map[string]bool)
|
||||
}
|
||||
retired[strings.ToLower(cleanPackagePath(base.reader.ResPath(location)))] = true
|
||||
e.source.retiredAttachments = retired
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateAttachmentName 校验非空附件名及XML可无损表示的Unicode字符
|
||||
|
||||
+11
-10
@@ -31,16 +31,17 @@ import (
|
||||
|
||||
// editorSource 保留输入包及按需加载的原始页面
|
||||
type editorSource struct {
|
||||
reader *Reader
|
||||
document *Document
|
||||
info DocInfo
|
||||
fallbackDocID string
|
||||
directory string
|
||||
pages map[string]*editorSourcePage
|
||||
origins map[string]*editorObjectOrigin
|
||||
idsReady bool
|
||||
annotationStates map[string]map[string]editorCompositeState
|
||||
annotationPages map[string]bool
|
||||
reader *Reader
|
||||
document *Document
|
||||
info DocInfo
|
||||
fallbackDocID string
|
||||
directory string
|
||||
pages map[string]*editorSourcePage
|
||||
origins map[string]*editorObjectOrigin
|
||||
idsReady bool
|
||||
annotationStates map[string]map[string]editorCompositeState
|
||||
annotationPages map[string]bool
|
||||
retiredAttachments map[string]bool
|
||||
}
|
||||
|
||||
// editorObjectOrigin 保留原对象的XML语义,供复制和局部更新复用
|
||||
|
||||
@@ -34,7 +34,7 @@ type editorResourceRefs struct {
|
||||
fonts map[string]*editorFontUsage
|
||||
}
|
||||
|
||||
// compactSourceResources 在保存副本中清理无引用字体和图片,并裁剪可完整确认用字的原有字体
|
||||
// compactSourceResources 在保存副本中清理无引用字体、图片和已删除附件,并裁剪可完整确认用字的原有字体
|
||||
// 按GB/T33190-2016附录A检查标识和路径引用,包括模板、注释、底纹、裁剪、版本和其他文档
|
||||
// 全包检查保留孤立XML中的引用;未知命名空间、扩展数据或无法解析的XML使本次清理跳过
|
||||
// 入参: parts 已修改和新增的包内条目, progress 保存进度回调
|
||||
@@ -89,7 +89,10 @@ func (e *Editor) compactSourceResources(parts map[string][]byte, progress editor
|
||||
if err := progress.report("references", len(ordered), len(ordered)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
removed := make(map[string]bool)
|
||||
removed := maps.Clone(e.source.retiredAttachments)
|
||||
if removed == nil {
|
||||
removed = make(map[string]bool)
|
||||
}
|
||||
usedFiles := maps.Clone(refs.files)
|
||||
updates := make(map[string][]byte)
|
||||
fontFiles := make(map[string]*editorFontUsage)
|
||||
|
||||
Reference in new issue
Block a user