diff --git a/ofdgo_doc.go b/ofdgo_doc.go index 4e4a5c2..61c2acd 100644 --- a/ofdgo_doc.go +++ b/ofdgo_doc.go @@ -24,6 +24,7 @@ type Document struct { Outlines Outlines `xml:"Outlines"` Permissions Permissions `xml:"Permissions"` Annotations string `xml:"Annotations"` + Signatures string `xml:"Signatures"` Attachments Attachments `xml:"Attachments"` Extensions Extensions `xml:"Extensions"` } diff --git a/ofdgo_page.go b/ofdgo_page.go index db9aafb..5553d4a 100644 --- a/ofdgo_page.go +++ b/ofdgo_page.go @@ -19,6 +19,7 @@ import "encoding/xml" // PageContent 页面内容 type PageContent struct { XMLName xml.Name `xml:"Page"` + ID string `xml:"-"` Area PageArea `xml:"Area"` Template []Template `xml:"Template"` Content Content `xml:"Content"` diff --git a/ofdgo_reader.go b/ofdgo_reader.go index a58b472..bbb260d 100644 --- a/ofdgo_reader.go +++ b/ofdgo_reader.go @@ -34,6 +34,7 @@ type Reader struct { fontCache map[string]*Font drawParamCache map[string]*DrawParam doc *Document + Stamps map[string][]Stamp } // Close 关闭阅读器 @@ -73,6 +74,19 @@ func (r *Reader) readFile(name string) ([]byte, error) { return nil, fmt.Errorf("file not found: %s", name) } +// openFile 打开压缩包内的文件流 +// 入参: name 文件名 +// 返回: io.ReadCloser 文件流, error 错误信息 +func (r *Reader) openFile(name string) (io.ReadCloser, error) { + name = strings.ReplaceAll(name, "\\", "/") + for _, f := range r.Zip.File { + if f.Name == name { + return f.Open() + } + } + return nil, fmt.Errorf("file not found: %s", name) +} + // readZipFile 读取zip文件内容 // 入参: f zip文件对象 // 返回: []byte 文件内容, error 错误信息 @@ -116,7 +130,8 @@ func (r *Reader) Doc() (*Document, error) { r.loadRes(doc.CommonData.PublicRes) } r.doc = &doc - return &doc, nil + _ = r.parseSignatures(&doc) + return r.doc, nil } // loadRes 加载资源文件 @@ -183,6 +198,7 @@ func (r *Reader) PageContent(page Page) (*PageContent, error) { if err := xml.Unmarshal(data, &content); err != nil { return nil, fmt.Errorf("failed to unmarshal page content: %w", err) } + content.ID = page.ID return &content, nil } diff --git a/ofdgo_renderer.go b/ofdgo_renderer.go index 913f207..fcbd71e 100644 --- a/ofdgo_renderer.go +++ b/ofdgo_renderer.go @@ -81,6 +81,11 @@ func (r *Renderer) RenderPage(page *PageContent) (*canvas.Canvas, error) { r.renderLayer(ctx, layer, pageH, nil, nil, 0) } } + if stamps, ok := r.Reader.Stamps[page.ID]; ok { + for _, s := range stamps { + r.renderStamp(ctx, s, pageH) + } + } return c, nil } @@ -570,6 +575,56 @@ func (r *Renderer) loadFont(fontID string) *canvas.FontFamily { return ff } } + winFontDir := `C:\Windows\Fonts` + matches, _ := filepath.Glob(filepath.Join(winFontDir, "*"+targetName+"*")) + if len(matches) == 0 { + if targetName == "SimSun" { + matches, _ = filepath.Glob(filepath.Join(winFontDir, "simsun.ttc")) + } else if targetName == "KaiTi" { + matches, _ = filepath.Glob(filepath.Join(winFontDir, "simkai.ttf")) + } else if targetName == "SimHei" { + matches, _ = filepath.Glob(filepath.Join(winFontDir, "simhei.ttf")) + } else if targetName == "FangSong" { + matches, _ = filepath.Glob(filepath.Join(winFontDir, "simfang.ttf")) + } + } + for _, m := range matches { + if err := ff.LoadFontFile(m, canvas.FontRegular); err == nil { + r.FontMap[fontID] = ff + return ff + } + } } return defaultFont } + +// renderStamp 渲染印章 +// 入参: ctx 画布上下文, s 印章对象, pageH 页面高度 +func (r *Renderer) renderStamp(ctx *canvas.Context, s Stamp, pageH float64) { + x, y, w, h := s.Box.X, s.Box.Y, s.Box.W, s.Box.H + if len(s.Data) > 0 { + img, _, err := image.Decode(bytes.NewReader(s.Data)) + if err == nil { + ctx.Push() + screenY := pageH - (y + h) + ctx.Translate(x, screenY) + ctx.Scale(w/float64(img.Bounds().Dx()), h/float64(img.Bounds().Dy())) + ctx.DrawImage(0, 0, img, canvas.DPMM(1.0)) + ctx.Pop() + return + } + } + ctx.Push() + screenY := pageH - (y + h) + ctx.SetStrokeColor(canvas.Red) + ctx.SetStrokeWidth(0.5) + ctx.SetFillColor(canvas.Transparent) + ctx.DrawPath(x, screenY, canvas.Rectangle(w, h)) + ctx.SetFillColor(canvas.Red) + fontSize := 3.0 + if r.fontFamily != nil { + font := r.fontFamily.Face(fontSize*2.83465, canvas.Red, canvas.FontRegular, canvas.FontNormal) + ctx.DrawText(x+w/2-font.TextWidth("Signature")/2, screenY+h/2-fontSize/2, canvas.NewTextLine(font, "Signature", canvas.Left)) + } + ctx.Pop() +} diff --git a/ofdgo_sign.go b/ofdgo_sign.go new file mode 100644 index 0000000..7eb02b5 --- /dev/null +++ b/ofdgo_sign.go @@ -0,0 +1,167 @@ +// Copyright 2025 肖其顿 (XIAO QI DUN) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ofdgo + +import ( + "encoding/asn1" + "encoding/xml" + "path" + "strings" +) + +const ( + // SignTypeSeal 签章类型: seal + SignTypeSeal = "seal" + // SignTypeSign 签章类型: sign + SignTypeSign = "sign" +) + +// OFDSignatures 签名列表列表 +type OFDSignatures struct { + XMLName xml.Name `xml:"Signatures"` + MaxSignId string `xml:"MaxSignId"` + List []OFDSignature `xml:"Signature"` +} + +// OFDSignature 签名列表引用 +type OFDSignature struct { + ID string `xml:"ID,attr"` + BaseLoc string `xml:"BaseLoc,attr"` +} + +// SignatureFile 签名文件内容描述 +type SignatureFile struct { + XMLName xml.Name `xml:"Signature"` + SignedValue string `xml:"SignedValue"` + SignedInfo struct { + StampAnnot []struct { + ID string `xml:"ID,attr"` + PageRef string `xml:"PageRef,attr"` + Boundary string `xml:"Boundary,attr"` + } `xml:"StampAnnot"` + } `xml:"SignedInfo"` +} + +// parseSignatures 解析签名文件 +// 入参: doc 文档结构 +// 返回: error 错误信息 +func (r *Reader) parseSignatures(doc *Document) error { + if doc.Signatures == "" { + return nil + } + f, err := r.openFile(doc.Signatures) + if err != nil { + return err + } + defer f.Close() + var signatures OFDSignatures + if err := xml.NewDecoder(f).Decode(&signatures); err != nil { + return err + } + for _, sigRef := range signatures.List { + sigPath := path.Join(path.Dir(doc.Signatures), sigRef.BaseLoc) + sf, err := r.openFile(sigPath) + if err != nil { + continue + } + var sigFile SignatureFile + if err := xml.NewDecoder(sf).Decode(&sigFile); err != nil { + sf.Close() + continue + } + sf.Close() + signedValuePath := path.Join(path.Dir(sigPath), sigFile.SignedValue) + svData, err := r.ResData(signedValuePath) + if err != nil { + continue + } + sealType, sealData := extractSeal(svData) + for _, annot := range sigFile.SignedInfo.StampAnnot { + pageID := annot.PageRef + bbox, _ := ParseBox(annot.Boundary) + r.addStamp(pageID, bbox, sealType, sealData) + } + } + return nil +} + +// extractSeal 尝试提取印章数据 +// 入参: data 签名值数据 +// 返回: string 印章类型, []byte 印章数据 +func extractSeal(data []byte) (string, []byte) { + var raw asn1.RawValue + _, err := asn1.Unmarshal(data, &raw) + if err != nil { + return "", nil + } + var foundType string + var foundData []byte + var search func(node asn1.RawValue) bool + search = func(node asn1.RawValue) bool { + if node.IsCompound { + var elements []asn1.RawValue + rest, err := asn1.Unmarshal(node.Bytes, &elements) + if err == nil && len(rest) == 0 { + if len(elements) == 4 { + e0, e1, e2, e3 := elements[0], elements[1], elements[2], elements[3] + if e1.Tag == 4 && e2.Tag == 2 && e3.Tag == 2 { + foundData = e1.Bytes + var s string + if _, err := asn1.Unmarshal(e0.FullBytes, &s); err == nil { + foundType = s + } else { + foundType = string(e0.Bytes) + } + foundType = strings.ToLower(strings.TrimSpace(strings.ReplaceAll(foundType, "\x00", ""))) + if foundType == "es" { + foundType = "png" + } + return true + } + } + for _, child := range elements { + if search(child) { + return true + } + } + } + } + return false + } + if search(raw) { + return foundType, foundData + } + return "", nil +} + +// Stamp 印章信息结构 +type Stamp struct { + Box Box + Type string + Data []byte +} + +// addStamp 添加印章到页面 +// 入参: pageID 页面ID, box 印章区域, sType 印章类型, data 印章数据 +func (r *Reader) addStamp(pageID string, box Box, sType string, data []byte) { + if r.Stamps == nil { + r.Stamps = make(map[string][]Stamp) + } + r.Stamps[pageID] = append(r.Stamps[pageID], Stamp{ + Box: box, + Type: sType, + Data: data, + }) +}