From 489db301c5b449b83d27ab382c7bbd716561b3d4 Mon Sep 17 00:00:00 2001 From: xiaoqidun Date: Tue, 13 Oct 2026 10:38:28 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E5=9B=BE=E6=96=87=E5=A4=84=E7=90=86):=20?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=9B=BE=E6=96=87=E7=BC=93=E5=AD=98=E4=B8=8E?= =?UTF-8?q?=E5=AF=BC=E5=87=BA=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ofdgo.go | 2 +- ofdgo_archive.go | 10 +++- ofdgo_backend.go | 26 ++++++++- ofdgo_compression.go | 5 +- ofdgo_font_backend.go | 2 +- ofdgo_render.go | 2 +- ofdgo_render_compile.go | 2 +- ofdgo_render_output.go | 96 ++++++++++++++++++++++++++------ ofdgo_render_text.go | 41 +++++++++++++- ofdgo_renderer.go | 9 +-- ofdgo_renderer_measure_canvas.go | 1 + ofdgo_renderer_output_canvas.go | 39 ++++++++++++- ofdgo_renderer_pdf_canvas.go | 86 ++++++++++++++++------------ ofdgo_renderer_text_canvas.go | 13 ++++- ofdgo_search.go | 45 +++++++++++++-- 15 files changed, 304 insertions(+), 75 deletions(-) diff --git a/ofdgo.go b/ofdgo.go index 4a0c338..f28e8fd 100644 --- a/ofdgo.go +++ b/ofdgo.go @@ -72,7 +72,7 @@ func NewRenderer(reader *Reader, opts ...RendererOption) *Renderer { DrawParams: reader.drawParamCache, CompositeGraphicUnits: reader.compositeGraphicUnitCache, templatePageCache: make(map[string]*PageContent), - imageCache: renderCache[string, image.Image]{limit: imageCacheLimit}, + imageCache: &renderCache[string, image.Image]{limit: imageCacheLimit}, backends: defaultRenderBackends(), backendStates: make(map[any]any), } diff --git a/ofdgo_archive.go b/ofdgo_archive.go index f2af967..6436c3c 100644 --- a/ofdgo_archive.go +++ b/ofdgo_archive.go @@ -64,6 +64,9 @@ func WriteArchive(ctx context.Context, writer io.Writer, files []ArchiveFile, op return count, err } } + if err := ctx.Err(); err != nil { + return count, err + } method := uint16(zip.Deflate) switch strings.ToLower(path.Ext(file.Name)) { case ".ofd", ".pdf", ".png", ".jpg", ".jpeg": @@ -85,8 +88,13 @@ func WriteArchive(ctx context.Context, writer io.Writer, files []ArchiveFile, op if err := archive.Close(); err != nil { return count, err } + if err := ctx.Err(); err != nil { + return count, err + } if options.OnProgress != nil { - return count, options.OnProgress(len(files), len(files)) + if err := options.OnProgress(len(files), len(files)); err != nil { + return count, err + } } return count, ctx.Err() } diff --git a/ofdgo_backend.go b/ofdgo_backend.go index 76a79e8..7733317 100644 --- a/ofdgo_backend.go +++ b/ofdgo_backend.go @@ -92,14 +92,17 @@ type SVGBackend interface { RenderSVG(renderer *Renderer, page *PageContent, writer io.Writer, mode SVGMode) (SVGResources, error) } -// RenderDocumentPage 保存已解析页面和物理区域,不得修改源页面内容 +// RenderDocumentPage 保存输出页面及物理区域,Content非空时优先使用已解析内容 +// Content为空时通过Source从当前阅读器按需读取,后端统一调用ReadContent且不得修改源页面 type RenderDocumentPage struct { Content *PageContent + Source Page Box Box } // PDFBackend 输出完整PDF,保留文字、链接、目录和文档信息 -// pages交由后端消费,可逐页清空Content以释放引用,不得改变页面顺序 +// pages交由后端消费,通过ReadContent逐页读取,可清空Content释放引用,不得改变页面顺序 +// 通过Renderer调用时,OutputContext为当前导出的取消上下文 // progress返回错误时应停止输出,调用方在失败后丢弃输出 type PDFBackend interface { Backend @@ -143,6 +146,25 @@ func (b RenderBackends) Info() BackendInfo { return BackendInfo{FontResources: backendName(b.FontResources), Fonts: backendName(b.Fonts), Geometry: backendName(b.Geometry), Compiler: backendName(b.Compiler), Raster: backendName(b.Raster), SVG: backendName(b.SVG), PDF: backendName(b.PDF), EPS: backendName(b.EPS)} } +// ReadContent 返回已有页面或按文档引用读取,不在描述对象中保留新解析的内容 +// 入参: ctx 取消上下文,nil使用后台上下文, reader 页面所属阅读器 +// 返回: *PageContent 只读页面内容, error 读取或取消错误 +func (p RenderDocumentPage) ReadContent(ctx context.Context, reader *Reader) (*PageContent, error) { + if ctx == nil { + ctx = context.Background() + } + if err := ctx.Err(); err != nil { + return nil, err + } + if p.Content != nil { + return p.Content, nil + } + if reader == nil || p.Source.BaseLoc == "" { + return nil, fmt.Errorf("missing document page source") + } + return reader.PageContentContext(ctx, p.Source) +} + // WithRenderBackends 替换完整后端组合,未配置的能力不会沿用默认实现 // 入参: backends 后端组合 // 返回: RendererOption 渲染选项 diff --git a/ofdgo_compression.go b/ofdgo_compression.go index bcd5c68..1a5f1e2 100644 --- a/ofdgo_compression.go +++ b/ofdgo_compression.go @@ -573,9 +573,12 @@ func (r *Renderer) writeCompressedImage(writer io.Writer, data []byte) error { return err } -// outputContext 获取仅用于输出的取消上下文 +// outputContext 优先使用当前页面上下文,否则使用输出上下文 // 返回: context.Context 取消上下文 func (r *Renderer) outputContext() context.Context { + if r.pageContext != nil { + return r.pageContext + } if r.OutputContext != nil { return r.OutputContext } diff --git a/ofdgo_font_backend.go b/ofdgo_font_backend.go index f26d6b7..a29cc2f 100644 --- a/ofdgo_font_backend.go +++ b/ofdgo_font_backend.go @@ -283,7 +283,7 @@ func (r *Renderer) resetFontBackendCache() { r.fontSourcesCache.fallbackRead = false } r.fontMetrics = &renderCache[[32]byte, FontMetrics]{limit: 32 << 20} - r.glyphOutlines = renderCache[glyphOutlineKey, GeometryPath]{limit: 16 << 20} + r.glyphOutlines = &renderCache[glyphOutlineKey, GeometryPath]{limit: 16 << 20} for _, state := range r.backendStates { if fonts, ok := state.(interface{ resetFonts() }); ok { fonts.resetFonts() diff --git a/ofdgo_render.go b/ofdgo_render.go index 8f61e6c..b7ad68a 100644 --- a/ofdgo_render.go +++ b/ofdgo_render.go @@ -27,7 +27,7 @@ func (r *Renderer) RenderPage(page *PageContent) (*RasterPage, error) { // 入参: page 页面内容 // 返回: Box 区域, error 错误信息 func (r *Renderer) GetPageBox(page *PageContent) (Box, error) { - area, err := r.Reader.resolvePageArea(page.Area, page.Template) + area, err := r.Reader.resolvePageAreaContext(r.outputContext(), page.Area, page.Template) if err != nil { return Box{}, err } diff --git a/ofdgo_render_compile.go b/ofdgo_render_compile.go index 28a0a2e..92f5adc 100644 --- a/ofdgo_render_compile.go +++ b/ofdgo_render_compile.go @@ -396,7 +396,7 @@ func (c *semanticCompiler) textObject(object TextObject, state RenderState) erro if object.Visible != nil && !*object.Visible || object.Fill != nil && !*object.Fill && (object.Stroke == nil || !*object.Stroke) { return nil } - positioned, err := c.renderer.PositionText(object, state) + positioned, err := c.renderer.positionText(object, state, c.textOnly && c.renderer.textContentOnly) if err != nil { return err } diff --git a/ofdgo_render_output.go b/ofdgo_render_output.go index 66f2ea2..d845e25 100644 --- a/ofdgo_render_output.go +++ b/ofdgo_render_output.go @@ -90,22 +90,34 @@ func (r *Renderer) RenderTo(page *PageContent, writer io.Writer, format string) } // RenderToZIP 逐页渲染并写入ZIP,文件编号按总页数补零,出错时调用方应丢弃输出 +// 读取和渲染使用OutputContext响应取消,TXT只解析文字提取所需的图元 // 入参: writer 输出流, format svg、pdf、eps、png、jpg、jpeg或txt, indices 零基页面索引,省略则全部,按原页序去重 // 返回: error 错误信息 func (r *Renderer) RenderToZIP(writer io.Writer, format string, indices ...int) error { + renderer := *r + if renderer.pageContext == nil { + renderer.pageContext = r.outputContext() + } + r = &renderer + ctx := r.pageContext extension, render, err := r.outputRenderer(format) if err != nil { return err } - count, err := r.Reader.PageCount() + if err := r.Compression.Validate(); err != nil { + return err + } + doc, err := r.Reader.DocContext(ctx) if err != nil { return err } + count := len(doc.Pages.Page) indices, err = exportPageIndices(count, indices) if err != nil { return err } - archive := zip.NewWriter(writer) + var countWritten int64 + archive := zip.NewWriter(convertWriter{context: ctx, writer: writer, count: &countWritten}) configureArchive(archive, r.Compression) method := zip.Deflate if extension == "png" || extension == "jpg" { @@ -116,7 +128,7 @@ func (r *Renderer) RenderToZIP(writer io.Writer, format string, indices ...int) return err } for i, index := range indices { - page, err := r.Reader.PageContentByIndex(index) + page, err := r.Reader.readPageContent(ctx, doc.Pages.Page[index], extension == "txt", false) if err != nil { return fmt.Errorf("failed to read page %d: %w", index+1, err) } @@ -130,34 +142,59 @@ func (r *Renderer) RenderToZIP(writer io.Writer, format string, indices ...int) if err := render(page, entry); err != nil { return fmt.Errorf("failed to export page %d: %w", index+1, err) } + if err := ctx.Err(); err != nil { + return err + } if err := r.exportProgress(i+1, len(indices)); err != nil { return err } } - return archive.Close() + if err := ctx.Err(); err != nil { + return err + } + if err := archive.Close(); err != nil { + return err + } + return ctx.Err() } // RenderToText 导出页面原文为UTF-8文本,以换行分隔非空文本对象 // 入参: page 页面内容, writer 输出流 // 返回: error 错误信息 func (r *Renderer) RenderToText(page *PageContent, writer io.Writer) error { - text, err := r.PageText(page) + renderer := *r + renderer.textContentOnly = true + ctx := r.pageContext + if ctx == nil { + ctx = r.outputContext() + } + text, err := renderer.PageTextContext(ctx, page) if err != nil { return err } _, err = text.WriteTo(writer) - return err + if err != nil { + return err + } + return ctx.Err() } // RenderToMultiPageText 逐页导出UTF-8原文,以换行分隔非空页面,出错时调用方应丢弃输出 // 入参: writer 输出流, indices 零基页面索引,省略则全部,按原页序去重 // 返回: error 错误信息 func (r *Renderer) RenderToMultiPageText(writer io.Writer, indices ...int) error { - count, err := r.Reader.PageCount() + renderer := *r + renderer.textContentOnly = true + r = &renderer + ctx := r.pageContext + if ctx == nil { + ctx = r.outputContext() + } + doc, err := r.Reader.DocContext(ctx) if err != nil { return err } - indices, err = exportPageIndices(count, indices) + indices, err = exportPageIndices(len(doc.Pages.Page), indices) if err != nil { return err } @@ -166,7 +203,7 @@ func (r *Renderer) RenderToMultiPageText(writer io.Writer, indices ...int) error } written := false for i, index := range indices { - text, err := r.PageTextByIndex(index) + text, err := r.PageTextByIndexContext(ctx, index) if err != nil { return fmt.Errorf("failed to extract page %d text: %w", index+1, err) } @@ -175,11 +212,14 @@ func (r *Renderer) RenderToMultiPageText(writer io.Writer, indices ...int) error return err } written = written || n > 0 + if err := ctx.Err(); err != nil { + return err + } if err := r.exportProgress(i+1, len(indices)); err != nil { return err } } - return nil + return ctx.Err() } // RenderToImage 渲染为光栅图 @@ -291,11 +331,12 @@ func (r *Renderer) RenderToEPS(page *PageContent, writer io.Writer) error { return r.backends.EPS.RenderEPS(r, page, writer) } -// RenderToMultiPagePDF 将文档页面导出为多页PDF +// RenderToMultiPagePDF 将文档页面导出为多页PDF,仅预读页面区域和资源索引 // 入参: writer 输出流, indices 零基页面索引,省略则全部,按原页序去重 // 返回: error 错误信息 func (r *Renderer) RenderToMultiPagePDF(writer io.Writer, indices ...int) error { - doc, err := r.Reader.Doc() + ctx := r.outputContext() + doc, err := r.Reader.DocContext(ctx) if err != nil { return err } @@ -308,15 +349,21 @@ func (r *Renderer) RenderToMultiPagePDF(writer io.Writer, indices ...int) error } pages := make([]RenderDocumentPage, len(indices)) for i, index := range indices { - page, err := r.Reader.PageContent(doc.Pages.Page[index]) + ref := doc.Pages.Page[index] + page, err := r.Reader.readPageHeaderContext(ctx, ref) if err != nil { return fmt.Errorf("failed to read page %d: %w", index+1, err) } - box, err := r.GetPageBox(page) + for _, res := range page.PageRes { + if err := r.Reader.loadResContext(ctx, "/"+resolveResourcePath(r.Reader.ResPath(ref.BaseLoc), "", res)); err != nil { + return fmt.Errorf("failed to read page %d resources: %w", index+1, err) + } + } + box, err := r.GetPageBox(&page) if err != nil { return fmt.Errorf("failed to read page %d area: %w", index+1, err) } - pages[i] = RenderDocumentPage{Content: page, Box: box} + pages[i] = RenderDocumentPage{Source: ref, Box: box} } return r.renderPDFPages(pages, writer, r.OnExportProgress) } @@ -325,6 +372,10 @@ func (r *Renderer) RenderToMultiPagePDF(writer io.Writer, indices ...int) error // 入参: contents 页面内容列表, writer 输出流 // 返回: error 错误信息 func (r *Renderer) RenderPagesToPDF(contents []*PageContent, writer io.Writer) error { + ctx := r.outputContext() + if err := ctx.Err(); err != nil { + return err + } if len(contents) == 0 { return fmt.Errorf("no pages found") } @@ -333,6 +384,9 @@ func (r *Renderer) RenderPagesToPDF(contents []*PageContent, writer io.Writer) e } pages := make([]RenderDocumentPage, len(contents)) for i, page := range contents { + if err := ctx.Err(); err != nil { + return err + } box, err := r.GetPageBox(page) if err != nil { return fmt.Errorf("failed to read page %d area: %w", i+1, err) @@ -405,11 +459,21 @@ func fillWhiteBackground(img image.Image) image.Image { // 入参: pages 页面列表, writer 输出流, progress 页面处理进度 // 返回: error 错误信息 func (r *Renderer) renderPDFPages(pages []RenderDocumentPage, writer io.Writer, progress func(int, int) error) error { + ctx := r.outputContext() + if err := ctx.Err(); err != nil { + return err + } if err := r.Compression.Validate(); err != nil { return err } if r.backends.PDF == nil { return fmt.Errorf("pdf: %w", ErrBackendUnavailable) } - return r.backends.PDF.RenderPDF(r, pages, writer, progress) + renderer := *r + renderer.pageContext = ctx + renderer.OutputContext = ctx + if err := renderer.backends.PDF.RenderPDF(&renderer, pages, writer, progress); err != nil { + return err + } + return ctx.Err() } diff --git a/ofdgo_render_text.go b/ofdgo_render_text.go index 553610b..ea2b5ae 100644 --- a/ofdgo_render_text.go +++ b/ofdgo_render_text.go @@ -47,8 +47,17 @@ type glyphOutlineKey struct { // 入参: object 文字对象, state 继承状态 // 返回: *PositionedText 定位结果, error 字体或几何错误 func (r *Renderer) PositionText(object TextObject, state RenderState) (*PositionedText, error) { + return r.positionText(object, state, false) +} + +// positionText 提取原文,按需计算字形与搜索范围 +// 入参: object 文字对象, state 继承状态, contentOnly 仅提取原文 +// 返回: *PositionedText 定位结果, error 字体或几何错误 +func (r *Renderer) positionText(object TextObject, state RenderState, contentOnly bool) (*PositionedText, error) { result := &PositionedText{Run: TextRun{ID: object.ID, Text: object.Text()}} - result.Run.Boxes = make([]Box, len([]rune(result.Run.Text))) + if !contentOnly { + result.Run.Boxes = make([]Box, len([]rune(result.Run.Text))) + } if state.Hidden || object.Visible != nil && !*object.Visible { return result, nil } @@ -87,6 +96,9 @@ func (r *Renderer) PositionText(object TextObject, state RenderState) (*Position return nil, err } if len(prepared.Data) == 0 { + if contentOnly { + return result, nil + } box, _ := ParseBox(object.Boundary) boundary, _ := renderObjectMatrix(object.Boundary, IdentityMatrix, state) path, err := geometry.Transform(geometryRectangle(Box{W: box.W, H: box.H}), boundary) @@ -132,9 +144,14 @@ func (r *Renderer) PositionText(object TextObject, state RenderState) (*Position if recoverBaseline { if text := object.Text(); text != result.Run.Text { result.Run.Text = text - result.Run.Boxes = make([]Box, len([]rune(text))) + if !contentOnly { + result.Run.Boxes = make([]Box, len([]rune(text))) + } } } + if contentOnly { + return result, nil + } transforms := r.textObjectGlyphTransforms(id, object) codeOffset, textOffset := 0, 0 angle := float64(object.CharDirection) * math.Pi / 180 @@ -174,6 +191,11 @@ func (r *Renderer) PositionText(object TextObject, state RenderState) (*Position } positioner := NewTextPositioner(code) for i, gid := range ids { + if i%64 == 0 && r.pageContext != nil { + if err := r.pageContext.Err(); err != nil { + return nil, err + } + } path := paths[i] width := float64(metrics.GlyphAdvance(gid)) * unit origin := positioner.Next() @@ -275,6 +297,11 @@ func (r *Renderer) preparedOutlines(prepared *PreparedFont, metrics FontMetrics, missing := make([]uint16, 0) unique := make(map[uint16]int) for i, glyph := range glyphs { + if i%64 == 0 && r.pageContext != nil { + if err := r.pageContext.Err(); err != nil { + return nil, err + } + } if path, ok := r.glyphOutlines.get(glyphOutlineKey{prepared, glyph, size}); ok { result[i] = path continue @@ -313,6 +340,11 @@ func (r *Renderer) preparedOutlines(prepared *PreparedFont, metrics FontMetrics, } paths = make([]GeometryPath, len(missing)) for i, glyph := range missing { + if r.pageContext != nil { + if err := r.pageContext.Err(); err != nil { + return nil, err + } + } var err error paths[i], err = provider.GlyphOutline(glyph, size) if err != nil { @@ -320,6 +352,11 @@ func (r *Renderer) preparedOutlines(prepared *PreparedFont, metrics FontMetrics, } } } + if r.pageContext != nil { + if err := r.pageContext.Err(); err != nil { + return nil, err + } + } for i, path := range paths { r.glyphOutlines.put(glyphOutlineKey{prepared, missing[i], size}, path, len(path)*96+128) } diff --git a/ofdgo_renderer.go b/ofdgo_renderer.go index 8ad1790..e88bee8 100644 --- a/ofdgo_renderer.go +++ b/ofdgo_renderer.go @@ -26,7 +26,7 @@ import ( // OnPageText可选,接收页面绘制时同步提取的文字,不包含图案和签名外观 // OnExportProgress可选,同步回报文档导出的已处理页数及总页数,返回错误则停止,页数完成不代表写入成功 // TransparentBackground关闭页面白底,供嵌套图案和印章保持透明背景 -// Compression仅控制导出编码,OutputContext可取消输出压缩,nil使用后台上下文 +// Compression仅控制导出编码,OutputContext用于导出和文字提取的取消,nil使用后台上下文 type Renderer struct { Reader *Reader DPI float64 @@ -38,13 +38,14 @@ type Renderer struct { DrawParams map[string]*DrawParam CompositeGraphicUnits map[string]*CompositeGraphicUnit templatePageCache map[string]*PageContent - imageCache renderCache[string, image.Image] + imageCache *renderCache[string, image.Image] fontDirs []string fontFS []fs.FS decodeImages bool pageText *PageText pageContext context.Context textOnly bool + textContentOnly bool backends RenderBackends backendStates map[any]any sharedBackendStates map[any]any @@ -54,7 +55,7 @@ type Renderer struct { fontPreparations *renderCache[[32]byte, PreparedFont] fontSourcesCache *fontSourceCache fontMetrics *renderCache[[32]byte, FontMetrics] - glyphOutlines renderCache[glyphOutlineKey, GeometryPath] + glyphOutlines *renderCache[glyphOutlineKey, GeometryPath] renderError error patternReferences map[*Pattern]bool patternPage *Matrix @@ -81,7 +82,7 @@ func (r *Renderer) deviceLineWidth(width float64) float64 { // ClearCache 释放派生字体、图片、模板与后端缓存,不改变文档和渲染配置 func (r *Renderer) ClearCache() { r.backendStates = make(map[any]any) - r.imageCache = renderCache[string, image.Image]{limit: imageCacheLimit} + r.imageCache = &renderCache[string, image.Image]{limit: imageCacheLimit} r.templatePageCache = make(map[string]*PageContent) r.resetFontCache() } diff --git a/ofdgo_renderer_measure_canvas.go b/ofdgo_renderer_measure_canvas.go index 6715086..526bad7 100644 --- a/ofdgo_renderer_measure_canvas.go +++ b/ofdgo_renderer_measure_canvas.go @@ -53,6 +53,7 @@ func (CanvasBackend) MeasureObject(r *Renderer, object GraphicObject, options Me renderer.renderError = nil renderer.pageText = nil renderer.textOnly = false + renderer.textContentOnly = false defaults := options.Defaults ctx := canvas.NewContext(bounds) switch object.Type { diff --git a/ofdgo_renderer_output_canvas.go b/ofdgo_renderer_output_canvas.go index bc7e1c8..cd2d442 100644 --- a/ofdgo_renderer_output_canvas.go +++ b/ofdgo_renderer_output_canvas.go @@ -20,6 +20,7 @@ import ( "fmt" "image" "io" + "strconv" "github.com/tdewolff/canvas" "github.com/tdewolff/canvas/renderers/pdf" @@ -111,10 +112,18 @@ func (w canvasEPSWriter) Write(data []byte) (int, error) { // 入参: r 渲染器, pages 页面列表, writer 输出流, progress 页面处理进度 // 返回: error 错误信息 func (CanvasBackend) RenderPDF(r *Renderer, pages []RenderDocumentPage, writer io.Writer, progress func(int, int) error) error { + ctx := r.outputContext() + if err := ctx.Err(); err != nil { + return err + } + view := *r + view.pageContext = ctx + r = &view if len(pages) == 0 { return fmt.Errorf("no pages found") } - navigation, err := newPDFNavigation(r, r.Reader.doc, pages) + doc := r.Reader.doc + navigation, err := newPDFNavigation(r, doc, pages) if err != nil { return err } @@ -139,13 +148,22 @@ func (CanvasBackend) RenderPDF(r *Renderer, pages []RenderDocumentPage, writer i } p.SetInfo(info.Title, info.Subject, "", info.Author, ofdCreator) for i, page := range pages { + content, err := page.ReadContent(ctx, r.Reader) + if err != nil { + buf.Truncate(start) + return fmt.Errorf("failed to read page %d: %w", i+1, err) + } + if err := navigation.addPage(r, i, content, pages); err != nil { + buf.Truncate(start) + return fmt.Errorf("failed to read page %d navigation: %w", i+1, err) + } if i > 0 { p.NewPage(page.Box.W, page.Box.H) renderer.images = append(renderer.images, nil) renderer.text = append(renderer.text, pdfPageText{}) } navigation.apply(p, i) - if err := r.renderCanvasPageToContext(canvas.NewContext(renderer), page.Content, !r.TransparentBackground); err != nil { + if err := r.renderCanvasPageToContext(canvas.NewContext(renderer), content, !r.TransparentBackground); err != nil { buf.Truncate(start) return fmt.Errorf("failed to render page %d: %w", i+1, err) } @@ -154,12 +172,29 @@ func (CanvasBackend) RenderPDF(r *Renderer, pages []RenderDocumentPage, writer i return fmt.Errorf("failed to render page %d: %w", i+1, renderer.imageError) } pages[i].Content = nil + if err := ctx.Err(); err != nil { + buf.Truncate(start) + return err + } if progress != nil { if err := progress(i+1, len(pages)); err != nil { buf.Truncate(start) return err } } + if err := ctx.Err(); err != nil { + buf.Truncate(start) + return err + } + } + if doc != nil { + if err := navigation.addOutlines(doc.Outlines.OutlineElem, 0, navigation.bookmarks, navigation.pageIndex, pages); err != nil { + buf.Truncate(start) + return err + } + } + for index, outline := range navigation.Outline { + p.AddOutline(strconv.Itoa(index), outline.Level, 0) } if err := p.Close(); err != nil { buf.Truncate(start) diff --git a/ofdgo_renderer_pdf_canvas.go b/ofdgo_renderer_pdf_canvas.go index 8a8aa91..622010c 100644 --- a/ofdgo_renderer_pdf_canvas.go +++ b/ofdgo_renderer_pdf_canvas.go @@ -52,6 +52,8 @@ type pdfNavigation struct { Outline []pdfOutline Open []pdfNavigationAction PageOpen map[int][]pdfNavigationAction + pageIndex map[string]int + bookmarks map[string]Dest attachments map[string]Attachment audio map[string]*pdfgo.FileSpecification video map[string]*pdfVideoResource @@ -149,14 +151,20 @@ func (r *pdfRenderer) glyphPath(path *canvas.Path, matrix canvas.Matrix) *canvas return converted } -// newPDFNavigation 创建PDF导航信息 -// 入参: renderer 渲染器, doc 文档结构, pages 页面数据 -// 返回: *pdfNavigation PDF导航信息, error 区域或目标错误 +// newPDFNavigation 建立文档导航与输出页索引,不读取页面图元 +// 入参: renderer 渲染器, doc 文档结构, pages 输出页面描述 +// 返回: *pdfNavigation 导航信息, error 资源或目标错误 func newPDFNavigation(renderer *Renderer, doc *Document, pages []RenderDocumentPage) (*pdfNavigation, error) { + ctx := renderer.outputContext() + if err := ctx.Err(); err != nil { + return nil, err + } navigation := &pdfNavigation{ - Link: make(map[int][]pdfLink), - PageOpen: make(map[int][]pdfNavigationAction), - source: renderer.Reader, + Link: make(map[int][]pdfLink), + PageOpen: make(map[int][]pdfNavigationAction), + pageIndex: make(map[string]int, len(pages)), + bookmarks: make(map[string]Dest), + source: renderer.Reader, } if doc != nil && (doc.Attachments.Path != "" || len(doc.Attachments.Attachment) != 0) { attachments, err := renderer.Reader.Attachments() @@ -188,11 +196,18 @@ func newPDFNavigation(renderer *Renderer, doc *Document, pages []RenderDocumentP } navigation.nativeWrite = len(attachments) != 0 } - pageIndex := make(map[string]int, len(pages)) + pageIndex := navigation.pageIndex for i, page := range pages { - pageIndex[page.Content.ID] = i + if err := ctx.Err(); err != nil { + return nil, err + } + id := page.Source.ID + if page.Content != nil { + id = page.Content.ID + } + pageIndex[id] = i } - bookmarks := make(map[string]Dest) + bookmarks := navigation.bookmarks if doc != nil { for _, bookmark := range doc.Bookmarks.Bookmark { bookmarks[bookmark.Name] = bookmark.Dest @@ -201,30 +216,34 @@ func newPDFNavigation(renderer *Renderer, doc *Document, pages []RenderDocumentP return nil, err } } - for i, page := range pages { - for _, source := range renderer.pageOpenActionSources(page.Content) { - if err := navigation.addLifecycleActions(renderer.outputContext(), source, i, bookmarks, pageIndex, pages); err != nil { - return nil, err - } - } - sources := renderer.pageActionSources(page.Content) - if renderer.RenderAnnotations { - sources = append(sources, renderer.annotationActionSources(renderer.Reader.Annots[page.Content.ID])...) - } - for _, source := range sources { - if err := navigation.addActions(renderer, i, source, bookmarks, pageIndex, pages); err != nil { - return nil, err - } - } - } - if doc != nil { - if err := navigation.addOutlines(doc.Outlines.OutlineElem, 0, bookmarks, pageIndex, pages); err != nil { - return nil, err - } - } return navigation, nil } +// addPage 收集当前页面及模板、注释中的导航,不保留页面图元 +// 入参: renderer 渲染器, index 输出页索引, page 页面内容, pages 输出页面描述 +// 返回: error 区域、目标或取消错误 +func (n *pdfNavigation) addPage(renderer *Renderer, index int, page *PageContent, pages []RenderDocumentPage) error { + ctx := renderer.outputContext() + if err := ctx.Err(); err != nil { + return err + } + for _, source := range renderer.pageOpenActionSources(page) { + if err := n.addLifecycleActions(ctx, source, index, n.bookmarks, n.pageIndex, pages); err != nil { + return err + } + } + sources := renderer.pageActionSources(page) + if renderer.RenderAnnotations { + sources = append(sources, renderer.annotationActionSources(renderer.Reader.Annots[page.ID])...) + } + for _, source := range sources { + if err := n.addActions(renderer, index, source, n.bookmarks, n.pageIndex, pages); err != nil { + return err + } + } + return ctx.Err() +} + // actionTarget 转换动作及资源引用,选页未包含的目标不写入 // 入参: action 动作, page 来源页索引,负值表示文档, source 动作来源, bookmarks 书签, pageIndex 页面索引表, pages 页面数据 // 返回: *pdfNavigationAction 导航动作,nil表示无输出动作, error 目标参数错误 @@ -298,17 +317,12 @@ func (n *pdfNavigation) addOutlines(outlines []OutlineElem, level int, bookmarks return nil } -// apply 生成链接及大纲结构,内部目标在原生写入时关联 +// apply 生成当前页链接,内部目标在原生写入时关联 // 入参: renderer PDF渲染器, page 页面索引 func (n *pdfNavigation) apply(renderer *pdf.PDF, page int) { for _, link := range n.Link[page] { renderer.AddLink("", link.Rect) } - if page == 0 { - for index, outline := range n.Outline { - renderer.AddOutline(strconv.Itoa(index), outline.Level, 0) - } - } } // prepareRewrite 关联输出页引用并准备附件、事件、链接及先序大纲的替换 diff --git a/ofdgo_renderer_text_canvas.go b/ofdgo_renderer_text_canvas.go index 589bb7a..b87077a 100644 --- a/ofdgo_renderer_text_canvas.go +++ b/ofdgo_renderer_text_canvas.go @@ -156,7 +156,7 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64 textRun = r.pageText.addRun(obj) } if ff == nil { - if textRun != nil { + if textRun != nil && !r.textContentOnly { box, _ := ParseBox(obj.Boundary) boundaryCTM := TranslationMatrix(box.X, box.Y) if parentCTM != nil { @@ -194,6 +194,10 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64 textRun.Boxes = make([]Box, len([]rune(text))) } } + if r.textOnly && r.textContentOnly { + ctx.Pop() + return + } glyphTransforms := r.textObjectGlyphTransforms(fontID, obj) hasUnderline := strings.Contains(obj.Decoration, "Underline") _, shadedFill := fillPaint.(canvas.Gradient) @@ -239,6 +243,13 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64 positioned := textCodePositioned(tc, positioner.xs, positioner.ys) drawAsPath := embeddedFont || face.FauxBold > 0 || positioned || fillClip != nil || shadedFill || fillPattern != nil || shouldStroke for i, glyph := range glyphs { + if i%64 == 0 && r.pageContext != nil { + if err := r.pageContext.Err(); err != nil { + r.renderError = err + ctx.Pop() + return + } + } str := glyph.Text drawAsGlyphPath := drawAsPath || glyph.GlyphID >= 0 if !drawAsGlyphPath { diff --git a/ofdgo_search.go b/ofdgo_search.go index b676750..c18df94 100644 --- a/ofdgo_search.go +++ b/ofdgo_search.go @@ -96,10 +96,33 @@ func (d *textPageTokens) Token() (xml.Token, error) { // 入参: page 页面内容 // 返回: *PageText 页面文字, error 错误信息 func (r *Renderer) PageText(page *PageContent) (*PageText, error) { + ctx := r.pageContext + if ctx == nil { + ctx = r.outputContext() + } + return r.PageTextContext(ctx, page) +} + +// PageTextContext 提取页面原文及字形位置,取消时不返回部分结果 +// 入参: ctx 取消上下文,nil使用后台上下文, page 页面内容 +// 返回: *PageText 页面文字, error 提取或取消错误 +func (r *Renderer) PageTextContext(ctx context.Context, page *PageContent) (*PageText, error) { + if ctx == nil { + ctx = context.Background() + } + if err := ctx.Err(); err != nil { + return nil, err + } if r.backends.Compiler == nil { return nil, fmt.Errorf("%w: page text", ErrBackendUnavailable) } - return r.backends.Compiler.PageText(r, page) + renderer := *r + renderer.pageContext = ctx + text, err := renderer.backends.Compiler.PageText(&renderer, page) + if canceled := ctx.Err(); canceled != nil { + return nil, canceled + } + return text, err } // PageTextByIndex 按页面索引提取文字,避免解析独立路径和图片图元 @@ -110,6 +133,16 @@ func (r *Renderer) PageTextByIndex(index int) (*PageText, error) { if ctx == nil { ctx = r.outputContext() } + return r.PageTextByIndexContext(ctx, index) +} + +// PageTextByIndexContext 按索引读取页面并提取文字,读取和页内计算均响应取消 +// 入参: ctx 取消上下文,nil使用后台上下文, index 页面索引,从0开始 +// 返回: *PageText 页面文字, error 读取、提取或取消错误 +func (r *Renderer) PageTextByIndexContext(ctx context.Context, index int) (*PageText, error) { + if ctx == nil { + ctx = context.Background() + } doc, err := r.Reader.DocContext(ctx) if err != nil { return nil, err @@ -121,7 +154,7 @@ func (r *Renderer) PageTextByIndex(index int) (*PageText, error) { if err != nil { return nil, err } - return r.PageText(page) + return r.PageTextContext(ctx, page) } // String 按文本对象顺序合并原文,以换行分隔非空对象 @@ -162,7 +195,7 @@ func (p *PageText) SearchEach(ctx context.Context, query string, visit func(Text } // SearchText 逐页搜索原文并立即返回匹配,不保留整本文字或识别图像 -// 入参: ctx 取消上下文,页面提取之间及匹配时检查, query 搜索文字, visit 零基页码和匹配访问函数, indices 页码,省略则全部,按原页序去重 +// 入参: ctx 取消上下文,页面读取、文字提取及匹配时检查, query 搜索文字, visit 零基页码和匹配访问函数, indices 页码,省略则全部,按原页序去重 // 返回: error 页面提取、取消或访问错误 func (r *Renderer) SearchText(ctx context.Context, query string, visit func(int, TextMatch) error, indices ...int) error { if err := ctx.Err(); err != nil { @@ -171,11 +204,11 @@ func (r *Renderer) SearchText(ctx context.Context, query string, visit func(int, if query == "" { return nil } - count, err := r.Reader.PageCount() + doc, err := r.Reader.DocContext(ctx) if err != nil { return err } - indices, err = exportPageIndices(count, indices) + indices, err = exportPageIndices(len(doc.Pages.Page), indices) if err != nil { return err } @@ -184,7 +217,7 @@ func (r *Renderer) SearchText(ctx context.Context, query string, visit func(int, if err := ctx.Err(); err != nil { return err } - text, err := r.PageTextByIndex(index) + text, err := r.PageTextByIndexContext(ctx, index) if err != nil { return fmt.Errorf("failed to search page %d: %w", index+1, err) }