feat(图文处理): 优化图文缓存与导出处理
build.yaml / build (push) Successful in 52s

This commit is contained in:
xiaoqidun committed 2026-10-13 10:38:28 +08:00
1 parent 7df35ac122
commit 489db301c5
15 files changed
+292 -63

No files matched your search

+1 -1
View File
@@ -72,7 +72,7 @@ func NewRenderer(reader *Reader, opts ...RendererOption) *Renderer {
DrawParams: reader.drawParamCache, DrawParams: reader.drawParamCache,
CompositeGraphicUnits: reader.compositeGraphicUnitCache, CompositeGraphicUnits: reader.compositeGraphicUnitCache,
templatePageCache: make(map[string]*PageContent), templatePageCache: make(map[string]*PageContent),
imageCache: renderCache[string, image.Image]{limit: imageCacheLimit}, imageCache: &renderCache[string, image.Image]{limit: imageCacheLimit},
backends: defaultRenderBackends(), backends: defaultRenderBackends(),
backendStates: make(map[any]any), backendStates: make(map[any]any),
} }
+9 -1
View File
@@ -64,6 +64,9 @@ func WriteArchive(ctx context.Context, writer io.Writer, files []ArchiveFile, op
return count, err return count, err
} }
} }
if err := ctx.Err(); err != nil {
return count, err
}
method := uint16(zip.Deflate) method := uint16(zip.Deflate)
switch strings.ToLower(path.Ext(file.Name)) { switch strings.ToLower(path.Ext(file.Name)) {
case ".ofd", ".pdf", ".png", ".jpg", ".jpeg": 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 { if err := archive.Close(); err != nil {
return count, err return count, err
} }
if err := ctx.Err(); err != nil {
return count, err
}
if options.OnProgress != nil { 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() return count, ctx.Err()
} }
+24 -2
View File
@@ -92,14 +92,17 @@ type SVGBackend interface {
RenderSVG(renderer *Renderer, page *PageContent, writer io.Writer, mode SVGMode) (SVGResources, error) RenderSVG(renderer *Renderer, page *PageContent, writer io.Writer, mode SVGMode) (SVGResources, error)
} }
// RenderDocumentPage 保存已解析页面和物理区域,不得修改源页面内容 // RenderDocumentPage 保存输出页面及物理区域,Content非空时优先使用已解析内容
// Content为空时通过Source从当前阅读器按需读取,后端统一调用ReadContent且不得修改源页面
type RenderDocumentPage struct { type RenderDocumentPage struct {
Content *PageContent Content *PageContent
Source Page
Box Box Box Box
} }
// PDFBackend 输出完整PDF,保留文字、链接、目录和文档信息 // PDFBackend 输出完整PDF,保留文字、链接、目录和文档信息
// pages交由后端消费,可逐页清空Content以释放引用,不得改变页面顺序 // pages交由后端消费,通过ReadContent逐页读取,可清空Content释放引用,不得改变页面顺序
// 通过Renderer调用时,OutputContext为当前导出的取消上下文
// progress返回错误时应停止输出,调用方在失败后丢弃输出 // progress返回错误时应停止输出,调用方在失败后丢弃输出
type PDFBackend interface { type PDFBackend interface {
Backend 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)} 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 替换完整后端组合,未配置的能力不会沿用默认实现 // WithRenderBackends 替换完整后端组合,未配置的能力不会沿用默认实现
// 入参: backends 后端组合 // 入参: backends 后端组合
// 返回: RendererOption 渲染选项 // 返回: RendererOption 渲染选项
+4 -1
View File
@@ -573,9 +573,12 @@ func (r *Renderer) writeCompressedImage(writer io.Writer, data []byte) error {
return err return err
} }
// outputContext 获取仅用于输出的取消上下文 // outputContext 优先使用当前页面上下文,否则使用输出上下文
// 返回: context.Context 取消上下文 // 返回: context.Context 取消上下文
func (r *Renderer) outputContext() context.Context { func (r *Renderer) outputContext() context.Context {
if r.pageContext != nil {
return r.pageContext
}
if r.OutputContext != nil { if r.OutputContext != nil {
return r.OutputContext return r.OutputContext
} }
+1 -1
View File
@@ -283,7 +283,7 @@ func (r *Renderer) resetFontBackendCache() {
r.fontSourcesCache.fallbackRead = false r.fontSourcesCache.fallbackRead = false
} }
r.fontMetrics = &renderCache[[32]byte, FontMetrics]{limit: 32 << 20} 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 { for _, state := range r.backendStates {
if fonts, ok := state.(interface{ resetFonts() }); ok { if fonts, ok := state.(interface{ resetFonts() }); ok {
fonts.resetFonts() fonts.resetFonts()
+1 -1
View File
@@ -27,7 +27,7 @@ func (r *Renderer) RenderPage(page *PageContent) (*RasterPage, error) {
// 入参: page 页面内容 // 入参: page 页面内容
// 返回: Box 区域, error 错误信息 // 返回: Box 区域, error 错误信息
func (r *Renderer) GetPageBox(page *PageContent) (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 { if err != nil {
return Box{}, err return Box{}, err
} }
+1 -1
View File
@@ -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) { if object.Visible != nil && !*object.Visible || object.Fill != nil && !*object.Fill && (object.Stroke == nil || !*object.Stroke) {
return nil return nil
} }
positioned, err := c.renderer.PositionText(object, state) positioned, err := c.renderer.positionText(object, state, c.textOnly && c.renderer.textContentOnly)
if err != nil { if err != nil {
return err return err
} }
+79 -15
View File
@@ -90,22 +90,34 @@ func (r *Renderer) RenderTo(page *PageContent, writer io.Writer, format string)
} }
// RenderToZIP 逐页渲染并写入ZIP,文件编号按总页数补零,出错时调用方应丢弃输出 // RenderToZIP 逐页渲染并写入ZIP,文件编号按总页数补零,出错时调用方应丢弃输出
// 读取和渲染使用OutputContext响应取消,TXT只解析文字提取所需的图元
// 入参: writer 输出流, format svg、pdf、eps、png、jpg、jpeg或txt, indices 零基页面索引,省略则全部,按原页序去重 // 入参: writer 输出流, format svg、pdf、eps、png、jpg、jpeg或txt, indices 零基页面索引,省略则全部,按原页序去重
// 返回: error 错误信息 // 返回: error 错误信息
func (r *Renderer) RenderToZIP(writer io.Writer, format string, indices ...int) 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) extension, render, err := r.outputRenderer(format)
if err != nil { if err != nil {
return err 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 { if err != nil {
return err return err
} }
count := len(doc.Pages.Page)
indices, err = exportPageIndices(count, indices) indices, err = exportPageIndices(count, indices)
if err != nil { if err != nil {
return err return err
} }
archive := zip.NewWriter(writer) var countWritten int64
archive := zip.NewWriter(convertWriter{context: ctx, writer: writer, count: &countWritten})
configureArchive(archive, r.Compression) configureArchive(archive, r.Compression)
method := zip.Deflate method := zip.Deflate
if extension == "png" || extension == "jpg" { if extension == "png" || extension == "jpg" {
@@ -116,7 +128,7 @@ func (r *Renderer) RenderToZIP(writer io.Writer, format string, indices ...int)
return err return err
} }
for i, index := range indices { 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 { if err != nil {
return fmt.Errorf("failed to read page %d: %w", index+1, err) 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 { if err := render(page, entry); err != nil {
return fmt.Errorf("failed to export page %d: %w", index+1, err) 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 { if err := r.exportProgress(i+1, len(indices)); err != nil {
return err 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文本,以换行分隔非空文本对象 // RenderToText 导出页面原文为UTF-8文本,以换行分隔非空文本对象
// 入参: page 页面内容, writer 输出流 // 入参: page 页面内容, writer 输出流
// 返回: error 错误信息 // 返回: error 错误信息
func (r *Renderer) RenderToText(page *PageContent, writer io.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 { if err != nil {
return err return err
} }
_, err = text.WriteTo(writer) _, err = text.WriteTo(writer)
if err != nil {
return err return err
}
return ctx.Err()
} }
// RenderToMultiPageText 逐页导出UTF-8原文,以换行分隔非空页面,出错时调用方应丢弃输出 // RenderToMultiPageText 逐页导出UTF-8原文,以换行分隔非空页面,出错时调用方应丢弃输出
// 入参: writer 输出流, indices 零基页面索引,省略则全部,按原页序去重 // 入参: writer 输出流, indices 零基页面索引,省略则全部,按原页序去重
// 返回: error 错误信息 // 返回: error 错误信息
func (r *Renderer) RenderToMultiPageText(writer io.Writer, indices ...int) 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 { if err != nil {
return err return err
} }
indices, err = exportPageIndices(count, indices) indices, err = exportPageIndices(len(doc.Pages.Page), indices)
if err != nil { if err != nil {
return err return err
} }
@@ -166,7 +203,7 @@ func (r *Renderer) RenderToMultiPageText(writer io.Writer, indices ...int) error
} }
written := false written := false
for i, index := range indices { for i, index := range indices {
text, err := r.PageTextByIndex(index) text, err := r.PageTextByIndexContext(ctx, index)
if err != nil { if err != nil {
return fmt.Errorf("failed to extract page %d text: %w", index+1, err) 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 return err
} }
written = written || n > 0 written = written || n > 0
if err := ctx.Err(); err != nil {
return err
}
if err := r.exportProgress(i+1, len(indices)); err != nil { if err := r.exportProgress(i+1, len(indices)); err != nil {
return err return err
} }
} }
return nil return ctx.Err()
} }
// RenderToImage 渲染为光栅图 // RenderToImage 渲染为光栅图
@@ -291,11 +331,12 @@ func (r *Renderer) RenderToEPS(page *PageContent, writer io.Writer) error {
return r.backends.EPS.RenderEPS(r, page, writer) return r.backends.EPS.RenderEPS(r, page, writer)
} }
// RenderToMultiPagePDF 将文档页面导出为多页PDF // RenderToMultiPagePDF 将文档页面导出为多页PDF,仅预读页面区域和资源索引
// 入参: writer 输出流, indices 零基页面索引,省略则全部,按原页序去重 // 入参: writer 输出流, indices 零基页面索引,省略则全部,按原页序去重
// 返回: error 错误信息 // 返回: error 错误信息
func (r *Renderer) RenderToMultiPagePDF(writer io.Writer, indices ...int) 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 { if err != nil {
return err return err
} }
@@ -308,15 +349,21 @@ func (r *Renderer) RenderToMultiPagePDF(writer io.Writer, indices ...int) error
} }
pages := make([]RenderDocumentPage, len(indices)) pages := make([]RenderDocumentPage, len(indices))
for i, index := range 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 { if err != nil {
return fmt.Errorf("failed to read page %d: %w", index+1, err) 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 { if err != nil {
return fmt.Errorf("failed to read page %d area: %w", index+1, err) 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) return r.renderPDFPages(pages, writer, r.OnExportProgress)
} }
@@ -325,6 +372,10 @@ func (r *Renderer) RenderToMultiPagePDF(writer io.Writer, indices ...int) error
// 入参: contents 页面内容列表, writer 输出流 // 入参: contents 页面内容列表, writer 输出流
// 返回: error 错误信息 // 返回: error 错误信息
func (r *Renderer) RenderPagesToPDF(contents []*PageContent, writer io.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 { if len(contents) == 0 {
return fmt.Errorf("no pages found") 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)) pages := make([]RenderDocumentPage, len(contents))
for i, page := range contents { for i, page := range contents {
if err := ctx.Err(); err != nil {
return err
}
box, err := r.GetPageBox(page) box, err := r.GetPageBox(page)
if err != nil { if err != nil {
return fmt.Errorf("failed to read page %d area: %w", i+1, err) 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 页面处理进度 // 入参: pages 页面列表, writer 输出流, progress 页面处理进度
// 返回: error 错误信息 // 返回: error 错误信息
func (r *Renderer) renderPDFPages(pages []RenderDocumentPage, writer io.Writer, progress func(int, int) error) 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 { if err := r.Compression.Validate(); err != nil {
return err return err
} }
if r.backends.PDF == nil { if r.backends.PDF == nil {
return fmt.Errorf("pdf: %w", ErrBackendUnavailable) 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()
} }
+37
View File
@@ -47,8 +47,17 @@ type glyphOutlineKey struct {
// 入参: object 文字对象, state 继承状态 // 入参: object 文字对象, state 继承状态
// 返回: *PositionedText 定位结果, error 字体或几何错误 // 返回: *PositionedText 定位结果, error 字体或几何错误
func (r *Renderer) PositionText(object TextObject, state RenderState) (*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 := &PositionedText{Run: TextRun{ID: object.ID, Text: object.Text()}}
if !contentOnly {
result.Run.Boxes = make([]Box, len([]rune(result.Run.Text))) result.Run.Boxes = make([]Box, len([]rune(result.Run.Text)))
}
if state.Hidden || object.Visible != nil && !*object.Visible { if state.Hidden || object.Visible != nil && !*object.Visible {
return result, nil return result, nil
} }
@@ -87,6 +96,9 @@ func (r *Renderer) PositionText(object TextObject, state RenderState) (*Position
return nil, err return nil, err
} }
if len(prepared.Data) == 0 { if len(prepared.Data) == 0 {
if contentOnly {
return result, nil
}
box, _ := ParseBox(object.Boundary) box, _ := ParseBox(object.Boundary)
boundary, _ := renderObjectMatrix(object.Boundary, IdentityMatrix, state) boundary, _ := renderObjectMatrix(object.Boundary, IdentityMatrix, state)
path, err := geometry.Transform(geometryRectangle(Box{W: box.W, H: box.H}), boundary) 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 recoverBaseline {
if text := object.Text(); text != result.Run.Text { if text := object.Text(); text != result.Run.Text {
result.Run.Text = text result.Run.Text = text
if !contentOnly {
result.Run.Boxes = make([]Box, len([]rune(text))) result.Run.Boxes = make([]Box, len([]rune(text)))
} }
} }
}
if contentOnly {
return result, nil
}
transforms := r.textObjectGlyphTransforms(id, object) transforms := r.textObjectGlyphTransforms(id, object)
codeOffset, textOffset := 0, 0 codeOffset, textOffset := 0, 0
angle := float64(object.CharDirection) * math.Pi / 180 angle := float64(object.CharDirection) * math.Pi / 180
@@ -174,6 +191,11 @@ func (r *Renderer) PositionText(object TextObject, state RenderState) (*Position
} }
positioner := NewTextPositioner(code) positioner := NewTextPositioner(code)
for i, gid := range ids { 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] path := paths[i]
width := float64(metrics.GlyphAdvance(gid)) * unit width := float64(metrics.GlyphAdvance(gid)) * unit
origin := positioner.Next() origin := positioner.Next()
@@ -275,6 +297,11 @@ func (r *Renderer) preparedOutlines(prepared *PreparedFont, metrics FontMetrics,
missing := make([]uint16, 0) missing := make([]uint16, 0)
unique := make(map[uint16]int) unique := make(map[uint16]int)
for i, glyph := range glyphs { 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 { if path, ok := r.glyphOutlines.get(glyphOutlineKey{prepared, glyph, size}); ok {
result[i] = path result[i] = path
continue continue
@@ -313,6 +340,11 @@ func (r *Renderer) preparedOutlines(prepared *PreparedFont, metrics FontMetrics,
} }
paths = make([]GeometryPath, len(missing)) paths = make([]GeometryPath, len(missing))
for i, glyph := range missing { for i, glyph := range missing {
if r.pageContext != nil {
if err := r.pageContext.Err(); err != nil {
return nil, err
}
}
var err error var err error
paths[i], err = provider.GlyphOutline(glyph, size) paths[i], err = provider.GlyphOutline(glyph, size)
if err != nil { 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 { for i, path := range paths {
r.glyphOutlines.put(glyphOutlineKey{prepared, missing[i], size}, path, len(path)*96+128) r.glyphOutlines.put(glyphOutlineKey{prepared, missing[i], size}, path, len(path)*96+128)
} }
+5 -4
View File
@@ -26,7 +26,7 @@ import (
// OnPageText可选,接收页面绘制时同步提取的文字,不包含图案和签名外观 // OnPageText可选,接收页面绘制时同步提取的文字,不包含图案和签名外观
// OnExportProgress可选,同步回报文档导出的已处理页数及总页数,返回错误则停止,页数完成不代表写入成功 // OnExportProgress可选,同步回报文档导出的已处理页数及总页数,返回错误则停止,页数完成不代表写入成功
// TransparentBackground关闭页面白底,供嵌套图案和印章保持透明背景 // TransparentBackground关闭页面白底,供嵌套图案和印章保持透明背景
// Compression仅控制导出编码,OutputContext可取消输出压缩,nil使用后台上下文 // Compression仅控制导出编码,OutputContext用于导出和文字提取的取消,nil使用后台上下文
type Renderer struct { type Renderer struct {
Reader *Reader Reader *Reader
DPI float64 DPI float64
@@ -38,13 +38,14 @@ type Renderer struct {
DrawParams map[string]*DrawParam DrawParams map[string]*DrawParam
CompositeGraphicUnits map[string]*CompositeGraphicUnit CompositeGraphicUnits map[string]*CompositeGraphicUnit
templatePageCache map[string]*PageContent templatePageCache map[string]*PageContent
imageCache renderCache[string, image.Image] imageCache *renderCache[string, image.Image]
fontDirs []string fontDirs []string
fontFS []fs.FS fontFS []fs.FS
decodeImages bool decodeImages bool
pageText *PageText pageText *PageText
pageContext context.Context pageContext context.Context
textOnly bool textOnly bool
textContentOnly bool
backends RenderBackends backends RenderBackends
backendStates map[any]any backendStates map[any]any
sharedBackendStates map[any]any sharedBackendStates map[any]any
@@ -54,7 +55,7 @@ type Renderer struct {
fontPreparations *renderCache[[32]byte, PreparedFont] fontPreparations *renderCache[[32]byte, PreparedFont]
fontSourcesCache *fontSourceCache fontSourcesCache *fontSourceCache
fontMetrics *renderCache[[32]byte, FontMetrics] fontMetrics *renderCache[[32]byte, FontMetrics]
glyphOutlines renderCache[glyphOutlineKey, GeometryPath] glyphOutlines *renderCache[glyphOutlineKey, GeometryPath]
renderError error renderError error
patternReferences map[*Pattern]bool patternReferences map[*Pattern]bool
patternPage *Matrix patternPage *Matrix
@@ -81,7 +82,7 @@ func (r *Renderer) deviceLineWidth(width float64) float64 {
// ClearCache 释放派生字体、图片、模板与后端缓存,不改变文档和渲染配置 // ClearCache 释放派生字体、图片、模板与后端缓存,不改变文档和渲染配置
func (r *Renderer) ClearCache() { func (r *Renderer) ClearCache() {
r.backendStates = make(map[any]any) 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.templatePageCache = make(map[string]*PageContent)
r.resetFontCache() r.resetFontCache()
} }
+1
View File
@@ -53,6 +53,7 @@ func (CanvasBackend) MeasureObject(r *Renderer, object GraphicObject, options Me
renderer.renderError = nil renderer.renderError = nil
renderer.pageText = nil renderer.pageText = nil
renderer.textOnly = false renderer.textOnly = false
renderer.textContentOnly = false
defaults := options.Defaults defaults := options.Defaults
ctx := canvas.NewContext(bounds) ctx := canvas.NewContext(bounds)
switch object.Type { switch object.Type {
+37 -2
View File
@@ -20,6 +20,7 @@ import (
"fmt" "fmt"
"image" "image"
"io" "io"
"strconv"
"github.com/tdewolff/canvas" "github.com/tdewolff/canvas"
"github.com/tdewolff/canvas/renderers/pdf" "github.com/tdewolff/canvas/renderers/pdf"
@@ -111,10 +112,18 @@ func (w canvasEPSWriter) Write(data []byte) (int, error) {
// 入参: r 渲染器, pages 页面列表, writer 输出流, progress 页面处理进度 // 入参: r 渲染器, pages 页面列表, writer 输出流, progress 页面处理进度
// 返回: error 错误信息 // 返回: error 错误信息
func (CanvasBackend) RenderPDF(r *Renderer, pages []RenderDocumentPage, writer io.Writer, progress func(int, int) error) 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 { if len(pages) == 0 {
return fmt.Errorf("no pages found") 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 { if err != nil {
return err return err
} }
@@ -139,13 +148,22 @@ func (CanvasBackend) RenderPDF(r *Renderer, pages []RenderDocumentPage, writer i
} }
p.SetInfo(info.Title, info.Subject, "", info.Author, ofdCreator) p.SetInfo(info.Title, info.Subject, "", info.Author, ofdCreator)
for i, page := range pages { 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 { if i > 0 {
p.NewPage(page.Box.W, page.Box.H) p.NewPage(page.Box.W, page.Box.H)
renderer.images = append(renderer.images, nil) renderer.images = append(renderer.images, nil)
renderer.text = append(renderer.text, pdfPageText{}) renderer.text = append(renderer.text, pdfPageText{})
} }
navigation.apply(p, i) 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) buf.Truncate(start)
return fmt.Errorf("failed to render page %d: %w", i+1, err) 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) return fmt.Errorf("failed to render page %d: %w", i+1, renderer.imageError)
} }
pages[i].Content = nil pages[i].Content = nil
if err := ctx.Err(); err != nil {
buf.Truncate(start)
return err
}
if progress != nil { if progress != nil {
if err := progress(i+1, len(pages)); err != nil { if err := progress(i+1, len(pages)); err != nil {
buf.Truncate(start) buf.Truncate(start)
return err 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 { if err := p.Close(); err != nil {
buf.Truncate(start) buf.Truncate(start)
+41 -27
View File
@@ -52,6 +52,8 @@ type pdfNavigation struct {
Outline []pdfOutline Outline []pdfOutline
Open []pdfNavigationAction Open []pdfNavigationAction
PageOpen map[int][]pdfNavigationAction PageOpen map[int][]pdfNavigationAction
pageIndex map[string]int
bookmarks map[string]Dest
attachments map[string]Attachment attachments map[string]Attachment
audio map[string]*pdfgo.FileSpecification audio map[string]*pdfgo.FileSpecification
video map[string]*pdfVideoResource video map[string]*pdfVideoResource
@@ -149,13 +151,19 @@ func (r *pdfRenderer) glyphPath(path *canvas.Path, matrix canvas.Matrix) *canvas
return converted return converted
} }
// newPDFNavigation 创建PDF导航信息 // newPDFNavigation 建立文档导航与输出页索引,不读取页面图元
// 入参: renderer 渲染器, doc 文档结构, pages 页面数据 // 入参: renderer 渲染器, doc 文档结构, pages 输出页面描述
// 返回: *pdfNavigation PDF导航信息, error 区域或目标错误 // 返回: *pdfNavigation 导航信息, error 资源或目标错误
func newPDFNavigation(renderer *Renderer, doc *Document, pages []RenderDocumentPage) (*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{ navigation := &pdfNavigation{
Link: make(map[int][]pdfLink), Link: make(map[int][]pdfLink),
PageOpen: make(map[int][]pdfNavigationAction), PageOpen: make(map[int][]pdfNavigationAction),
pageIndex: make(map[string]int, len(pages)),
bookmarks: make(map[string]Dest),
source: renderer.Reader, source: renderer.Reader,
} }
if doc != nil && (doc.Attachments.Path != "" || len(doc.Attachments.Attachment) != 0) { if doc != nil && (doc.Attachments.Path != "" || len(doc.Attachments.Attachment) != 0) {
@@ -188,11 +196,18 @@ func newPDFNavigation(renderer *Renderer, doc *Document, pages []RenderDocumentP
} }
navigation.nativeWrite = len(attachments) != 0 navigation.nativeWrite = len(attachments) != 0
} }
pageIndex := make(map[string]int, len(pages)) pageIndex := navigation.pageIndex
for i, page := range pages { for i, page := range pages {
pageIndex[page.Content.ID] = i if err := ctx.Err(); err != nil {
return nil, err
} }
bookmarks := make(map[string]Dest) id := page.Source.ID
if page.Content != nil {
id = page.Content.ID
}
pageIndex[id] = i
}
bookmarks := navigation.bookmarks
if doc != nil { if doc != nil {
for _, bookmark := range doc.Bookmarks.Bookmark { for _, bookmark := range doc.Bookmarks.Bookmark {
bookmarks[bookmark.Name] = bookmark.Dest bookmarks[bookmark.Name] = bookmark.Dest
@@ -201,28 +216,32 @@ func newPDFNavigation(renderer *Renderer, doc *Document, pages []RenderDocumentP
return nil, err return nil, err
} }
} }
for i, page := range pages { return navigation, nil
for _, source := range renderer.pageOpenActionSources(page.Content) { }
if err := navigation.addLifecycleActions(renderer.outputContext(), source, i, bookmarks, pageIndex, pages); err != nil {
return nil, err // 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.Content) sources := renderer.pageActionSources(page)
if renderer.RenderAnnotations { if renderer.RenderAnnotations {
sources = append(sources, renderer.annotationActionSources(renderer.Reader.Annots[page.Content.ID])...) sources = append(sources, renderer.annotationActionSources(renderer.Reader.Annots[page.ID])...)
} }
for _, source := range sources { for _, source := range sources {
if err := navigation.addActions(renderer, i, source, bookmarks, pageIndex, pages); err != nil { if err := n.addActions(renderer, index, source, n.bookmarks, n.pageIndex, pages); err != nil {
return nil, err return err
} }
} }
} return ctx.Err()
if doc != nil {
if err := navigation.addOutlines(doc.Outlines.OutlineElem, 0, bookmarks, pageIndex, pages); err != nil {
return nil, err
}
}
return navigation, nil
} }
// actionTarget 转换动作及资源引用,选页未包含的目标不写入 // actionTarget 转换动作及资源引用,选页未包含的目标不写入
@@ -298,17 +317,12 @@ func (n *pdfNavigation) addOutlines(outlines []OutlineElem, level int, bookmarks
return nil return nil
} }
// apply 生成链接及大纲结构,内部目标在原生写入时关联 // apply 生成当前页链接,内部目标在原生写入时关联
// 入参: renderer PDF渲染器, page 页面索引 // 入参: renderer PDF渲染器, page 页面索引
func (n *pdfNavigation) apply(renderer *pdf.PDF, page int) { func (n *pdfNavigation) apply(renderer *pdf.PDF, page int) {
for _, link := range n.Link[page] { for _, link := range n.Link[page] {
renderer.AddLink("", link.Rect) renderer.AddLink("", link.Rect)
} }
if page == 0 {
for index, outline := range n.Outline {
renderer.AddOutline(strconv.Itoa(index), outline.Level, 0)
}
}
} }
// prepareRewrite 关联输出页引用并准备附件、事件、链接及先序大纲的替换 // prepareRewrite 关联输出页引用并准备附件、事件、链接及先序大纲的替换
+12 -1
View File
@@ -156,7 +156,7 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64
textRun = r.pageText.addRun(obj) textRun = r.pageText.addRun(obj)
} }
if ff == nil { if ff == nil {
if textRun != nil { if textRun != nil && !r.textContentOnly {
box, _ := ParseBox(obj.Boundary) box, _ := ParseBox(obj.Boundary)
boundaryCTM := TranslationMatrix(box.X, box.Y) boundaryCTM := TranslationMatrix(box.X, box.Y)
if parentCTM != nil { 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))) textRun.Boxes = make([]Box, len([]rune(text)))
} }
} }
if r.textOnly && r.textContentOnly {
ctx.Pop()
return
}
glyphTransforms := r.textObjectGlyphTransforms(fontID, obj) glyphTransforms := r.textObjectGlyphTransforms(fontID, obj)
hasUnderline := strings.Contains(obj.Decoration, "Underline") hasUnderline := strings.Contains(obj.Decoration, "Underline")
_, shadedFill := fillPaint.(canvas.Gradient) _, 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) positioned := textCodePositioned(tc, positioner.xs, positioner.ys)
drawAsPath := embeddedFont || face.FauxBold > 0 || positioned || fillClip != nil || shadedFill || fillPattern != nil || shouldStroke drawAsPath := embeddedFont || face.FauxBold > 0 || positioned || fillClip != nil || shadedFill || fillPattern != nil || shouldStroke
for i, glyph := range glyphs { 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 str := glyph.Text
drawAsGlyphPath := drawAsPath || glyph.GlyphID >= 0 drawAsGlyphPath := drawAsPath || glyph.GlyphID >= 0
if !drawAsGlyphPath { if !drawAsGlyphPath {
+39 -6
View File
@@ -96,10 +96,33 @@ func (d *textPageTokens) Token() (xml.Token, error) {
// 入参: page 页面内容 // 入参: page 页面内容
// 返回: *PageText 页面文字, error 错误信息 // 返回: *PageText 页面文字, error 错误信息
func (r *Renderer) PageText(page *PageContent) (*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 { if r.backends.Compiler == nil {
return nil, fmt.Errorf("%w: page text", ErrBackendUnavailable) 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 按页面索引提取文字,避免解析独立路径和图片图元 // PageTextByIndex 按页面索引提取文字,避免解析独立路径和图片图元
@@ -110,6 +133,16 @@ func (r *Renderer) PageTextByIndex(index int) (*PageText, error) {
if ctx == nil { if ctx == nil {
ctx = r.outputContext() 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) doc, err := r.Reader.DocContext(ctx)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -121,7 +154,7 @@ func (r *Renderer) PageTextByIndex(index int) (*PageText, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
return r.PageText(page) return r.PageTextContext(ctx, page)
} }
// String 按文本对象顺序合并原文,以换行分隔非空对象 // String 按文本对象顺序合并原文,以换行分隔非空对象
@@ -162,7 +195,7 @@ func (p *PageText) SearchEach(ctx context.Context, query string, visit func(Text
} }
// SearchText 逐页搜索原文并立即返回匹配,不保留整本文字或识别图像 // SearchText 逐页搜索原文并立即返回匹配,不保留整本文字或识别图像
// 入参: ctx 取消上下文,页面提取之间及匹配时检查, query 搜索文字, visit 零基页码和匹配访问函数, indices 页码,省略则全部,按原页序去重 // 入参: ctx 取消上下文,页面读取、文字提取及匹配时检查, query 搜索文字, visit 零基页码和匹配访问函数, indices 页码,省略则全部,按原页序去重
// 返回: error 页面提取、取消或访问错误 // 返回: error 页面提取、取消或访问错误
func (r *Renderer) SearchText(ctx context.Context, query string, visit func(int, TextMatch) error, indices ...int) error { func (r *Renderer) SearchText(ctx context.Context, query string, visit func(int, TextMatch) error, indices ...int) error {
if err := ctx.Err(); err != nil { if err := ctx.Err(); err != nil {
@@ -171,11 +204,11 @@ func (r *Renderer) SearchText(ctx context.Context, query string, visit func(int,
if query == "" { if query == "" {
return nil return nil
} }
count, err := r.Reader.PageCount() doc, err := r.Reader.DocContext(ctx)
if err != nil { if err != nil {
return err return err
} }
indices, err = exportPageIndices(count, indices) indices, err = exportPageIndices(len(doc.Pages.Page), indices)
if err != nil { if err != nil {
return err return err
} }
@@ -184,7 +217,7 @@ func (r *Renderer) SearchText(ctx context.Context, query string, visit func(int,
if err := ctx.Err(); err != nil { if err := ctx.Err(); err != nil {
return err return err
} }
text, err := r.PageTextByIndex(index) text, err := r.PageTextByIndexContext(ctx, index)
if err != nil { if err != nil {
return fmt.Errorf("failed to search page %d: %w", index+1, err) return fmt.Errorf("failed to search page %d: %w", index+1, err)
} }