签名
diff --git a/assets/webui/ofdgo.css b/assets/webui/ofdgo.css
index 7887bb1..49c471c 100644
--- a/assets/webui/ofdgo.css
+++ b/assets/webui/ofdgo.css
@@ -411,7 +411,7 @@ body[data-hide-meta] .meta-panel {
list-style-position: outside;
}
-.outline-leaf {
+.outline-list:has(> li > details) .outline-leaf {
padding-left: 14px;
}
@@ -731,12 +731,43 @@ button.outline-link:hover {
.font-panel,
.doc-font-panel,
+.attachment-panel,
.signature-panel {
margin-top: 18px;
padding-top: 12px;
border-top: 1px solid var(--line);
}
+.attachment-list {
+ display: grid;
+ gap: 8px;
+}
+
+.attachment-item {
+ width: 100%;
+ padding: 4px 0;
+ border: 0;
+ background: transparent;
+ color: var(--accent-strong);
+ font-size: 13px;
+ line-height: 1.4;
+ text-align: left;
+ overflow-wrap: anywhere;
+}
+
+.attachment-item:not(:disabled):hover {
+ background: transparent;
+}
+
+.attachment-item:hover .attachment-name {
+ text-decoration: underline;
+}
+
+.attachment-detail {
+ color: var(--text-muted);
+ font-size: 11px;
+}
+
.count-pill {
display: inline-flex;
align-items: center;
diff --git a/assets/webui/ofdgo.js b/assets/webui/ofdgo.js
index 4e7869a..5ea93f3 100644
--- a/assets/webui/ofdgo.js
+++ b/assets/webui/ofdgo.js
@@ -110,6 +110,8 @@ const el = {
metaType: document.querySelector("#metaType"),
metaFonts: document.querySelector("#metaFonts"),
metaSignatures: document.querySelector("#metaSignatures"),
+ attachmentPanel: document.querySelector("#attachmentPanel"),
+ attachmentList: document.querySelector("#attachmentList"),
signaturePanel: document.querySelector("#signaturePanel"),
signatureSummary: document.querySelector("#signatureSummary"),
signatureList: document.querySelector("#signatureList"),
@@ -1288,6 +1290,34 @@ async function renderPage(index, options = {}) {
}
}
+async function downloadAttachment(attachment) {
+ if (!state.doc || document.body.hasAttribute("aria-busy")) {
+ return;
+ }
+ const openSeq = state.openSeq;
+ state.exporting = true;
+ updateControls();
+ setBusy(true, "正在读取附件", null, "正在读取附件");
+ try {
+ const result = await callWASM("ofdgoAttachmentData", attachment.id);
+ if (openSeq !== state.openSeq) {
+ return;
+ }
+ downloadBytes(result.bytes, "application/octet-stream", attachment.fileName);
+ setStatus(`附件已下载 ${formatBytes(result.bytes.length, "0 KB")}`);
+ } catch (err) {
+ if (openSeq === state.openSeq) {
+ showError(err, false);
+ }
+ } finally {
+ state.exporting = false;
+ if (openSeq === state.openSeq) {
+ setBusy(false);
+ updateControls();
+ }
+ }
+}
+
async function exportPDF() {
if (!state.doc || document.body.hasAttribute("aria-busy")) {
return;
@@ -2016,12 +2046,47 @@ function renderMeta() {
el.metaFonts.textContent = String(doc.fontCount || 0);
el.metaSignatures.textContent = doc.detailsPending ? "正在检查" : String(doc.signatureCount || 0);
el.pageTotal.textContent = String(doc.pageCount || 0);
+ renderAttachments();
renderSignatures();
renderDocumentFonts();
renderFontList();
updateLocalFontButton();
}
+function renderAttachments() {
+ const attachments = state.doc?.attachments || [];
+ el.attachmentPanel.hidden = !attachments.length && !state.doc?.attachmentError;
+ el.attachmentList.replaceChildren();
+ if (state.doc?.attachmentError) {
+ const error = document.createElement("div");
+ error.className = "attachment-detail";
+ error.textContent = "附件读取失败";
+ error.title = state.doc.attachmentError;
+ el.attachmentList.append(error);
+ return;
+ }
+ for (const attachment of attachments) {
+ const item = document.createElement("button");
+ item.type = "button";
+ item.className = "attachment-item";
+ 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, "0 KB")].filter(Boolean).join(" · ");
+ if (detail) {
+ const meta = document.createElement("span");
+ meta.className = "attachment-detail";
+ meta.textContent = ` · ${detail}`;
+ item.append(meta);
+ }
+ item.addEventListener("click", () => downloadAttachment(attachment));
+ el.attachmentList.append(item);
+ }
+}
+
function renderSignatures() {
const signatures = state.doc?.signatures || [];
const showPanel = Boolean(state.doc?.signatureError || signatures.length);
diff --git a/internal/webui/wasm.go b/internal/webui/wasm.go
index 08bff3b..6bd4f49 100644
--- a/internal/webui/wasm.go
+++ b/internal/webui/wasm.go
@@ -37,6 +37,7 @@ func RunWASM() {
registerCallback("ofdgoOpen", openDocument)
registerCallback("ofdgoConfigure", configureDocument)
registerCallback("ofdgoDocumentInfo", documentInfo)
+ registerCallback("ofdgoAttachmentData", attachmentData)
registerCallback("ofdgoRenderPage", renderPage)
registerCallback("ofdgoExportFormats", exportFormats)
registerCallback("ofdgoExportPage", exportPage)
@@ -131,6 +132,20 @@ func documentInfo(args []js.Value) (any, error) {
return currentSession.Info(), nil
}
+// attachmentData 读取附件数据
+// 入参: args 浏览器参数
+// 返回: any 附件数据, error 错误信息
+func attachmentData(args []js.Value) (any, error) {
+ if currentSession == nil {
+ return nil, fmt.Errorf("ofd document is not opened")
+ }
+ data, err := currentSession.Reader.AttachmentData(args[0].String())
+ if err != nil {
+ return nil, err
+ }
+ return successResult(map[string]any{"bytes": bytesToJS(data)}), nil
+}
+
// renderPage 渲染OFD页面
// 入参: args 浏览器参数
// 返回: any 页面SVG结果, error 错误信息
diff --git a/internal/webui/webui.go b/internal/webui/webui.go
index 3252a6c..1b53ad9 100644
--- a/internal/webui/webui.go
+++ b/internal/webui/webui.go
@@ -22,6 +22,7 @@ import (
"image/draw"
"image/jpeg"
"image/png"
+ "path"
"strings"
"time"
@@ -113,27 +114,38 @@ type Session struct {
// DocumentInfo 文档信息
type DocumentInfo struct {
- 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"`
- Fonts []FontInfo `json:"fonts"`
- Signatures []SignatureInfo `json:"signatures"`
- Pages []PageInfo `json:"pages"`
- Outlines []OutlineInfo `json:"outlines,omitempty"`
- DetailsPending bool `json:"detailsPending,omitempty"`
+ 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"`
@@ -307,6 +319,21 @@ func (s *Session) Info() DocumentInfo {
Pages: make([]PageInfo, 0, len(s.doc.Pages.Page)),
Outlines: s.doc.OutlineInfos(),
}
+ if attachments, err := s.Reader.Attachments(); err == nil {
+ for _, attachment := range attachments {
+ if attachment.Visible {
+ 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)),
+ })
+ }
+ }
+ } else {
+ info.AttachmentError = err.Error()
+ }
if signatures, err := s.signatureInfos(); err == nil {
info.Signatures = signatures
} else {
diff --git a/ofdgo_reader.go b/ofdgo_reader.go
index 2d5c289..9e5208b 100644
--- a/ofdgo_reader.go
+++ b/ofdgo_reader.go
@@ -505,7 +505,9 @@ func (r *Reader) Attachments() ([]Attachment, error) {
}
for i := range attachments.Attachment {
attachment := &attachments.Attachment[i]
- attachment.FileLoc = resolveResourcePath(partPath, "", attachment.FileLoc)
+ if fileLoc := resolveResourcePath(partPath, "", attachment.FileLoc); fileLoc != "" {
+ attachment.FileLoc = "/" + fileLoc
+ }
}
if attachments.Attachment == nil {
attachments.Attachment = []Attachment{}
@@ -515,6 +517,22 @@ func (r *Reader) Attachments() ([]Attachment, error) {
return doc.Attachments.Attachment, nil
}
+// AttachmentData 获取附件文件数据
+// 入参: id 附件标识
+// 返回: []byte 附件数据, error 错误信息
+func (r *Reader) AttachmentData(id string) ([]byte, error) {
+ attachments, err := r.Attachments()
+ if err != nil {
+ return nil, err
+ }
+ for _, attachment := range attachments {
+ if attachment.ID == id {
+ return r.ResData(attachment.FileLoc)
+ }
+ }
+ return nil, fmt.Errorf("attachment not found: %s", id)
+}
+
// CustomTags 获取自定义标引
// 返回: []CustomTag 自定义标引列表, error 错误信息
func (r *Reader) CustomTags() ([]CustomTag, error) {