From 6dceeee194888cb01bb99700362088be28199491 Mon Sep 17 00:00:00 2001 From: xiaoqidun Date: Mon, 28 Sep 2026 00:09:41 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E5=8A=A0=E5=AF=86=E6=96=87=E6=A1=A3):=20?= =?UTF-8?q?=E5=A2=9E=E5=BC=BA=E5=8A=A0=E5=AF=86=E6=89=93=E5=BC=80=E4=B8=8E?= =?UTF-8?q?=E5=AD=97=E4=BD=93=E8=BD=AC=E6=8D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/webui/index.html | 4 ++-- assets/webui/ofdgo.js | 32 +++++++++++++++++++++++++------- assets/webui/ofdgo_wasm.js | 2 +- go.mod | 2 +- go.sum | 4 ++-- internal/webui/wasm.go | 15 ++++++++++++--- ofdgo_font_cff.go | 30 +++++++++++++++++++++++++++++- ofdgo_pdf.go | 12 ++++++++++-- 8 files changed, 82 insertions(+), 19 deletions(-) diff --git a/assets/webui/index.html b/assets/webui/index.html index 6721dc0..1c0cdae 100644 --- a/assets/webui/index.html +++ b/assets/webui/index.html @@ -472,8 +472,8 @@

解锁文档

