From 786b1a41c203d81ea7dc5de00bf19aa3bbc4f501 Mon Sep 17 00:00:00 2001 From: xiaoqidun Date: Fri, 25 Sep 2026 00:31:52 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E8=BD=AC=E6=8D=A2=E8=83=BD=E5=8A=9B):=20?= =?UTF-8?q?=E5=A2=9E=E5=BC=BAPDF=E5=AD=97=E5=BD=A2=E4=B8=8E=E6=89=AB?= =?UTF-8?q?=E6=8F=8F=E4=BB=B6=E8=BD=AC=E6=8D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/webui/ofdgo.js | 4 ++ go.mod | 2 +- go.sum | 4 +- internal/webui/wasm.go | 9 ++- internal/webui/webui.go | 52 +++++++++------- ofdgo_editor.go | 35 ++++++++--- ofdgo_editor_font.go | 24 +++++++- ofdgo_editor_open.go | 17 +++++- ofdgo_font_info.go | 39 ++++++++---- ofdgo_pdf.go | 24 ++++++-- ofdgo_pdf_group.go | 2 +- ofdgo_pdf_object.go | 129 +++++++++++++++++++++++++++------------- ofdgo_sign_verify.go | 3 + ofdgo_writer.go | 3 + 14 files changed, 250 insertions(+), 97 deletions(-) diff --git a/assets/webui/ofdgo.js b/assets/webui/ofdgo.js index 0e8b970..7fc881d 100644 --- a/assets/webui/ofdgo.js +++ b/assets/webui/ofdgo.js @@ -4047,6 +4047,10 @@ async function openDocument(options = {}) { async function loadDocumentDetails(openSeq) { try { + const details = await callWASM("ofdgoDocumentInfo"); + if (openSeq !== state.openSeq) return; + Object.assign(state.doc, details, { detailsPending: false, detailsError: "" }); + renderMeta(); while (openSeq === state.openSeq) { await waitForPaint(); if (openSeq !== state.openSeq) return; diff --git a/go.mod b/go.mod index 3bfe202..8503b09 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-20260924125905-097a17bdf501 + github.com/xiaoqidun/pdfgo v0.0.0-20260924162208-698614ef984e 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 d17644f..7b48bd2 100644 --- a/go.sum +++ b/go.sum @@ -60,8 +60,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-20260924125905-097a17bdf501 h1:6kzh25IgKXVcAbigV1i3AO3pfpzAniC0F4mXwPGRTrY= -github.com/xiaoqidun/pdfgo v0.0.0-20260924125905-097a17bdf501/go.mod h1:/D0XHgfZ7xNhix95swpNaThpuqoJjVR5wIadogtR5pI= +github.com/xiaoqidun/pdfgo v0.0.0-20260924162208-698614ef984e h1:17a5JOpYM6gwXZnhapgrXA6H7LhtGVrF0rpeiEORYUA= +github.com/xiaoqidun/pdfgo v0.0.0-20260924162208-698614ef984e/go.mod h1:/D0XHgfZ7xNhix95swpNaThpuqoJjVR5wIadogtR5pI= 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 3742c66..29014e8 100644 --- a/internal/webui/wasm.go +++ b/internal/webui/wasm.go @@ -412,10 +412,13 @@ func documentInfo(args []js.Value) (any, error) { if currentSession == nil { return nil, fmt.Errorf("ofd document is not opened") } - if len(args) > 0 && args[0].Bool() && !currentSession.scanFontInfoBatch() { - return map[string]bool{"detailsPending": true}, nil + if len(args) > 0 && args[0].Bool() { + if !currentSession.scanFontInfoBatch() { + return map[string]bool{"detailsPending": true}, nil + } + return map[string]any{"fonts": currentSession.fontInfos, "fontCount": len(currentSession.fontInfos), "detailsPending": false}, nil } - return currentSession.Info(), nil + return currentSession.Details(), nil } // exportAttachment 分块导出附件 diff --git a/internal/webui/webui.go b/internal/webui/webui.go index 99fd111..d90b98a 100644 --- a/internal/webui/webui.go +++ b/internal/webui/webui.go @@ -89,6 +89,17 @@ type DocumentInfo struct { DetailsPending bool `json:"detailsPending,omitempty"` } +// DocumentDetails 不含页面数组的文档补充信息 +type DocumentDetails struct { + 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"` +} + // OutlineInfo 目录节点信息 type OutlineInfo = ofdgo.OutlineInfo @@ -491,17 +502,10 @@ func (s *Session) pageInfos() []PageInfo { return pages } -// Info 获取完整文档信息 -// 返回: DocumentInfo 文档信息 -func (s *Session) Info() DocumentInfo { - info := DocumentInfo{ - Encryption: s.encryptionInfo(), - Version: s.Reader.Version(), - DocType: s.Reader.DocType(), - PageCount: len(s.doc.Pages.Page), - Pages: s.pageInfos(), - Outlines: s.doc.OutlineInfos(), - } +// Details 获取无需逐页扫描的附件、签名及字体匹配结果 +// 返回: DocumentDetails 文档补充信息 +func (s *Session) Details() DocumentDetails { + info := DocumentDetails{} if attachments, err := s.Reader.Attachments(); err == nil { for _, attachment := range attachments { if attachment.Visible { @@ -523,20 +527,28 @@ func (s *Session) Info() DocumentInfo { info.SignatureError = err.Error() } info.SignatureCount = len(info.Signatures) - if docInfo, err := s.Reader.DocInfo(); err == nil && docInfo != nil { - info.Title = docInfo.Title - info.Author = docInfo.Author - info.Subject = docInfo.Subject - if docInfo.CustomDatas != nil { - info.CustomData = append([]ofdgo.CustomData(nil), docInfo.CustomDatas.CustomData...) - } - info.CreationDate = docInfo.CreationDate - info.ModDate = docInfo.ModDate + if fonts, err := s.Renderer.DeclaredFontInfos(); err == nil { + info.Fonts = fonts + info.FontCount = len(fonts) } + return info +} + +// Info 获取完整文档信息及逐页字体用量 +// 返回: DocumentInfo 文档信息 +func (s *Session) Info() DocumentInfo { + info := s.Summary() + details := s.Details() + info.Attachments = details.Attachments + info.AttachmentError = details.AttachmentError + info.Signatures = details.Signatures + info.SignatureCount = details.SignatureCount + info.SignatureError = details.SignatureError for !s.scanFontInfo() { } info.Fonts = s.fontInfos info.FontCount = len(info.Fonts) + info.DetailsPending = false return info } diff --git a/ofdgo_editor.go b/ofdgo_editor.go index c806bfa..d0586eb 100644 --- a/ofdgo_editor.go +++ b/ofdgo_editor.go @@ -503,7 +503,7 @@ func (e *Editor) nextID() string { return strconv.Itoa(e.maxID) } -// AddImage 注册PNG或JPEG图片,重复资源复用标识,引用后写入文档 +// AddImage 注册PNG、JPEG或JBIG2图片,重复资源复用标识,引用后写入文档 // 不透明纯黑白PNG仅在无损JBIG2编码更小时转换,其他图片保留原始编码 // 入参: data 图片数据 // 返回: string 图片资源标识, error 错误信息 @@ -512,8 +512,8 @@ func (e *Editor) AddImage(data []byte) (string, error) { if err != nil { return "", err } - if (format != "png" && format != "jpeg") || config.Width <= 0 || config.Height <= 0 { - return "", fmt.Errorf("only PNG and JPEG images are supported") + if (format != "png" && format != "jpeg" && format != "jbig2") || config.Width <= 0 || config.Height <= 0 { + return "", fmt.Errorf("only PNG, JPEG and JBIG2 images are supported") } key := editorResourceKey{checksum: sha256.Sum256(data), index: -1} if id, ok := e.resourceID[key]; ok { @@ -991,9 +991,20 @@ func (e *Editor) LayoutText(obj *TextObject, value string, options TextLayout) e // 入参: obj 文字对象 // 返回: error 错误信息 func (e *Editor) prepareText(obj *TextObject) error { - sfnt, err := e.editorFont(obj.Font) - if err != nil { - return err + var sfnt FontMetrics + external := false + for _, resource := range e.resources { + if resource.font != nil && resource.font.ID == obj.Font && resource.font.FontFile == "" { + external = true + break + } + } + if !external { + var err error + sfnt, err = e.editorFont(obj.Font) + if err != nil { + return err + } } if !finite(obj.Size) || obj.Size <= 0 { return fmt.Errorf("text requires a positive finite size") @@ -1004,8 +1015,14 @@ func (e *Editor) prepareText(obj *TextObject) error { if obj.VScale != 0 || obj.Decoration != "" { return &EditError{Code: EditUnsupportedObject, Err: fmt.Errorf("text extensions are not supported for creation")} } - if err := validateTextGlyphs(*obj, sfnt.NumGlyphs()); err != nil { - return err + if external { + if len(obj.CGTransform) != 0 { + return fmt.Errorf("external font text cannot use glyph indices") + } + } else { + if err := validateTextGlyphs(*obj, sfnt.NumGlyphs()); err != nil { + return err + } } if !finite(obj.HScale) || obj.HScale < 0 || !finite(obj.LineWidth) || obj.LineWidth < 0 || !finite(obj.MiterLimit) || obj.MiterLimit < 0 { return fmt.Errorf("invalid text dimensions") @@ -1045,7 +1062,7 @@ func (e *Editor) prepareText(obj *TextObject) error { transform++ } mapped := transform < len(obj.CGTransform) && position >= obj.CGTransform[transform].CodePosition - if !mapped && sfnt.GlyphIndex(char) == 0 { + if !external && !mapped && sfnt.GlyphIndex(char) == 0 { missing = append(missing, char) } position++ diff --git a/ofdgo_editor_font.go b/ofdgo_editor_font.go index bee4e5f..67b8e1c 100644 --- a/ofdgo_editor_font.go +++ b/ofdgo_editor_font.go @@ -72,6 +72,28 @@ func (e *Editor) AddFont(file FontFile, index int) (string, error) { return id, nil } +// AddExternalFont 注册不含字体文件的名称引用,由阅读环境匹配 +// 入参: name 字体名称 +// 返回: string 字体资源标识, error 错误信息 +func (e *Editor) AddExternalFont(name string) (string, error) { + name = strings.TrimSpace(name) + if name == "" { + return "", fmt.Errorf("empty external font name") + } + for _, resource := range e.resources { + if resource.font != nil && resource.font.FontFile == "" && resource.font.FontName == name { + return resource.font.ID, nil + } + } + if err := e.prepareSourceIDs(); err != nil { + return "", err + } + id := e.nextID() + e.resources = append(e.resources, editorResource{font: &Font{ID: id, FontName: name, FamilyName: name}}) + e.fontRenderer = nil + return id, nil +} + // newEditorFontUsage 创建字体用字记录 // 返回: *editorFontUsage 用字记录 func newEditorFontUsage() *editorFontUsage { @@ -133,7 +155,7 @@ func (e *Editor) subsetFonts(progress editorProgress) (map[string][]byte, error) used := make(map[string]map[uint16]bool) mapped := make(map[string]bool) for _, resource := range e.resources { - if resource.font == nil { + if resource.font == nil || resource.font.FontFile == "" { continue } resourceFont := e.fonts[resource.font.ID] diff --git a/ofdgo_editor_open.go b/ofdgo_editor_open.go index 2e61caa..f373a1f 100644 --- a/ofdgo_editor_open.go +++ b/ofdgo_editor_open.go @@ -781,13 +781,15 @@ func (e *Editor) editorFont(id string) (FontMetrics, error) { return nil, &EditError{Code: EditFontUnavailable, Err: fmt.Errorf("fonts: %w", ErrBackendUnavailable)} } var data []byte + createdExternal := false for _, resource := range e.resources { if resource.font != nil && resource.font.ID == id { data = resource.data + createdExternal = resource.font.FontFile == "" break } } - if data == nil && e.source != nil { + if data == nil && e.source != nil && !createdExternal { if e.fontRenderer == nil { e.fontRenderer = e.newRenderer(e.source.reader) } @@ -797,6 +799,17 @@ func (e *Editor) editorFont(id string) (FontMetrics, error) { } data = resolved.Data } + if data == nil && createdExternal { + reader, err := e.Reader() + if err != nil { + return nil, &EditError{Code: EditFontUnavailable, Err: err} + } + resolved, err := e.newRenderer(reader).ResolveFont(id, false) + if err != nil { + return nil, &EditError{Code: EditFontUnavailable, Err: err} + } + data = resolved.Data + } if data == nil { return nil, &EditError{Code: EditFontUnavailable, Err: fmt.Errorf("font %q not found", id)} } @@ -813,7 +826,7 @@ func (e *Editor) editorFont(id string) (FontMetrics, error) { // 返回: []byte 独立字体数据, error 错误信息 func (e *Editor) FontData(id string) ([]byte, error) { for _, resource := range e.resources { - if resource.font != nil && resource.font.ID == id { + if resource.font != nil && resource.font.ID == id && len(resource.data) != 0 { return bytes.Clone(resource.data), nil } } diff --git a/ofdgo_font_info.go b/ofdgo_font_info.go index a1278b2..1f1edd5 100644 --- a/ofdgo_font_info.go +++ b/ofdgo_font_info.go @@ -176,6 +176,15 @@ func (r *Renderer) ScanFontInfos() (*FontInfoScanner, error) { return &FontInfoScanner{renderer: r, doc: doc, usage: make(map[string]int)}, nil } +// DeclaredFontInfos 获取已声明字体的匹配结果,不扫描页面用量 +// 返回: []FontInfo 字体诊断列表, error 错误信息 +func (r *Renderer) DeclaredFontInfos() ([]FontInfo, error) { + if r == nil || r.Reader == nil { + return nil, fmt.Errorf("ofd renderer is not initialized") + } + return r.fontInfos(nil) +} + // Next 统计下一页或模板的字体用量 // 返回: bool 是否处理了页面,false表示扫描结束 func (s *FontInfoScanner) Next() bool { @@ -298,17 +307,27 @@ func (r *Renderer) fontInfo(font Font) FontInfo { } return info } - if resolved, err := r.ResolveFont(font.ID, false); err == nil && resolved.Source != "" { - info.Matched = resolved.Source - info.MatchedFace = resolved.Face - if resolved.Exact { - info.Status = FontStatusMatched - info.Detail = "使用外部字体文件" - } else { - info.Status = FontStatusFallback - info.Detail = "使用外部字体回退" + if r.backends.Fonts != nil { + if source, metrics := r.fontSourceMatch(r.backends.Fonts, font.ID, &font, false); metrics != nil { + data, err := r.readFontSource(source, &font) + if err == nil { + faces, err := (FontFile{Data: data}).Faces() + if err == nil && len(faces) != 0 { + face := faces[0] + face.Index = source.face + info.Matched = source.name + info.MatchedFace = &face + if source.exact { + info.Status = FontStatusMatched + info.Detail = "使用外部字体文件" + } else { + info.Status = FontStatusFallback + info.Detail = "使用外部字体回退" + } + return info + } + } } - return info } info.Status = FontStatusMissing info.Detail = "可用字体文件缺失" diff --git a/ofdgo_pdf.go b/ofdgo_pdf.go index 14c9c4e..756f816 100644 --- a/ofdgo_pdf.go +++ b/ofdgo_pdf.go @@ -26,15 +26,15 @@ import ( "github.com/xiaoqidun/pdfgo" ) -// PDFImportOptions 指定PDF转换的运行时后端、进度通知与源文件检查策略 -// Strict禁止恢复缺失的图形状态资源和无目标链接,默认恢复并在报告中记录警告 +// PDFImportOptions 指定PDF转换的运行时后端、进度通知与检查策略 +// Strict禁止恢复缺失资源或丢失内容语义,默认在报告中记录警告 type PDFImportOptions struct { Backends *RenderBackends Progress func(int) error Strict bool } -// PDFImportReport 汇总转换页数、对象数、链接数和源文件恢复警告 +// PDFImportReport 汇总转换页数、对象数、链接数和转换警告 type PDFImportReport struct { Pages int TextObjects int @@ -46,6 +46,7 @@ type PDFImportReport struct { // pdfImporter 保存单次转换的对象与资源状态 type pdfImporter struct { + ctx context.Context reader *pdfgo.Reader editor *Editor report PDFImportReport @@ -60,6 +61,8 @@ type pdfImporter struct { imageIDs map[*pdfgo.Stream]string maskClips map[*pdfgo.SoftMask]pdfgo.Path pageBox pdfgo.Rectangle + pageWidth float64 + pageHeight float64 pendingPath *pdfgo.PathMark } @@ -78,7 +81,7 @@ func ImportPDF(ctx context.Context, source io.ReaderAt, size int64, options PDFI if options.Backends != nil { editor.SetRenderBackends(*options.Backends) } - importer := pdfImporter{reader: reader, editor: editor, fontIDs: map[*pdfgo.Font]string{}, fontMetrics: map[string]FontMetrics{}, pages: map[pdfgo.Reference]*pdfgo.Page{}, pageIDs: map[pdfgo.Reference]string{}} + importer := pdfImporter{ctx: ctx, reader: reader, editor: editor, fontIDs: map[*pdfgo.Font]string{}, fontMetrics: map[string]FontMetrics{}, pages: map[pdfgo.Reference]*pdfgo.Page{}, pageIDs: map[pdfgo.Reference]string{}} err = reader.WalkPages(ctx, func(index int, page *pdfgo.Page) error { _, width, height := pdfPageMatrix(page) if _, err := editor.AddPage(width, height); err != nil { @@ -92,10 +95,12 @@ func ImportPDF(ctx context.Context, source io.ReaderAt, size int64, options PDFI return nil, PDFImportReport{}, err } err = reader.WalkPages(ctx, func(index int, page *pdfgo.Page) error { - matrix, _, _ := pdfPageMatrix(page) + matrix, width, height := pdfPageMatrix(page) importer.matrix = matrix importer.page = index importer.pageBox = page.CropBox + importer.pageWidth = width + importer.pageHeight = height importer.maskClips = make(map[*pdfgo.SoftMask]pdfgo.Path) visitor := pdfgo.Visitor{Path: importer.path, Text: importer.text, Image: importer.image} visitor.Group = func(mark pdfgo.GroupMark, walk func(pdfgo.Visitor) error) error { @@ -322,7 +327,7 @@ func (p *pdfImporter) appendPath(mark pdfgo.PathMark) error { if err != nil { return err } - if mark.Fill && mark.Style.FillOverprint && !pdfOpaqueBlack(mark.Style.Fill) || mark.Stroke && mark.Style.StrokeOverprint && !pdfOpaqueBlack(mark.Style.Stroke) { + if mark.Fill && mark.Style.FillOverprint && pdfOverprintNeedsSeparation(mark.Style.Fill) || mark.Stroke && mark.Style.StrokeOverprint && pdfOverprintNeedsSeparation(mark.Style.Stroke) { return &pdfgo.UnsupportedError{Feature: "color separation overprint"} } var points []pdfgo.Point @@ -392,3 +397,10 @@ func (p *pdfImporter) pathColor(paint pdfgo.Paint, box Box) *FillColor { func pdfOpaqueBlack(paint pdfgo.Paint) bool { return paint.Alpha == 1 && paint.CMYK != nil && paint.CMYK[3] == 1 } + +// pdfOverprintNeedsSeparation 判断彩色CMYK套印是否依赖分色输出 +// 入参: paint PDF颜色与不透明度 +// 返回: bool 是否需要保留分色语义 +func pdfOverprintNeedsSeparation(paint pdfgo.Paint) bool { + return paint.CMYK != nil && !pdfOpaqueBlack(paint) +} diff --git a/ofdgo_pdf_group.go b/ofdgo_pdf_group.go index 06f8b2f..84a5a82 100644 --- a/ofdgo_pdf_group.go +++ b/ofdgo_pdf_group.go @@ -72,7 +72,7 @@ func (p *pdfImporter) group(mark pdfgo.GroupMark, walk func(pdfgo.Visitor) error if stroke { color, overprint = path.Style.Stroke, path.Style.StrokeOverprint } - if color.Alpha != 1 || color.Axial != nil || overprint && !pdfOpaqueBlack(color) { + if color.Alpha != 1 || color.Axial != nil || overprint && pdfOverprintNeedsSeparation(color) { return &pdfgo.UnsupportedError{Feature: "transparent or overprinted group content"} } if paint == nil { diff --git a/ofdgo_pdf_object.go b/ofdgo_pdf_object.go index dc419c4..80652f0 100644 --- a/ofdgo_pdf_object.go +++ b/ofdgo_pdf_object.go @@ -56,14 +56,22 @@ func (p *pdfImporter) image(mark pdfgo.ImageMark) error { id = "" } if id == "" { + jbig2Original, err := source.JBIG2File() + if err != nil { + return err + } filter, err := p.reader.Resolve(source.Stream.Dictionary["Filter"]) if err != nil { return err } - jpegOriginal := filter == pdfgo.Name("DCTDecode") && len(source.Decode) == 0 && !source.ImageMask && source.ColorSpace != pdfgo.Name("DeviceCMYK") && source.Mask == nil && source.SoftMask == nil - decoded, err := source.DecodeImage() - if err != nil { - return err + space, _ := source.ColorSpace.(pdfgo.Name) + jpegOriginal := filter == pdfgo.Name("DCTDecode") && len(source.Decode) == 0 && !source.ImageMask && (space == "DeviceGray" || space == "DeviceRGB") && source.Mask == nil && source.SoftMask == nil + var decoded image.Image + if len(jbig2Original) == 0 { + decoded, err = source.DecodeImage() + if err != nil { + return err + } } if source.ImageMask { if mark.Style.Fill.CMYK != nil { @@ -85,7 +93,9 @@ func (p *pdfImporter) image(mark pdfgo.ImageMark) error { decoded = stencil } var encoded bytes.Buffer - if jpegOriginal { + if len(jbig2Original) != 0 { + encoded.Write(jbig2Original) + } else if jpegOriginal { encoded.Write(source.Stream.Data) } else { if err := png.Encode(&encoded, decoded); err != nil { @@ -112,10 +122,25 @@ func (p *pdfImporter) image(mark pdfgo.ImageMark) error { return nil } -// text 保留原字体、字形编号与逐字基线,不重新塑形 +// text 保留内嵌字形及逐字基线,外部字体保留名称引用 // 入参: mark PDF文字绘制信息 // 返回: error 错误信息 func (p *pdfImporter) text(mark pdfgo.TextMark) error { + if mark.Font.Subtype == pdfgo.Name("Type3") { + if err := p.flushPath(); err != nil { + return err + } + visitor := pdfgo.Visitor{Path: p.path, Image: p.image} + visitor.Group = func(group pdfgo.GroupMark, walk func(pdfgo.Visitor) error) error { + return p.group(group, walk, visitor) + } + for index := range mark.Glyphs { + if err := p.reader.WalkType3Glyph(p.ctx, mark, index, visitor); err != nil { + return err + } + } + return p.flushPath() + } if mark.Style.BlendMode == "Multiply" { return &pdfgo.UnsupportedError{Feature: "multiply text"} } @@ -130,44 +155,50 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error { if err != nil { return err } - if (mark.Mode == 0 || mark.Mode == 2) && mark.Style.FillOverprint && !pdfOpaqueBlack(mark.Style.Fill) || (mark.Mode == 1 || mark.Mode == 2) && mark.Style.StrokeOverprint && !pdfOpaqueBlack(mark.Style.Stroke) { + if (mark.Mode == 0 || mark.Mode == 2) && mark.Style.FillOverprint && pdfOverprintNeedsSeparation(mark.Style.Fill) || (mark.Mode == 1 || mark.Mode == 2) && mark.Style.StrokeOverprint && pdfOverprintNeedsSeparation(mark.Style.Stroke) { return &pdfgo.UnsupportedError{Feature: "text color separation overprint"} } font := mark.Font - if len(font.Program) == 0 { - return &pdfgo.UnsupportedError{Feature: "text without embedded font"} - } - if font.ProgramType != "FontFile2" && font.ProgramType != "OpenType" && font.ProgramType != "Type1C" && font.ProgramType != "CIDFontType0C" { + embedded := len(font.Program) != 0 + if embedded && font.ProgramType != "FontFile2" && font.ProgramType != "OpenType" && font.ProgramType != "Type1C" && font.ProgramType != "CIDFontType0C" { return &pdfgo.UnsupportedError{Feature: "font program " + string(font.ProgramType)} } id := p.fontIDs[font] if id == "" { var err error - program, err := pdfFontProgram(font) - if err != nil { - return err + if embedded { + program, err := pdfFontProgram(font) + if err != nil { + return err + } + id, err = p.editor.AddFont(FontFile{Name: font.Name + ".ttf", Data: program}, 0) + } else { + id, err = p.editor.AddExternalFont(font.Name) } - id, err = p.editor.AddFont(FontFile{Name: font.Name + ".ttf", Data: program}, 0) if err != nil { return err } p.fontIDs[font] = id } - metrics := p.fontMetrics[id] - if metrics == nil { - if p.editor.backends.Fonts == nil { - return fmt.Errorf("PDF font backend unavailable") + var metrics FontMetrics + var outline FontOutlines + if embedded { + metrics = p.fontMetrics[id] + if metrics == nil { + if p.editor.backends.Fonts == nil { + return fmt.Errorf("PDF font backend unavailable") + } + metrics, err = p.editor.backends.Fonts.OpenFont(p.editor.fonts[id].Data) + if err != nil { + return err + } + p.fontMetrics[id] = metrics } - var err error - metrics, err = p.editor.backends.Fonts.OpenFont(p.editor.fonts[id].Data) - if err != nil { - return err + var ok bool + outline, ok = metrics.(FontOutlines) + if !ok { + return fmt.Errorf("PDF font backend does not provide glyph outlines") } - p.fontMetrics[id] = metrics - } - outline, ok := metrics.(FontOutlines) - if !ok { - return fmt.Errorf("PDF font backend does not provide glyph outlines") } const unit = 25.4 / 72 m := p.matrix.Mul(mark.Matrix).Mul(pdfgo.Matrix{1 / unit, 0, 0, -1 / unit, 0, 0}) @@ -183,7 +214,7 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error { } glyph.Text = string(character) } - if !glyph.HasID { + if embedded && !glyph.HasID { if utf8.RuneCountInString(glyph.Text) != 1 { return &pdfgo.UnsupportedError{Feature: "simple font glyph ligature mapping"} } @@ -193,23 +224,34 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error { return fmt.Errorf("missing PDF glyph for %U", char) } } - if gid >= metrics.NumGlyphs() { + if embedded && gid >= metrics.NumGlyphs() { return fmt.Errorf("PDF glyph index outside embedded font") } + if !embedded && glyph.Text == "" { + return &pdfgo.UnsupportedError{Feature: "unmapped external font character"} + } origin := mark.Positions[n] object.TextCode = append(object.TextCode, TextCode{X: pdfNumbers(origin.X * unit), Y: pdfNumbers(-origin.Y * unit), Value: glyph.Text}) - object.CGTransform = append(object.CGTransform, CGTransform{CodePosition: position, CodeCount: utf8.RuneCountInString(glyph.Text), GlyphCount: 1, Glyphs: strconv.Itoa(int(gid))}) - position += utf8.RuneCountInString(glyph.Text) - path, err := outline.GlyphOutline(gid, object.Size) - if err != nil { - return err - } - bounds, err := path.Bounds() - if err != nil { - return err - } - for _, point := range []pdfgo.Point{{X: bounds.X, Y: bounds.Y}, {X: bounds.X + bounds.W, Y: bounds.Y}, {X: bounds.X, Y: bounds.Y + bounds.H}, {X: bounds.X + bounds.W, Y: bounds.Y + bounds.H}} { - points = append(points, m.Apply(pdfgo.Point{X: origin.X*unit + point.X*mark.HorizontalScale, Y: -origin.Y*unit + point.Y})) + if embedded { + count := utf8.RuneCountInString(glyph.Text) + object.CGTransform = append(object.CGTransform, CGTransform{CodePosition: position, CodeCount: count, GlyphCount: 1, Glyphs: strconv.Itoa(int(gid))}) + position += count + path, err := outline.GlyphOutline(gid, object.Size) + if err != nil { + return err + } + bounds, err := path.Bounds() + if err != nil { + return err + } + for _, point := range []pdfgo.Point{{X: bounds.X, Y: bounds.Y}, {X: bounds.X + bounds.W, Y: bounds.Y}, {X: bounds.X, Y: bounds.Y + bounds.H}, {X: bounds.X + bounds.W, Y: bounds.Y + bounds.H}} { + points = append(points, m.Apply(pdfgo.Point{X: origin.X*unit + point.X*mark.HorizontalScale, Y: -origin.Y*unit + point.Y})) + } + } else { + width := glyph.Width / 1000 * object.Size * mark.HorizontalScale + for _, point := range []pdfgo.Point{{X: 0, Y: -object.Size}, {X: width, Y: -object.Size}, {X: 0, Y: object.Size / 4}, {X: width, Y: object.Size / 4}} { + points = append(points, m.Apply(pdfgo.Point{X: origin.X*unit + point.X, Y: -origin.Y*unit + point.Y})) + } } } box := pdfBounds(points) @@ -232,6 +274,9 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error { } } object.Boundary = pdfBoundary(box) + if box.X >= p.pageWidth || box.Y >= p.pageHeight || box.X+box.W <= 0 || box.Y+box.H <= 0 { + visible = false + } object.CTM = pdfNumbers(m[0], m[1], m[2], m[3], m[4]-box.X, m[5]-box.Y) object.Fill = &fill object.Stroke = &stroke diff --git a/ofdgo_sign_verify.go b/ofdgo_sign_verify.go index 8d80d3e..7a7436b 100644 --- a/ofdgo_sign_verify.go +++ b/ofdgo_sign_verify.go @@ -307,6 +307,9 @@ func (r *Reader) VerifyDocumentSignatures(index int, opts ...SignatureVerifyOpti } body := r.OFD.DocBody[index] options.DocIndex, options.DocRoot = index, r.signatureCoveragePath(body.DocRoot) + if index == 0 && r.doc != nil && r.doc.Signatures == "" { + return nil, nil + } data, err := r.readFile(body.DocRoot) if err != nil { return nil, err diff --git a/ofdgo_writer.go b/ofdgo_writer.go index 2ec19e4..34e3003 100644 --- a/ofdgo_writer.go +++ b/ofdgo_writer.go @@ -330,6 +330,9 @@ func (e *Editor) writeParts(write func(string, []byte, bool) error, progress edi } for _, resources := range [][]editorResource{fonts, images} { for _, resource := range resources { + if resource.name == "" { + continue + } if err := write(resource.name, resource.data, resource.image != nil); err != nil { return err }