feat(编辑能力): 修复重复编号及加密文档编辑异常,统一签名信息顺序,优化弹窗标题与布局
build.yaml / build (push) Successful in 42s

This commit is contained in:
xiaoqidun committed 2026-09-23 23:39:34 +08:00
1 parent 3927929822
commit e3908c1149
8 files changed
+317 -145

No files matched your search

+8 -4
View File
@@ -353,8 +353,9 @@
</div>
</form>
</dialog>
<dialog id="signPanel" class="form-dialog security-dialog" aria-label="签章">
<dialog id="signPanel" class="form-dialog security-dialog" aria-labelledby="signTitle">
<form id="signForm" autocomplete="off">
<h2 id="signTitle" class="form-title">签署文档</h2>
<div class="form-row"><label for="signCertificate">证书</label><input id="signCertificate" type="file" accept=".pem,.cer,.crt,.der" required></div>
<div class="form-row"><label for="signKey">私钥</label><input id="signKey" type="file" accept=".pem,.key,.der" required></div>
<div class="form-row"><label for="signKeyPassword">私钥口令</label><input id="signKeyPassword" type="password" autocomplete="off"></div>
@@ -374,8 +375,9 @@
<div class="form-actions"><div id="signStatus" class="form-status" role="status" aria-live="polite"></div><button id="signCancel" class="button" type="button">取消</button><button class="button" type="submit" title="签署另存">签署</button></div>
</form>
</dialog>
<dialog id="verifyPanel" class="form-dialog security-dialog" aria-label="验签">
<dialog id="verifyPanel" class="form-dialog security-dialog" aria-labelledby="verifyTitle">
<form id="verifyForm">
<h2 id="verifyTitle" class="form-title">验证签名</h2>
<div class="form-row"><label for="verifyRoots">信任根</label><input id="verifyRoots" type="file" accept=".pem,.cer,.crt,.der" multiple></div>
<div class="form-row"><label for="verifyCerts">证书</label><input id="verifyCerts" type="file" accept=".pem,.cer,.crt,.der" multiple></div>
<div class="form-row"><label for="verifyTimeRoots">时间戳根</label><input id="verifyTimeRoots" type="file" accept=".pem,.cer,.crt,.der" multiple></div>
@@ -386,8 +388,9 @@
<div class="form-actions"><div id="verifyStatus" class="form-status" role="status" aria-live="polite"></div><button id="verifyCancel" class="button" type="button">取消</button><button class="button" type="submit">验签</button></div>
</form>
</dialog>
<dialog id="credentialsPanel" class="form-dialog security-dialog" aria-label="解锁文档">
<dialog id="credentialsPanel" class="form-dialog security-dialog" aria-labelledby="credentialsTitle">
<form id="credentialsForm" autocomplete="off">
<h2 id="credentialsTitle" class="form-title">解锁文档</h2>
<div class="form-row"><label for="credentialsType">方式</label><select id="credentialsType"><option value="password">口令</option><option value="certificate">证书</option></select></div>
<div class="form-row"><label for="credentialsUser">用户</label><input id="credentialsUser" type="text" autocomplete="off" spellcheck="false"></div>
<div id="credentialsPasswordRow" class="form-row"><label for="credentialsPassword">口令</label><input id="credentialsPassword" type="password" autocomplete="off" required autofocus></div>
@@ -397,8 +400,9 @@
<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="exportPanel" class="form-dialog" aria-label="导出文档">
<dialog id="exportPanel" class="form-dialog" aria-labelledby="exportTitle">
<form id="exportForm">
<h2 id="exportTitle" class="form-title">导出文档</h2>
<div id="encryptionFields" hidden>
<div class="form-row"><label for="encryptionType">方式</label><select id="encryptionType"><option value="password">口令</option><option value="certificate">证书</option></select></div>
<div id="encryptionUserRow" class="form-row"><label for="encryptionUser">用户</label><input id="encryptionUser" type="text" autocomplete="off" spellcheck="false"></div>
+10
View File
@@ -645,6 +645,16 @@ button:not(:disabled):hover {
padding: 12px;
}
.form-title {
margin: -12px -12px 12px;
padding: 10px 12px;
border-bottom: 1px solid var(--line);
background: #eef1f4;
font-size: 14px;
font-weight: 600;
line-height: 1.5;
}
.security-dialog {
width: 360px;
}
+21 -20
View File
@@ -350,6 +350,7 @@ const el = {
exportPageButton: document.querySelector("#exportPageButton"),
exportButton: document.querySelector("#exportButton"),
exportPanel: document.querySelector("#exportPanel"),
exportTitle: document.querySelector("#exportTitle"),
exportForm: document.querySelector("#exportForm"),
exportAll: document.querySelector("#exportAll"),
exportSpecified: document.querySelector("#exportSpecified"),
@@ -1602,7 +1603,7 @@ async function toggleEditor() {
const openSeq = ++state.openSeq;
setBusy(true, "正在准备编辑", null, "正在准备编辑");
try {
const doc = await callWASM("ofdgoEditDocument");
const doc = await callWASM("ofdgoEditDocument", pageIndex);
if (openSeq !== state.openSeq) return;
state.savedRevision = doc.revision;
state.editorViews.clear();
@@ -3961,7 +3962,7 @@ function openExportPanel(encrypted = false) {
el.encryptionFields.hidden = !encrypted;
el.encryptionPassword.required = el.encryptionConfirm.required = encrypted;
updateEncryptionType();
el.exportPanel.setAttribute("aria-label", encrypted ? "加密另存" : "导出文档");
el.exportTitle.textContent = encrypted ? "加密另存" : "导出文档";
el.exportSubmit.textContent = encrypted ? "保存" : "导出";
if (state.editing && state.pageSelection.size) {
el.exportSpecified.checked = true;
@@ -5851,9 +5852,9 @@ function renderSignatures() {
head.append(badges, name);
row.append(head);
appendInfoLine(row, "章名", signature.sealName);
appendInfoLine(row, "签署人", signature.signer);
appendInfoLine(row, "时间", formatDocumentTime(signature.signatureDateTime));
appendInfoLine(row, "章名", signature.sealName);
appendInfoLine(row, "机构", signatureAgency(signature));
appendSignatureCheck(row, "信任", signature.certTrustChecked, signature.certTrustOK);
appendInfoLine(row, "保护文件", signatureReferenceText(signature), signatureReferenceStatus(signature));
@@ -5864,25 +5865,25 @@ function renderSignatures() {
checks.append(checksTitle);
appendSignatureCheck(checks, "原文", signature.dataHashChecked, signature.dataHashOK);
appendSignatureCheck(checks, "签名", signature.signedValueChecked, signature.signedValueOK);
appendSignatureCheck(checks, "证书", signature.certChecked, signature.certOK);
appendSignaturePolicy(checks, "证书期限", signature.certTimeChecked, signature.certTimeOK);
appendInfoLine(checks, "信任错误", signature.certTrustError, "fail");
appendSignaturePolicy(checks, "签署时间", signature.signatureTimeChecked, signature.signatureTimeOK);
if (signature.type !== "Sign") {
appendSignatureCheck(checks, "印章", signature.sealChecked, signature.sealOK);
appendSignaturePolicy(checks, "印章匹配", signature.sealMatchChecked, signature.sealMatchOK);
appendSignaturePolicy(checks, "印章期限", signature.sealTimeChecked, signature.sealTimeOK);
if (signature.sealCertTimeChecked) {
appendInfoLine(checks, "制章证书", signature.sealCertTimeOK ? "有效" : "失效", signature.sealCertTimeOK ? "ok" : "fail");
}
}
appendSignatureCheck(checks, "证书", signature.certChecked, signature.certOK);
appendSignaturePolicy(checks, "签署时间", signature.signatureTimeChecked, signature.signatureTimeOK);
appendSignaturePolicy(checks, "印章期限", signature.sealTimeChecked, signature.sealTimeOK);
if (signature.sealCertTimeChecked) {
appendInfoLine(checks, "制章证书", signature.sealCertTimeOK ? "有效" : "失效", signature.sealCertTimeOK ? "ok" : "fail");
}
appendSignaturePolicy(checks, "证书期限", signature.certTimeChecked, signature.certTimeOK);
appendSignatureCheck(checks, "策略", signature.policyChecked, signature.policyOK);
appendSignatureCheck(checks, "覆盖", signature.coverageChecked, signature.coverageOK);
appendSignatureCheck(checks, "时间戳", signature.timestampChecked, signature.timestampOK);
appendSignatureCheck(checks, "撤销", signature.revocationChecked, signature.revocationOK);
appendInfoLine(checks, "策略错误", signature.policyError, "fail");
appendInfoLine(checks, "信任错误", signature.certTrustError, "fail");
appendInfoLine(checks, "覆盖错误", signature.coverageError, "fail");
appendInfoLine(checks, "未保护", signature.uncoveredFiles?.join("、"), "fail");
appendSignatureCheck(checks, "策略", signature.policyChecked, signature.policyOK);
appendInfoLine(checks, "策略错误", signature.policyError, "fail");
appendSignatureCheck(checks, "时间戳", signature.timestampChecked, signature.timestampOK);
appendSignatureCheck(checks, "撤销", signature.revocationChecked, signature.revocationOK);
row.append(checks);
for (const timestamp of signature.timestamps || []) {
const details = document.createElement("details");
@@ -5917,17 +5918,17 @@ function renderSignatures() {
title.textContent = "签章信息";
details.append(title);
appendInfoLine(details, "编号", signature.id);
appendInfoLine(details, "版本", signature.version);
appendInfoLine(details, "章图", signature.sealType);
appendInfoLine(details, "章号", signature.sealId);
appendInfoLine(details, "厂商", signature.sealVendor);
appendInfoLine(details, "文档", signature.docRoot);
appendInfoLine(details, "算法", signature.signatureMethod);
appendInfoLine(details, "摘要", signature.digestMethod);
appendInfoLine(details, "序列号", signature.signSerial);
appendInfoLine(details, "证书主体", signature.signSubject);
appendInfoLine(details, "颁发者", signature.signIssuer);
appendInfoLine(details, "序列号", signature.signSerial);
appendInfoLine(details, "章号", signature.sealId);
appendInfoLine(details, "章图", signature.sealType);
appendInfoLine(details, "制章证书", signature.sealSubject);
appendInfoLine(details, "印章厂商", signature.sealVendor);
appendInfoLine(details, "组件版本", signature.version);
row.append(details);
fragment.append(row);
}
+45 -2
View File
@@ -498,6 +498,15 @@ func renderPage(args []js.Value) (any, error) {
dpi = args[2].Float()
}
raster := len(args) > 3 && args[3].Bool()
if currentEditor != nil {
page, err := currentEditor.Page(args[0].Int())
if err != nil {
return nil, err
}
if err := syncEditorPageIDs(args[0].Int(), page); err != nil {
return nil, err
}
}
page, err := currentSession.RenderPage(args[0].Int(), backend, dpi, raster)
if err != nil {
return nil, err
@@ -569,7 +578,7 @@ func renderPage(args []js.Value) (any, error) {
}
// pageTextString 提取OFD页面原文
// 入参: args 浏览器参数
// 入参: args 页面索引
// 返回: any 页面原文, error 错误信息
func pageTextString(args []js.Value) (any, error) {
if currentSession == nil {
@@ -2906,7 +2915,7 @@ func updateInfo(args []js.Value) (any, error) {
}
// editDocument 将已打开文档接入编辑器,沿用页面、资源及字体配置
// 入参: args 浏览器参数
// 入参: args 当前页索引,省略时使用首页
// 返回: any 编辑状态, error 错误信息
func editDocument(args []js.Value) (any, error) {
if currentSession == nil {
@@ -2924,6 +2933,17 @@ func editDocument(args []js.Value) (any, error) {
editor.SetFontFS(currentSession.fontFS)
}
editor.SetRenderBackends(currentSession.Renderer.Backends())
page := 0
if len(args) != 0 {
page = args[0].Int()
}
content, err := editor.Page(page)
if err != nil {
return nil, err
}
if err := syncEditorPageIDs(page, content); err != nil {
return nil, err
}
currentEditor = editor
currentSession.editing = true
copiedObjects = nil
@@ -2931,6 +2951,29 @@ func editDocument(args []js.Value) (any, error) {
return editorSummary(), nil
}
// syncEditorPageIDs 同步编辑副本的对象编号,不改变原渲染属性或丢弃正常页面缓存
// 入参: index 页面索引, content 编辑页面
// 返回: error 页面读取错误
func syncEditorPageIDs(index int, content *ofdgo.PageContent) error {
page, err := currentSession.pageContent(index)
if err != nil {
return err
}
for i, layer := range content.Content.Layer {
for j, object := range layer.Objects {
before := &page.Content.Layer[i].Objects[j]
if before.TextObject.ID != object.TextObject.ID || before.PathObject.ID != object.PathObject.ID || before.ImageObject.ID != object.ImageObject.ID || before.CompositeGraphicUnit.ID != object.CompositeGraphicUnit.ID {
before.TextObject.ID = object.TextObject.ID
before.PathObject.ID = object.PathObject.ID
before.ImageObject.ID = object.ImageObject.ID
before.CompositeGraphicUnit.ID = object.CompositeGraphicUnit.ID
delete(currentSession.textCache, index)
}
}
}
return nil
}
// previewEditor 通过内存快照更新预览,不生成中间压缩包
// 入参: editor 编辑文档, annotations 是否显示注解
// 返回: editorInfo 文档信息, error 错误信息
+86 -86
View File
@@ -43,65 +43,65 @@ type FontInfo = ofdgo.FontInfo
type SignatureInfo struct {
DocIndex int `json:"docIndex"`
DocRoot string `json:"docRoot,omitempty"`
ID string `json:"id"`
Type string `json:"type"`
Status string `json:"status"`
IntegrityValid bool `json:"integrityValid"`
TrustedValid bool `json:"trustedValid"`
PolicyChecked bool `json:"policyChecked"`
PolicyOK bool `json:"policyOK"`
PolicyError string `json:"policyError,omitempty"`
Error string `json:"error,omitempty"`
Signer string `json:"signer,omitempty"`
SignatureDateTime string `json:"signatureDateTime,omitempty"`
SealName string `json:"sealName,omitempty"`
Provider string `json:"provider,omitempty"`
Company string `json:"company,omitempty"`
SealType string `json:"sealType,omitempty"`
SealID string `json:"sealId,omitempty"`
SealVendor string `json:"sealVendor,omitempty"`
Version string `json:"version,omitempty"`
SignSubject string `json:"signSubject,omitempty"`
SignIssuer string `json:"signIssuer,omitempty"`
SignSerial string `json:"signSerial,omitempty"`
SealSubject string `json:"sealSubject,omitempty"`
SignatureMethod string `json:"signatureMethod,omitempty"`
DigestMethod string `json:"digestMethod,omitempty"`
ReferenceCount int `json:"referenceCount"`
ReferenceChecked int `json:"referenceChecked"`
ReferencePassed int `json:"referencePassed"`
Stamps []SignatureStampInfo `json:"stamps,omitempty"`
DataHashChecked bool `json:"dataHashChecked"`
DataHashOK bool `json:"dataHashOK"`
SignedValueChecked bool `json:"signedValueChecked"`
SignedValueOK bool `json:"signedValueOK"`
CertChecked bool `json:"certChecked"`
CertOK bool `json:"certOK"`
CertTimeChecked bool `json:"certTimeChecked,omitempty"`
CertTimeOK bool `json:"certTimeOK,omitempty"`
CertTrustChecked bool `json:"certTrustChecked,omitempty"`
CertTrustOK bool `json:"certTrustOK,omitempty"`
CertTrustError string `json:"certTrustError,omitempty"`
SignatureTimeChecked bool `json:"signatureTimeChecked,omitempty"`
SignatureTimeOK bool `json:"signatureTimeOK,omitempty"`
SealChecked bool `json:"sealChecked"`
SealOK bool `json:"sealOK"`
SealMatchChecked bool `json:"sealMatchChecked"`
SealMatchOK bool `json:"sealMatchOK"`
SealTimeChecked bool `json:"sealTimeChecked,omitempty"`
SealTimeOK bool `json:"sealTimeOK,omitempty"`
SealCertTimeChecked bool `json:"sealCertTimeChecked,omitempty"`
SealCertTimeOK bool `json:"sealCertTimeOK,omitempty"`
CoverageChecked bool `json:"coverageChecked"`
CoverageOK bool `json:"coverageOK"`
CoverageError string `json:"coverageError,omitempty"`
UncoveredFiles []string `json:"uncoveredFiles,omitempty"`
PolicyChecked bool `json:"policyChecked"`
PolicyOK bool `json:"policyOK"`
PolicyError string `json:"policyError,omitempty"`
TimestampChecked bool `json:"timestampChecked"`
TimestampOK bool `json:"timestampOK"`
Timestamps []TimestampInfo `json:"timestamps,omitempty"`
RevocationChecked bool `json:"revocationChecked"`
RevocationOK bool `json:"revocationOK"`
Revocations []RevocationInfo `json:"revocations,omitempty"`
ID string `json:"id"`
Type string `json:"type"`
Status string `json:"status"`
Version string `json:"version,omitempty"`
SealType string `json:"sealType,omitempty"`
SealID string `json:"sealId,omitempty"`
SealName string `json:"sealName,omitempty"`
SealVendor string `json:"sealVendor,omitempty"`
Signer string `json:"signer,omitempty"`
SignatureDateTime string `json:"signatureDateTime,omitempty"`
Provider string `json:"provider,omitempty"`
Company string `json:"company,omitempty"`
DataHashChecked bool `json:"dataHashChecked"`
DataHashOK bool `json:"dataHashOK"`
SignedValueChecked bool `json:"signedValueChecked"`
SignedValueOK bool `json:"signedValueOK"`
SealChecked bool `json:"sealChecked"`
SealOK bool `json:"sealOK"`
SealMatchChecked bool `json:"sealMatchChecked"`
SealMatchOK bool `json:"sealMatchOK"`
CertChecked bool `json:"certChecked"`
CertOK bool `json:"certOK"`
SignatureTimeChecked bool `json:"signatureTimeChecked,omitempty"`
SignatureTimeOK bool `json:"signatureTimeOK,omitempty"`
SealTimeChecked bool `json:"sealTimeChecked,omitempty"`
SealTimeOK bool `json:"sealTimeOK,omitempty"`
SealCertTimeChecked bool `json:"sealCertTimeChecked,omitempty"`
SealCertTimeOK bool `json:"sealCertTimeOK,omitempty"`
CertTimeChecked bool `json:"certTimeChecked,omitempty"`
CertTimeOK bool `json:"certTimeOK,omitempty"`
CertTrustChecked bool `json:"certTrustChecked,omitempty"`
CertTrustOK bool `json:"certTrustOK,omitempty"`
CertTrustError string `json:"certTrustError,omitempty"`
ReferenceCount int `json:"referenceCount"`
ReferenceChecked int `json:"referenceChecked"`
ReferencePassed int `json:"referencePassed"`
SignatureMethod string `json:"signatureMethod,omitempty"`
DigestMethod string `json:"digestMethod,omitempty"`
SignSerial string `json:"signSerial,omitempty"`
SignSubject string `json:"signSubject,omitempty"`
SignIssuer string `json:"signIssuer,omitempty"`
SealSubject string `json:"sealSubject,omitempty"`
Stamps []SignatureStampInfo `json:"stamps,omitempty"`
Error string `json:"error,omitempty"`
}
// TimestampInfo 独立展示时间戳绑定、签名、信任和证书时效
@@ -681,63 +681,63 @@ func signatureInfo(report ofdgo.SignatureVerifyReport) SignatureInfo {
info := SignatureInfo{
DocIndex: report.DocIndex,
DocRoot: report.DocRoot,
IntegrityValid: report.IntegrityValid(),
TrustedValid: report.TrustedValid(),
PolicyChecked: report.PolicyChecked,
PolicyOK: report.PolicyOK,
PolicyError: report.PolicyError,
CoverageChecked: report.CoverageChecked,
CoverageOK: report.CoverageOK,
CoverageError: report.CoverageError,
UncoveredFiles: report.UncoveredFiles,
TimestampChecked: report.TimestampChecked,
TimestampOK: report.TimestampOK,
RevocationChecked: report.RevocationChecked,
RevocationOK: report.RevocationOK,
ID: report.ID,
Type: string(report.Type),
Status: status,
Version: report.Provider.Version,
SealType: report.SealType,
SealID: report.SealInfo.ID,
SealName: report.SealInfo.Name,
SealVendor: report.SealInfo.VendorID,
IntegrityValid: report.IntegrityValid(),
TrustedValid: report.TrustedValid(),
Error: signatureReportError(report),
Signer: signatureSigner(report),
SignatureDateTime: dateTime,
SealName: report.SealInfo.Name,
Provider: report.Provider.ProviderName,
Company: report.Provider.Company,
SealType: report.SealType,
SealID: report.SealInfo.ID,
SealVendor: report.SealInfo.VendorID,
Version: report.Provider.Version,
SignSubject: report.SignCert.Subject,
SignIssuer: report.SignCert.Issuer,
SignSerial: report.SignCert.SerialNumber,
SealSubject: report.SealCert.Subject,
SignatureMethod: report.SignatureMethod,
DigestMethod: report.DigestMethod,
ReferenceCount: len(report.References),
ReferenceChecked: checked,
ReferencePassed: passed,
Stamps: signatureStampInfos(report.StampPositions),
DataHashChecked: report.DataHashChecked,
DataHashOK: report.DataHashOK,
SignedValueChecked: report.SignedValueChecked,
SignedValueOK: report.SignedValueOK,
SealChecked: report.SealChecked,
SealOK: report.SealOK,
SealMatchChecked: report.SealMatchChecked,
SealMatchOK: report.SealMatchOK,
CertChecked: report.CertChecked,
CertOK: report.CertOK,
SignatureTimeChecked: report.SignatureTimeChecked,
SignatureTimeOK: report.SignatureTimeOK,
SealTimeChecked: report.SealTimeChecked,
SealTimeOK: report.SealTimeOK,
SealCertTimeChecked: report.SealCertTimeChecked,
SealCertTimeOK: report.SealCertTimeOK,
CertTimeChecked: report.CertTimeChecked,
CertTimeOK: report.CertTimeOK,
CertTrustChecked: report.CertTrustChecked,
CertTrustOK: report.CertTrustOK,
CertTrustError: report.CertTrustError,
ReferenceCount: len(report.References),
ReferenceChecked: checked,
ReferencePassed: passed,
SignatureMethod: report.SignatureMethod,
DigestMethod: report.DigestMethod,
SignSerial: report.SignCert.SerialNumber,
SignSubject: report.SignCert.Subject,
SignIssuer: report.SignCert.Issuer,
SealSubject: report.SealCert.Subject,
Stamps: signatureStampInfos(report.StampPositions),
Error: signatureReportError(report),
SignatureTimeChecked: report.SignatureTimeChecked,
SignatureTimeOK: report.SignatureTimeOK,
SealChecked: report.SealChecked,
SealOK: report.SealOK,
SealMatchChecked: report.SealMatchChecked,
SealMatchOK: report.SealMatchOK,
SealTimeChecked: report.SealTimeChecked,
SealTimeOK: report.SealTimeOK,
SealCertTimeChecked: report.SealCertTimeChecked,
SealCertTimeOK: report.SealCertTimeOK,
CoverageChecked: report.CoverageChecked,
CoverageOK: report.CoverageOK,
CoverageError: report.CoverageError,
UncoveredFiles: report.UncoveredFiles,
PolicyChecked: report.PolicyChecked,
PolicyOK: report.PolicyOK,
PolicyError: report.PolicyError,
TimestampChecked: report.TimestampChecked,
TimestampOK: report.TimestampOK,
RevocationChecked: report.RevocationChecked,
RevocationOK: report.RevocationOK,
}
for _, stamp := range report.Timestamps {
info.Timestamps = append(info.Timestamps, TimestampInfo{
+114
View File
@@ -57,6 +57,7 @@ type editorSourcePage struct {
root *editorXML
original *PageContent
nodes map[string]*editorXML
repaired bool
}
// ObjectCapabilities 已有对象可执行的操作,Reason说明受限原因
@@ -260,6 +261,103 @@ func (e *Editor) sourceRGB() bool {
return space != nil && space.Type == "RGB" && (space.BitsPerComponent == 0 || space.BitsPerComponent == 8) && len(space.Palette) == 0
}
// repairSourceObjectIDs 按实际节点区分重复对象,只修复没有引用歧义的编号
// 入参: data 页面原文, root 页面节点
// 返回: []byte 编辑副本, bool 是否修复, error 错误信息
func (e *Editor) repairSourceObjectIDs(data []byte, root *editorXML) ([]byte, bool, error) {
seen := make(map[string]bool)
duplicates := make(map[string]bool)
var nodes []*editorXML
var visit func(*editorXML) error
visit = func(parent *editorXML) error {
for _, node := range parent.children {
id := node.attr("ID")
if id != "" && seen[id] {
switch node.name.Local {
case "TextObject", "PathObject", "ImageObject", "CompositeObject":
nodes = append(nodes, node)
duplicates[id] = true
default:
return fmt.Errorf("duplicate container ID %q", id)
}
}
seen[id] = true
if node.name.Local == "PageBlock" {
if err := visit(node); err != nil {
return err
}
}
}
return nil
}
if content := root.child("Content"); content != nil {
for _, layer := range content.children {
if layer.name.Local == "Layer" {
if err := visit(layer); err != nil {
return nil, false, err
}
}
}
}
if len(nodes) == 0 {
return data, false, nil
}
if err := e.checkDuplicateObjectReferences(duplicates); err != nil {
return nil, false, err
}
if err := e.prepareSourceIDs(); err != nil {
return nil, false, err
}
maximum := e.maxID
var patches []editorXMLPatch
for _, node := range nodes {
maximum++
encoded, err := editorXMLAttribute(data, node, "ID", strconv.Itoa(maximum))
if err != nil {
return nil, false, err
}
patches = append(patches, editorXMLPatch{node.start, node.end, encoded})
}
e.maxID = maximum
return editorPatchXML(data, patches), true, nil
}
// checkDuplicateObjectReferences 拒绝无法判定目标的编号引用,不猜测其所属对象
// 入参: duplicates 重复编号
// 返回: error 引用歧义或读取错误
func (e *Editor) checkDuplicateObjectReferences(duplicates map[string]bool) error {
reader := e.source.reader
for _, name := range reader.fileNamesFold {
if !strings.EqualFold(path.Ext(name), ".xml") {
continue
}
input, err := reader.openFile(name)
if err != nil {
return err
}
decoder := xml.NewDecoder(input)
for {
token, readErr := decoder.Token()
if readErr != nil {
input.Close()
if readErr != io.EOF {
return readErr
}
break
}
if node, ok := token.(xml.StartElement); ok {
for _, attr := range node.Attr {
if editorObjectReference(attr.Name.Local) && duplicates[editorResourceID(attr.Value)] {
input.Close()
return fmt.Errorf("ambiguous reference to duplicate object ID %q in %s", attr.Value, name)
}
}
}
}
}
return nil
}
// loadSourcePage 首次访问时解析原始页面,不加载图片或字体二进制数据
// 入参: index 页面索引
// 返回: error 错误信息
@@ -281,6 +379,21 @@ func (e *Editor) loadSourcePage(index int) error {
if err != nil {
return err
}
data, repaired, err := e.repairSourceObjectIDs(data, root)
if err != nil {
return err
}
if repaired {
root, err = parseEditorXML(data)
if err != nil {
return err
}
var updated PageContent
if err := xml.Unmarshal(data, &updated); err != nil {
return err
}
page.Content = updated.Content
}
nodes := make(map[string]*editorXML)
if content := root.child("Content"); content != nil {
for _, layer := range content.children {
@@ -293,6 +406,7 @@ func (e *Editor) loadSourcePage(index int) error {
}
}
source.data, source.root, source.nodes = data, root, nodes
source.repaired = repaired
for i := range page.Content.Layer {
layer := &page.Content.Layer[i]
for j, object := range layer.Objects {
+1 -1
View File
@@ -49,7 +49,7 @@ func (e *Editor) sourceParts(progress editorProgress) (map[string][]byte, error)
if err != nil {
return nil, err
}
if !bytes.Equal(data, original.data) {
if original.repaired || !bytes.Equal(data, original.data) {
parts[reader.ResPath(original.ref.BaseLoc)] = data
}
} else {
+32 -32
View File
@@ -46,32 +46,20 @@ import (
type SignatureVerifyReport struct {
DocIndex int
DocRoot string
PolicyChecked bool
PolicyOK bool
PolicyError string
CoverageChecked bool
CoverageOK bool
CoverageError string
CoveredFiles []string
UncoveredFiles []string
TimestampChecked bool
TimestampOK bool
Timestamps []SignatureTimestampReport
RevocationChecked bool
RevocationOK bool
Revocations []SignatureRevocationReport
ID string
BaseLoc string
Type SignType
Provider SignatureProvider
Valid bool
Error string
Signer string
SignCert SignatureCertInfo
SealCert SignatureCertInfo
SealInfo SignatureSealInfo
SealType string
SignatureMethod string
SignatureDateTime string
SignatureTime time.Time
Provider SignatureProvider
SealInfo SignatureSealInfo
SealType string
SignCert SignatureCertInfo
SealCert SignatureCertInfo
SignatureMethod string
DigestMethod string
References []SignatureReferenceVerify
Stamps []SignatureStamp
@@ -82,25 +70,37 @@ type SignatureVerifyReport struct {
DataHashOK bool
SignedValueChecked bool
SignedValueOK bool
SealChecked bool
SealOK bool
SealMatchChecked bool
SealMatchOK bool
CertChecked bool
CertOK bool
SignatureTimeChecked bool
SignatureTimeOK bool
SealCertTimeChecked bool
SealCertTimeOK bool
SealTimeChecked bool
SealTimeOK bool
CertTimeChecked bool
CertTimeOK bool
CertTrustChecked bool
CertTrustOK bool
CertTrustError string
Valid bool
Error string
SignatureTimeChecked bool
SignatureTimeOK bool
SealChecked bool
SealOK bool
SealMatchChecked bool
SealMatchOK bool
SealTimeChecked bool
SealTimeOK bool
SealCertTimeChecked bool
SealCertTimeOK bool
CoverageChecked bool
CoverageOK bool
CoverageError string
CoveredFiles []string
UncoveredFiles []string
PolicyChecked bool
PolicyOK bool
PolicyError string
TimestampChecked bool
TimestampOK bool
Timestamps []SignatureTimestampReport
RevocationChecked bool
RevocationOK bool
Revocations []SignatureRevocationReport
}
// IntegrityValid 判断签名完整性是否有效