mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 13:56:32 +08:00
1 parent
7df35ac122
commit
489db301c5
15 files changed
+304
-75
No files matched your search
@@ -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),
|
||||
}
|
||||
|
||||
+9
-1
@@ -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()
|
||||
}
|
||||
|
||||
+24
-2
@@ -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 渲染选项
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
+1
-1
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
+80
-16
@@ -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()
|
||||
}
|
||||
+39
-2
@@ -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)
|
||||
}
|
||||
|
||||
+5
-4
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 关联输出页引用并准备附件、事件、链接及先序大纲的替换
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+39
-6
@@ -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)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user