-
-
+
+
diff --git a/assets/webui/ofdgo.js b/assets/webui/ofdgo.js index 4f911e4..b395817 100644 --- a/assets/webui/ofdgo.js +++ b/assets/webui/ofdgo.js @@ -161,6 +161,8 @@ const el = { encryptSaveButton: document.querySelector("#encryptSaveButton"), credentialsPanel: document.querySelector("#credentialsPanel"), credentialsType: document.querySelector("#credentialsType"), + credentialsTypeRow: document.querySelector("#credentialsTypeRow"), + credentialsUserRow: document.querySelector("#credentialsUserRow"), credentialsPasswordRow: document.querySelector("#credentialsPasswordRow"), credentialsCertificateRow: document.querySelector("#credentialsCertificateRow"), credentialsKeyRow: document.querySelector("#credentialsKeyRow"), @@ -3387,7 +3389,7 @@ async function openOFD(file) { if (openSeq !== state.openSeq) return; let converted; try { - converted = await callWASM("ofdgoConvertPDF", bytes, await fontManager.files(fontManager.records())); + converted = await openWithCredentials(await fontManager.files(fontManager.records()), openSeq, bytes, false, true); } finally { if (openSeq === state.openSeq) { state.exportRequestID = 0; @@ -3881,8 +3883,14 @@ function finishCredentials(credentials) { } } -function requestCredentials(code, openSeq) { +function requestCredentials(code, openSeq, pdf = false) { finishCredentials(null); + el.credentialsTypeRow.hidden = pdf; + el.credentialsUserRow.hidden = pdf; + if (pdf) { + el.credentialsType.value = "password"; + updateCredentialsType(); + } el.credentialsStatus.textContent = code === "invalidCredentials" ? "解锁失败,请重试" : ""; el.progressPanel.hidden = true; return new Promise(resolve => { @@ -3892,19 +3900,21 @@ function requestCredentials(code, openSeq) { }); } -async function openWithCredentials(fonts, openSeq, data = state.ofdBytes, importing = false) { +async function openWithCredentials(fonts, openSeq, data = state.ofdBytes, importing = false, pdf = false) { let credentials = null; try { while (openSeq === state.openSeq) { try { - return importing ? await callWASM("ofdgoLoadImport", data.slice(), credentials) + return pdf ? await callWASM("ofdgoConvertPDF", data, fonts, credentials?.password || null) + : importing ? await callWASM("ofdgoLoadImport", data.slice(), credentials) : await callWASM("ofdgoOpen", data, fonts, state.renderAnnotations, credentials); } catch (err) { - if (openSeq !== state.openSeq || !["credentialsRequired", "invalidCredentials"].includes(err.code)) throw err; + if (openSeq !== state.openSeq || !(pdf ? err.code === "pdfPassword" : ["credentialsRequired", "invalidCredentials"].includes(err.code))) throw err; + const code = pdf ? credentials ? "invalidCredentials" : "credentialsRequired" : err.code; credentials?.password.fill(0); credentials?.key?.fill(0); credentials?.keyPassword?.fill(0); - credentials = await requestCredentials(err.code, openSeq); + credentials = await requestCredentials(code, openSeq, pdf); if (!credentials || openSeq !== state.openSeq) { const canceled = new Error("打开已取消"); canceled.name = "AbortError"; @@ -4283,6 +4293,7 @@ async function openSealPosition() { const file = el.signSeal.files?.[0]; if (!file || !state.doc || document.body.hasAttribute("aria-busy")) return; const sequence = state.openSeq; + let preview; setBusy(true); try { const range = el.signPages.value.trim(), mode = el.signPlacement.value; @@ -4291,8 +4302,10 @@ async function openSealPosition() { const result = await callWASM("ofdgoPreviewSignatureSeal", new Uint8Array(await file.arrayBuffer()), `1-${state.doc.pageCount}`); if (sequence !== state.openSeq || !el.signPanel.open || file !== el.signSeal.files?.[0]) return; sealPreview = {pages:result.pages, selected, range, mode, ratio:document.querySelector("#signKeepRatio").checked ? result.width / result.height : 0, sequence, enabled:canvasEditor.enabled, status:el.statusText.textContent, url:URL.createObjectURL(new Blob([result.svg], {type:"image/svg+xml"}))}; + preview = sealPreview; sealPosition.Image.src = sealPreview.url; await sealPosition.Image.decode(); + if (sealPreview !== preview) return; if (sequence !== state.openSeq || !el.signPanel.open) { finishSealPosition(false); return; } canvasEditor.enabled = false; if (COMPACT_LAYOUT.matches) { @@ -4305,7 +4318,12 @@ async function openSealPosition() { el.signStatus.textContent = ""; setStatus("拖动选取签章区域,Esc 取消"); el.viewerPanel.focus({preventScroll:true}); - } catch (error) { finishSealPosition(); el.signStatus.textContent = error.message; } + } catch (error) { + if (sequence === state.openSeq && (!preview || sealPreview === preview)) { + finishSealPosition(); + el.signStatus.textContent = error.message; + } + } finally { if (sequence === state.openSeq) setBusy(false); } } diff --git a/assets/webui/ofdgo_wasm.js b/assets/webui/ofdgo_wasm.js index 677d46d..dd06975 100644 --- a/assets/webui/ofdgo_wasm.js +++ b/assets/webui/ofdgo_wasm.js @@ -168,7 +168,7 @@ async function handleMessage({ id, name, args }) { const saving = name === "ofdgoSaveDocument" || name === "ofdgoSaveEncrypted" || name === "ofdgoSaveSigned"; const key = name === "ofdgoOpen" ? args[3]?.key : name === "ofdgoSaveSigned" || name === "ofdgoLoadImport" ? args[1]?.key : null; const keyPassword = name === "ofdgoOpen" ? args[3]?.keyPassword : name === "ofdgoSaveSigned" || name === "ofdgoLoadImport" ? args[1]?.keyPassword : null; - const password = name === "ofdgoOpen" ? args[3]?.password : name === "ofdgoSaveEncrypted" || name === "ofdgoLoadImport" ? args[1]?.password : null; + const password = converting ? args[2] : name === "ofdgoOpen" ? args[3]?.password : name === "ofdgoSaveEncrypted" || name === "ofdgoLoadImport" ? args[1]?.password : null; try { signal?.throwIfAborted(); const chunks = []; diff --git a/go.mod b/go.mod index 757e2f8..e48a4ea 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/tdewolff/canvas v0.0.0-20260923214215-09804640d00c github.com/tdewolff/font v0.0.0-20260913163313-54f98bb59ee6 github.com/xiaoqidun/jbig2 v1.1.1 - github.com/xiaoqidun/pdfgo v0.0.0-20260927150509-ae8390858589 + github.com/xiaoqidun/pdfgo v0.0.0-20260927160712-be47f27aa7ac golang.org/x/crypto v0.57.0 golang.org/x/image v0.46.0 golang.org/x/text v0.42.0 diff --git a/go.sum b/go.sum index e90118e..4e4d33d 100644 --- a/go.sum +++ b/go.sum @@ -62,8 +62,8 @@ github.com/tdewolff/test v1.0.12 h1:7F21DqIajswxuche0geHdrUZRCWE4oko4b7bcmkkrxk= github.com/tdewolff/test v1.0.12/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8= github.com/xiaoqidun/jbig2 v1.1.1 h1:SjliS9ZO0e9HX5oYUfBuXrfFsXhWricrSz+XY4zORDI= github.com/xiaoqidun/jbig2 v1.1.1/go.mod h1:8QonVWtENmh2kc6l+leqeGl7psb79DWlHDkCpdChjfQ= -github.com/xiaoqidun/pdfgo v0.0.0-20260927150509-ae8390858589 h1:dHH5wnrIyOKRpx3Dw4IiMgMEPMrNFdatfkoMKpX3Khk= -github.com/xiaoqidun/pdfgo v0.0.0-20260927150509-ae8390858589/go.mod h1:4Pf0JO0BEf1uzRY+uZjlMjN/OPn3PYa0YMi6cqPPTLQ= +github.com/xiaoqidun/pdfgo v0.0.0-20260927160712-be47f27aa7ac h1:RLwc4JpZm5PU9vBrMffyvdubmy7u3hknjsAId9gMvB4= +github.com/xiaoqidun/pdfgo v0.0.0-20260927160712-be47f27aa7ac/go.mod h1:4Pf0JO0BEf1uzRY+uZjlMjN/OPn3PYa0YMi6cqPPTLQ= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA= diff --git a/internal/webui/wasm.go b/internal/webui/wasm.go index 77eae3a..26f0edb 100644 --- a/internal/webui/wasm.go +++ b/internal/webui/wasm.go @@ -332,6 +332,8 @@ func callbackResult(fn func([]js.Value) (any, error), args []js.Value) any { if err != nil { result := apiResult{Error: err.Error()} switch { + case errors.Is(err, ofdgo.ErrPDFPassword): + result.Code, result.Error = "pdfPassword", "PDF密码缺失或错误" case errors.Is(err, ofdgo.ErrCredentialsRequired): result.Code, result.Error = "credentialsRequired", "文档已加密" case errors.Is(err, ofdgo.ErrInvalidCredentials): @@ -369,10 +371,10 @@ func safeCall(fn func([]js.Value) (any, error), args []js.Value) (data any, err } // convertPDFDocument 通过库转换PDF,不替换当前文档会话 -// 入参: args PDF数据、外部字体及进度回调 +// 入参: args PDF数据、外部字体、UTF-8密码及进度回调 // 返回: any OFD数据及转换警告, error 错误信息 func convertPDFDocument(args []js.Value) (any, error) { - if len(args) != 3 { + if len(args) != 4 { return nil, fmt.Errorf("missing PDF data") } data, err := bytesFromJS(args[0]) @@ -384,7 +386,14 @@ func convertPDFDocument(args []js.Value) (any, error) { return nil, err } var output bytes.Buffer - options := ofdgo.PDFImportOptions{OnProgress: editorOperationProgress(args[2])} + options := ofdgo.PDFImportOptions{OnProgress: editorOperationProgress(args[3]), PasswordUTF8: true} + if !args[2].IsNull() && !args[2].IsUndefined() { + options.Password, err = bytesFromJS(args[2]) + if err != nil { + return nil, err + } + defer clear(options.Password) + } if len(fonts) > 0 { options.RendererOptions = []ofdgo.RendererOption{ofdgo.WithFontFS(ofdgo.NewFontFS(fonts))} } diff --git a/ofdgo_font_cff.go b/ofdgo_font_cff.go index 709b1ef..c9cbc29 100644 --- a/ofdgo_font_cff.go +++ b/ofdgo_font_cff.go @@ -397,8 +397,18 @@ func sanitizeCFF(data []byte) ([]byte, error) { var finalPrivData []byte if len(privDictData) > 0 { pDict := parseCFFDict(privDictData) + if err := normalizeCFFPrivate(pDict); err != nil { + return nil, err + } if _, ok := pDict[19]; ok || len(localSubrData) > 0 { - pDict[19] = []float64{float64(privateLen)} + pDict[19] = []float64{0} + for { + length := len(encodeCFFDict(pDict)) + if pDict[19][0] == float64(length) { + break + } + pDict[19][0] = float64(length) + } } finalPrivData = encodeCFFDict(pDict) privateLen = len(finalPrivData) @@ -438,6 +448,21 @@ func sanitizeCFF(data []byte) ([]byte, error) { return newCFF.Bytes(), nil } +// normalizeCFFPrivate 移除旧版CFF中不生效的缺省字段,拒绝需要额外解释的取值 +// 入参: dict 私有字典 +// 返回: error 不支持的旧版行为 +func normalizeCFFPrivate(dict cffDict) error { + for op, value := range map[int]float64{1215: 0, 1216: -1} { + if values, ok := dict[op]; ok { + if len(values) != 1 || values[0] != value { + return fmt.Errorf("unsupported legacy CFF private operator %d", op) + } + delete(dict, op) + } + } + return nil +} + // sanitizeMultiFDCFF 清洗多FD的CID CFF数据 // 入参: data 原始CFF数据, hdrSize 头部大小, nameIndexData 名称索引, topDict 顶层字典 // 入参: stringIndexData 字符串索引, globalSubrIndexData 全局子程序索引, fdArrOff FDArray偏移, fdCount FD数量 @@ -479,6 +504,9 @@ func sanitizeMultiFDCFF(data []byte, hdrSize int, nameIndexData []byte, topDict } } delete(privateDict, 19) + if err := normalizeCFFPrivate(privateDict); err != nil { + return nil, err + } finalPrivData := encodeCFFDict(privateDict) privateLen := len(finalPrivData) charStringsData := encodeCFFIndex(inlined) diff --git a/ofdgo_pdf.go b/ofdgo_pdf.go index 151c26d..fcead51 100644 --- a/ofdgo_pdf.go +++ b/ofdgo_pdf.go @@ -26,14 +26,18 @@ import ( "github.com/xiaoqidun/pdfgo" ) +// ErrPDFPassword 表示PDF需要正确的用户或所有者密码 +var ErrPDFPassword = pdfgo.ErrPassword + // PDFImportOptions 指定PDF转换的密码、运行时后端、进度通知与检查策略 -// Password按PDFDocEncoding字节传入,空值尝试默认空密码 +// Password默认使用PDF安全处理器编码,PasswordUTF8启用UTF-8输入及标准密码准备,空值尝试空密码 // Strict禁止恢复缺失资源或丢失内容语义,默认在报告中记录警告 // RendererOptions复用渲染器字体来源和后端配置,Backends优先于其中的后端设置 // RasterDPI指定局部透明效果合成精度,0沿用渲染器DPI,默认300dpi,Strict禁用局部合成 // OnProgress按open、pages、convert及write.*阶段报告进度,total为0表示总量未知 type PDFImportOptions struct { Password []byte + PasswordUTF8 bool Backends *RenderBackends RendererOptions []RendererOption Progress func(int) error @@ -108,7 +112,11 @@ func ImportPDF(ctx context.Context, source io.ReaderAt, size int64, options PDFI return nil, PDFImportReport{}, err } } - reader, err := pdfgo.NewReaderWithPassword(source, size, options.Password) + open := pdfgo.NewReaderWithPassword + if options.PasswordUTF8 { + open = pdfgo.NewReaderWithUTF8Password + } + reader, err := open(source, size, options.Password) if err != nil { return nil, PDFImportReport{}, err }