From 155f5c7299b3b10aab4a8b93ba515bd89ba018ed Mon Sep 17 00:00:00 2001 From: xiaoqidun Date: Thu, 9 Jul 2026 12:17:31 +0800 Subject: [PATCH] =?UTF-8?q?refactor(=E9=87=8D=E6=9E=84=E4=BB=A3=E7=A0=81):?= =?UTF-8?q?=20=E6=8B=86=E5=88=86=E4=BB=A3=E7=A0=81=EF=BC=8C=E6=8F=90?= =?UTF-8?q?=E5=8D=87=E4=BB=A3=E7=A0=81=E8=B4=A8=E9=87=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ofdgo_renderer.go | 1046 ----------------- ...render_image.go => ofdgo_renderer_image.go | 0 ofdgo_renderer_layer.go | 247 ++++ ofdgo_renderer_output.go | 134 +++ ofdgo_renderer_path.go | 481 ++++++++ ofdgo_renderer_stamp.go | 102 ++ ofdgo_renderer_text.go | 248 ++++ 7 files changed, 1212 insertions(+), 1046 deletions(-) rename ofdgo_render_image.go => ofdgo_renderer_image.go (100%) create mode 100644 ofdgo_renderer_layer.go create mode 100644 ofdgo_renderer_output.go create mode 100644 ofdgo_renderer_path.go create mode 100644 ofdgo_renderer_stamp.go create mode 100644 ofdgo_renderer_text.go diff --git a/ofdgo_renderer.go b/ofdgo_renderer.go index d4ef788..ecb5354 100644 --- a/ofdgo_renderer.go +++ b/ofdgo_renderer.go @@ -15,20 +15,10 @@ package ofdgo import ( - "bytes" "fmt" - "image" - "image/color" - "io" "io/fs" - "math" - "strconv" - "strings" "github.com/tdewolff/canvas" - "github.com/tdewolff/canvas/renderers" - "github.com/tdewolff/canvas/renderers/pdf" - "github.com/tdewolff/canvas/renderers/rasterizer" ) // Renderer 渲染器实现 @@ -155,339 +145,6 @@ func (r *Renderer) RenderPageByIndex(index int) (*canvas.Canvas, error) { return r.RenderPage(page) } -// RenderToImage 渲染为光栅图 -// 入参: page 页面内容 -// 返回: image.Image 图像对象, error 错误信息 -func (r *Renderer) RenderToImage(page *PageContent) (image.Image, error) { - c, err := r.RenderPage(page) - if err != nil { - return nil, err - } - dpmm := r.DPI / 25.4 - return rasterizer.Draw(c, canvas.DPMM(dpmm), canvas.DefaultColorSpace), nil -} - -// RenderToSVG 渲染为SVG -// 入参: page 页面内容, writer 输出流 -// 返回: error 错误信息 -func (r *Renderer) RenderToSVG(page *PageContent, writer io.Writer) error { - c, err := r.RenderPage(page) - if err != nil { - return err - } - return c.Write(writer, renderers.SVG()) -} - -// replacePDFProducer 替换PDF的Producer属性 -// 入参: data PDF字节数据 -// 返回: []byte 替换后的PDF字节数据 -func replacePDFProducer(data []byte) []byte { - old := []byte("/Producer(tdewolff/canvas)") - idx := bytes.LastIndex(data, old) - if idx < 0 { - return data - } - dst := []byte("/Producer(xiaoqidun/ofdgo)") - result := make([]byte, 0, len(data)-len(old)+len(dst)) - return append(append(append(result, data[:idx]...), dst...), data[idx+len(old):]...) -} - -// RenderToPDF 渲染为PDF -// 入参: page 页面内容, writer 输出流 -// 返回: error 错误信息 -func (r *Renderer) RenderToPDF(page *PageContent, writer io.Writer) error { - c, err := r.RenderPage(page) - if err != nil { - return err - } - var buf bytes.Buffer - p := pdf.New(&buf, c.W, c.H, nil) - p.SetInfo("", "", "", "", "xiaoqidun/ofdgo") - c.RenderTo(p) - if err := p.Close(); err != nil { - return err - } - _, err = writer.Write(replacePDFProducer(buf.Bytes())) - return err -} - -// RenderToEPS 渲染为EPS -// 入参: page 页面内容, writer 输出流 -// 返回: error 错误信息 -func (r *Renderer) RenderToEPS(page *PageContent, writer io.Writer) error { - c, err := r.RenderPage(page) - if err != nil { - return err - } - return c.Write(writer, renderers.EPS()) -} - -// RenderToMultiPagePDF 将整个文档导出为多页PDF -// 入参: writer 输出流 -// 返回: error 错误信息 -func (r *Renderer) RenderToMultiPagePDF(writer io.Writer) error { - doc, err := r.Reader.Doc() - if err != nil { - return err - } - if len(doc.Pages.Page) == 0 { - return fmt.Errorf("no pages found") - } - var buf bytes.Buffer - var p *pdf.PDF - for _, pgRef := range doc.Pages.Page { - page, err := r.Reader.PageContent(pgRef) - if err != nil { - continue - } - c, err := r.RenderPage(page) - if err != nil { - continue - } - if p == nil { - p = pdf.New(&buf, c.W, c.H, nil) - p.SetInfo("", "", "", "", "xiaoqidun/ofdgo") - } else { - p.NewPage(c.W, c.H) - } - c.RenderTo(p) - } - if p == nil { - return fmt.Errorf("failed to render any page") - } - if err := p.Close(); err != nil { - return err - } - _, err = writer.Write(replacePDFProducer(buf.Bytes())) - return err -} - -// renderAnnotations 渲染页面注释外观 -// 入参: ctx 画布上下文, pageID 页面ID, pageH 页面高度 -func (r *Renderer) renderAnnotations(ctx *canvas.Context, pageID string, pageH float64) { - for _, annot := range r.Reader.Annots[pageID] { - if len(annot.Appearance.Objects) == 0 { - continue - } - box, _ := ParseBox(annot.Appearance.Boundary) - ctm := Matrix{a: 1, d: 1, e: box.X, f: box.Y} - for _, obj := range annot.Appearance.Objects { - r.renderObject(ctx, obj, pageH, nil, nil, 0, &ctm, false) - } - } -} - -// renderTemplate 渲染模板 -// 入参: ctx 画布上下文, templateID 模板ID, pageH 页面高度 -func (r *Renderer) renderTemplate(ctx *canvas.Context, templateID string, pageH float64) { - var tplPage *TemplatePage - for _, tp := range r.Reader.doc.CommonData.TemplatePage { - if tp.ID == templateID { - tplPage = &tp - break - } - } - if tplPage == nil { - return - } - tplContent, err := r.Reader.PageContent(Page{BaseLoc: tplPage.BaseLoc}) - if err != nil { - return - } - if tplContent.Content.Layer != nil { - for _, layer := range tplContent.Content.Layer { - r.renderLayer(ctx, layer, pageH, nil, nil, 0, nil) - } - } -} - -// renderLayer 渲染图层 -// 入参: ctx 画布上下文, layer 图层对象, pageH 页面高度, defaultFill 默认填充色, defaultStroke 默认描边色, defaultLW 默认线宽, parentCTM 父级CTM -func (r *Renderer) renderLayer(ctx *canvas.Context, layer Layer, pageH float64, defaultFill, defaultStroke color.Color, defaultLW float64, parentCTM *Matrix) { - defaultFill, defaultStroke, defaultLW = r.drawParamDefaults(layer.DrawParam, defaultFill, defaultStroke, defaultLW) - if len(layer.Objects) > 0 { - for _, obj := range layer.Objects { - r.renderObject(ctx, obj, pageH, defaultFill, defaultStroke, defaultLW, parentCTM, false) - } - return - } - for _, textObj := range layer.TextObject { - r.renderText(ctx, textObj, pageH, defaultFill, defaultStroke, parentCTM, false) - } - for _, pathObj := range layer.PathObject { - r.renderPath(ctx, pathObj, pageH, defaultFill, defaultStroke, defaultLW, parentCTM, false) - } - for _, imgObj := range layer.ImageObject { - r.renderImage(ctx, imgObj, pageH, parentCTM, false) - } - for _, cgu := range layer.CompositeGraphicUnit { - r.renderCompositeGraphicUnit(ctx, cgu, pageH, defaultFill, defaultStroke, defaultLW, parentCTM, false) - } -} - -// renderCompositeGraphicUnit 渲染复合图元 -// 入参: ctx 画布上下文, cgu 复合图元对象, pageH 页面高度, defaultFill 默认填充色, defaultStroke 默认描边色, defaultLW 默认线宽, parentCTM 父级CTM, boundaryInCTM 边界是否参与CTM变换 -func (r *Renderer) renderCompositeGraphicUnit(ctx *canvas.Context, cgu CompositeGraphicUnit, pageH float64, defaultFill, defaultStroke color.Color, defaultLW float64, parentCTM *Matrix, boundaryInCTM bool) { - if cgu.Visible != nil && !*cgu.Visible { - return - } - ctx.Push() - currentCTM := NewMatrix(cgu.CTM) - if parentCTM != nil { - currentCTM = parentCTM.Multiply(currentCTM) - } - if cgu.ResourceID != "" { - if ref, ok := r.CompositeGraphicUnits[cgu.ResourceID]; ok { - refCopy := *ref - refCopy.Alpha = mergeAlpha(refCopy.Alpha, cgu.Alpha) - r.renderCompositeGraphicUnit(ctx, refCopy, pageH, defaultFill, defaultStroke, defaultLW, ¤tCTM, true) - } - } - defaultFill, defaultStroke, defaultLW = r.drawParamDefaults(cgu.DrawParam, defaultFill, defaultStroke, defaultLW) - if len(cgu.Objects) > 0 { - for _, obj := range cgu.Objects { - obj = mergeGraphicObjectAlpha(obj, cgu.Alpha) - r.renderObject(ctx, obj, pageH, defaultFill, defaultStroke, defaultLW, ¤tCTM, boundaryInCTM) - } - ctx.Pop() - return - } - for _, imgObj := range cgu.ImageObject { - imgObj.Alpha = mergeAlpha(imgObj.Alpha, cgu.Alpha) - r.renderImage(ctx, imgObj, pageH, ¤tCTM, boundaryInCTM) - } - for _, pathObj := range cgu.PathObject { - pathObj.Alpha = mergeAlpha(pathObj.Alpha, cgu.Alpha) - r.renderPath(ctx, pathObj, pageH, defaultFill, defaultStroke, defaultLW, ¤tCTM, boundaryInCTM) - } - for _, textObj := range cgu.TextObject { - textObj.Alpha = mergeAlpha(textObj.Alpha, cgu.Alpha) - r.renderText(ctx, textObj, pageH, defaultFill, defaultStroke, ¤tCTM, boundaryInCTM) - } - for _, subCgu := range cgu.CompositeGraphicUnit { - subCgu.Alpha = mergeAlpha(subCgu.Alpha, cgu.Alpha) - r.renderCompositeGraphicUnit(ctx, subCgu, pageH, defaultFill, defaultStroke, defaultLW, ¤tCTM, boundaryInCTM) - } - ctx.Pop() -} - -// renderObject 渲染图形对象 -// 入参: ctx 画布上下文, obj 图形对象, pageH 页面高度, defaultFill 默认填充色, defaultStroke 默认描边色, defaultLW 默认线宽, parentCTM 父级CTM, boundaryInCTM 边界是否参与CTM变换 -func (r *Renderer) renderObject(ctx *canvas.Context, obj GraphicObject, pageH float64, defaultFill, defaultStroke color.Color, defaultLW float64, parentCTM *Matrix, boundaryInCTM bool) { - switch obj.Type { - case "TextObject": - r.renderText(ctx, obj.TextObject, pageH, defaultFill, defaultStroke, parentCTM, boundaryInCTM) - case "PathObject": - r.renderPath(ctx, obj.PathObject, pageH, defaultFill, defaultStroke, defaultLW, parentCTM, boundaryInCTM) - case "ImageObject": - r.renderImage(ctx, obj.ImageObject, pageH, parentCTM, boundaryInCTM) - case "CompositeGraphicUnit", "CompositeObject": - r.renderCompositeGraphicUnit(ctx, obj.CompositeGraphicUnit, pageH, defaultFill, defaultStroke, defaultLW, parentCTM, boundaryInCTM) - } -} - -// mergeGraphicObjectAlpha 合并图形对象透明度 -// 入参: obj 图形对象, alpha 父级透明度 -// 返回: GraphicObject 合并后的图形对象 -func mergeGraphicObjectAlpha(obj GraphicObject, alpha *int) GraphicObject { - if alpha == nil { - return obj - } - switch obj.Type { - case "TextObject": - obj.TextObject.Alpha = mergeAlpha(obj.TextObject.Alpha, alpha) - case "PathObject": - obj.PathObject.Alpha = mergeAlpha(obj.PathObject.Alpha, alpha) - case "ImageObject": - obj.ImageObject.Alpha = mergeAlpha(obj.ImageObject.Alpha, alpha) - case "CompositeGraphicUnit", "CompositeObject": - obj.CompositeGraphicUnit.Alpha = mergeAlpha(obj.CompositeGraphicUnit.Alpha, alpha) - } - return obj -} - -// drawParamDefaults 合并绘制参数默认样式 -// 入参: id 绘制参数ID, defaultFill 默认填充色, defaultStroke 默认描边色, defaultLW 默认线宽 -// 返回: color.Color 默认填充色, color.Color 默认描边色, float64 默认线宽 -func (r *Renderer) drawParamDefaults(id string, defaultFill, defaultStroke color.Color, defaultLW float64) (color.Color, color.Color, float64) { - if id == "" { - return defaultFill, defaultStroke, defaultLW - } - dp := r.getDrawParam(id, nil) - if dp == nil { - return defaultFill, defaultStroke, defaultLW - } - if dp.LineWidth > 0 { - defaultLW = dp.LineWidth - } - if dp.FillColor != nil { - defaultFill = parseFillColor(dp.FillColor) - } - if dp.StrokeColor != nil { - defaultStroke = parseStrokeColor(dp.StrokeColor) - } - return defaultFill, defaultStroke, defaultLW -} - -// getDrawParam 获取绘制参数逻辑 -// 入参: id 参数ID, visited 访问记录 -// 返回: *DrawParam 绘制参数 -func (r *Renderer) getDrawParam(id string, visited map[string]bool) *DrawParam { - if visited == nil { - visited = make(map[string]bool) - } - if visited[id] { - return nil - } - visited[id] = true - if dp, ok := r.DrawParams[id]; ok { - if dp.Relative != "" { - base := r.getDrawParam(dp.Relative, visited) - if base == nil { - return dp - } - merged := *base - if dp.LineWidth > 0 { - merged.LineWidth = dp.LineWidth - } - if dp.Join != "" { - merged.Join = dp.Join - } - if dp.Cap != "" { - merged.Cap = dp.Cap - } - if dp.DashPattern != "" { - merged.DashPattern = dp.DashPattern - merged.DashOffset = dp.DashOffset - } - if dp.MiterLimit > 0 { - merged.MiterLimit = dp.MiterLimit - } - if dp.FillColor != nil { - merged.FillColor = dp.FillColor - } - if dp.StrokeColor != nil { - merged.StrokeColor = dp.StrokeColor - } - if dp.Font != "" { - merged.Font = dp.Font - } - if dp.Size > 0 { - merged.Size = dp.Size - } - if dp.Weight > 0 { - merged.Weight = dp.Weight - } - if dp.Italic { - merged.Italic = dp.Italic - } - return &merged - } - return dp - } - return nil -} - // initCommon 初始化公共资源 func (r *Renderer) initCommon() { r.fontFamily = canvas.NewFontFamily("default") @@ -510,706 +167,3 @@ func (r *Renderer) childRenderer(reader *Reader) *Renderer { } return NewRenderer(reader, opts...) } - -// renderPath 渲染路径 -// 入参: ctx 画布上下文, obj 路径对象, pageH 页面高度, defaultFill 默认填充色, defaultStroke 默认描边色, defaultLW 默认线宽, parentCTM 父级CTM, boundaryInCTM 边界是否参与CTM变换 -func (r *Renderer) renderPath(ctx *canvas.Context, obj PathObject, pageH float64, defaultFill, defaultStroke color.Color, defaultLW float64, parentCTM *Matrix, boundaryInCTM bool) { - if obj.Visible != nil && !*obj.Visible { - return - } - ctx.Push() - bx, by := 0.0, 0.0 - if obj.Boundary != "" { - if box, err := ParseBox(obj.Boundary); err == nil { - bx, by = box.X, box.Y - } - } - ctm := NewMatrix(obj.CTM) - if parentCTM != nil { - ctm = parentCTM.Multiply(ctm) - } - fillColor, strokeColor := colorWithAlpha(defaultFill, obj.Alpha), colorWithAlpha(defaultStroke, obj.Alpha) - var fillPaint any = fillColor - var strokePaint any = strokeColor - var fillPattern *Pattern - var fillPatternColor color.Color - lineWidth := defaultLW - if lineWidth == 0 { - lineWidth = 0.353 - } - lineCap := canvas.ButtCap - lineJoin := canvas.MiterJoin - dashOffset := 0.0 - var dashPattern []float64 - if obj.DrawParam != "" { - if dp := r.getDrawParam(obj.DrawParam, nil); dp != nil { - if dp.LineWidth > 0 { - lineWidth = dp.LineWidth - } - if dp.FillColor != nil { - fillColorNode := withFillAlpha(dp.FillColor, obj.Alpha) - fillPattern = fillColorNode.Pattern - fillPatternColor = patternColor(fillColorNode) - fillColor = parseFillColor(fillColorNode) - fillPaint = parseFillPaint(fillColorNode, bx, by, pageH, 0, 0) - } - if dp.StrokeColor != nil { - strokeColorNode := withStrokeAlpha(dp.StrokeColor, obj.Alpha) - strokeColor = parseStrokeColor(strokeColorNode) - strokePaint = parseStrokePaint(strokeColorNode, bx, by, pageH, 0, 0) - } - if dp.Cap == "Round" { - lineCap = canvas.RoundCap - } else if dp.Cap == "Square" { - lineCap = canvas.SquareCap - } - if dp.Join == "Round" { - lineJoin = canvas.RoundJoin - } else if dp.Join == "Bevel" { - lineJoin = canvas.BevelJoin - } - if dp.DashPattern != "" { - dashPattern = parseFloats(dp.DashPattern) - dashOffset = dp.DashOffset - } - } - } - if obj.LineWidth > 0 { - lineWidth = obj.LineWidth - } - if obj.FillColor != nil { - fillColorNode := withFillAlpha(obj.FillColor, obj.Alpha) - fillPattern = fillColorNode.Pattern - fillPatternColor = patternColor(fillColorNode) - fillColor = parseFillColor(fillColorNode) - fillPaint = parseFillPaint(fillColorNode, bx, by, pageH, 0, 0) - } - if obj.StrokeColor != nil { - strokeColorNode := withStrokeAlpha(obj.StrokeColor, obj.Alpha) - strokeColor = parseStrokeColor(strokeColorNode) - strokePaint = parseStrokePaint(strokeColorNode, bx, by, pageH, 0, 0) - } - if obj.Cap != "" { - if obj.Cap == "Round" { - lineCap = canvas.RoundCap - } else if obj.Cap == "Square" { - lineCap = canvas.SquareCap - } else { - lineCap = canvas.ButtCap - } - } - if obj.Join != "" { - if obj.Join == "Round" { - lineJoin = canvas.RoundJoin - } else if obj.Join == "Bevel" { - lineJoin = canvas.BevelJoin - } else { - lineJoin = canvas.MiterJoin - } - } - if obj.DashPattern != "" { - dashPattern = parseFloats(obj.DashPattern) - dashOffset = obj.DashOffset - } - if scale := math.Sqrt(math.Abs(ctm.a*ctm.d - ctm.b*ctm.c)); scale > 0 { - lineWidth *= scale - dashOffset *= scale - for i := range dashPattern { - dashPattern[i] *= scale - } - } - p := r.buildPath(obj, pageH, ctm, boundaryInCTM) - if rectPath := r.buildTinyFillRectPath(obj, pageH, ctm, bx, by); rectPath != nil { - p = rectPath - } - clipPath := r.buildClipPath(obj.Clips, pageH, bx, by, ctm) - shouldFill := false - if obj.Fill != nil { - shouldFill = *obj.Fill - } - if fillPaint == nil { - fillPaint = fillColor - } - if shouldFill && fillPattern != nil { - fp := p - if clipPath != nil { - fp = p.Copy() - fp.Close() - fp = fp.And(clipPath) - } - r.renderPattern(ctx, fillPattern, fillPatternColor, pageH, fp, ctm, bx, by) - } else if shouldFill && fillPaint != nil { - ctx.SetFill(fillPaint) - ctx.SetStrokeColor(canvas.Transparent) - fp := p - if clipPath != nil { - fp = p.Copy() - fp.Close() - fp = fp.And(clipPath) - } - ctx.DrawPath(0, 0, fp) - } - shouldStroke := true - if obj.Stroke != nil { - shouldStroke = *obj.Stroke - } - if shouldStroke { - if strokePaint == nil { - strokePaint = strokeColor - } - if strokePaint == nil { - strokePaint = colorWithAlpha(canvas.Black, obj.Alpha) - } - ctx.SetFillColor(canvas.Transparent) - ctx.SetStroke(strokePaint) - ctx.SetStrokeWidth(lineWidth) - ctx.SetStrokeCapper(lineCap) - ctx.SetStrokeJoiner(lineJoin) - if len(dashPattern) > 0 { - ctx.SetDashes(dashOffset, dashPattern...) - } - if clipPath != nil { - sp := p.Copy() - if len(dashPattern) > 0 { - sp = sp.Dash(dashOffset, dashPattern...) - } - sp = sp.Stroke(lineWidth, lineCap, lineJoin, canvas.Tolerance) - sp = sp.And(clipPath) - ctx.SetFill(strokePaint) - ctx.SetStrokeColor(canvas.Transparent) - ctx.DrawPath(0, 0, sp) - } else { - ctx.DrawPath(0, 0, p) - } - } - ctx.Pop() -} - -// renderPattern 渲染图案填充 -// 入参: ctx 画布上下文, pattern 图案对象, defaultColor 默认颜色, pageH 页面高度, clip 填充区域, parentCTM 父级CTM, bx 边界X坐标, by 边界Y坐标 -func (r *Renderer) renderPattern(ctx *canvas.Context, pattern *Pattern, defaultColor color.Color, pageH float64, clip *canvas.Path, parentCTM Matrix, bx, by float64) { - if pattern == nil || clip == nil || len(pattern.CellContent.Objects) == 0 { - return - } - xStep, yStep := pattern.XStep, pattern.YStep - if xStep == 0 { - xStep = pattern.Width - } - if yStep == 0 { - yStep = pattern.Height - } - if xStep <= 0 || yStep <= 0 { - return - } - patternCTM := TranslationMatrix(bx, by).Multiply(parentCTM).Multiply(NewMatrix(pattern.CTM)) - invCTM, ok := patternCTM.Invert() - if !ok { - return - } - bounds := clip.FastBounds() - points := [][2]float64{ - {bounds.X0, pageH - bounds.Y0}, - {bounds.X1, pageH - bounds.Y0}, - {bounds.X1, pageH - bounds.Y1}, - {bounds.X0, pageH - bounds.Y1}, - } - minX, maxX := 0.0, 0.0 - minY, maxY := 0.0, 0.0 - for i, point := range points { - x, y := invCTM.Transform(point[0], point[1]) - if i == 0 { - minX, maxX = x, x - minY, maxY = y, y - continue - } - minX = math.Min(minX, x) - maxX = math.Max(maxX, x) - minY = math.Min(minY, y) - maxY = math.Max(maxY, y) - } - startX := int(math.Floor(minX/xStep)) - 1 - endX := int(math.Ceil(maxX/xStep)) + 1 - startY := int(math.Floor(minY/yStep)) - 1 - endY := int(math.Ceil(maxY/yStep)) + 1 - for ix := startX; ix <= endX; ix++ { - for iy := startY; iy <= endY; iy++ { - tileCTM := patternCTM.Multiply(TranslationMatrix(float64(ix)*xStep, float64(iy)*yStep)) - for _, obj := range pattern.CellContent.Objects { - r.renderObject(ctx, obj, pageH, defaultColor, defaultColor, 0, &tileCTM, false) - } - } - } -} - -// renderText 渲染文本 -// 入参: ctx 画布上下文, obj 文本对象, pageH 页面高度, defaultFill 默认填充色, defaultStroke 默认描边色, parentCTM 父级CTM, boundaryInCTM 边界是否参与父级CTM -func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64, defaultFill, defaultStroke color.Color, parentCTM *Matrix, boundaryInCTM bool) { - if obj.Visible != nil && !*obj.Visible { - return - } - ctx.Push() - bx, by := 0.0, 0.0 - if obj.Boundary != "" { - if box, err := ParseBox(obj.Boundary); err == nil { - bx, by = box.X, box.Y - } - } - localCTM := NewMatrix(obj.CTM) - ctm := localCTM - if parentCTM != nil { - ctm = parentCTM.Multiply(ctm) - } - var dp *DrawParam - if obj.DrawParam != "" { - dp = r.getDrawParam(obj.DrawParam, nil) - } - sizeMM := obj.Size - if sizeMM == 0 && dp != nil && dp.Size > 0 { - sizeMM = dp.Size - } - if sizeMM == 0 { - sizeMM = 3.5 - } - if obj.VScale != 0 { - sizeMM *= obj.VScale - } - hScale := obj.HScale - if hScale == 0 { - hScale = 1 - } - useTextMatrix := hasTextMatrix(ctm) - if scale := ctm.YScale(); scale > 0 && !useTextMatrix { - sizeMM *= scale - } - sizePt := sizeMM * 2.83465 - fillColor := colorWithAlpha(defaultFill, obj.Alpha) - if fillColor == nil { - fillColor = colorWithAlpha(canvas.Black, obj.Alpha) - } - var fillPaint any = fillColor - var fillColorNode *FillColor - if dp != nil && dp.FillColor != nil { - fillColorNode = withFillAlpha(dp.FillColor, obj.Alpha) - fillColor = parseFillColor(fillColorNode) - fillPaint = parseFillPaint(fillColorNode, bx, by, pageH, 0, 0) - } - if obj.FillColor != nil { - fillColorNode = withFillAlpha(obj.FillColor, obj.Alpha) - fillColor = parseFillColor(fillColorNode) - fillPaint = parseFillPaint(fillColorNode, bx, by, pageH, 0, 0) - } - if fillPaint == nil { - fillPaint = fillColor - } - fontStyle := canvas.FontRegular - weight := obj.Weight - if weight == 0 && dp != nil && dp.Weight > 0 { - weight = dp.Weight - } - syntheticBold := weight >= 700 - italic := obj.Italic - if !italic && dp != nil && dp.Italic { - italic = true - } - if italic { - fontStyle |= canvas.FontItalic - } - fontID := r.textObjectFontID(obj) - embeddedFont := false - if of, ok := r.Reader.fontCache[fontID]; ok { - embeddedFont = of.FontFile != "" - if !embeddedFont && fontNoSyntheticBold(of.FontName, of.FamilyName) { - syntheticBold = false - } - if of.Bold { - fontStyle |= canvas.FontBold - } - if of.Italic { - fontStyle |= canvas.FontItalic - } - } - if syntheticBold { - fontStyle |= canvas.FontBold - } - ff := r.loadFont(fontID) - if ff == nil { - return - } - face := ff.Face(sizePt, fillPaint, fontStyle, canvas.FontNormal) - glyphTransforms := r.textObjectGlyphTransforms(fontID, obj) - hasUnderline := strings.Contains(obj.Decoration, "Underline") - useGlyphFillPaint := fillColorNode != nil && fillColorNode.AxialShd != nil - codePos := 0 - for _, tc := range obj.TextCode { - var runes []rune - var glyphs []textGlyph - if tc.Index != "" { - runes = r.parseIndexRunes(tc.Index, fontID) - glyphs = textRuneGlyphs(runes) - } else { - runes = textCodeRunes(tc.Value) - glyphs = textCodeGlyphs(runes, glyphTransforms, codePos) - } - dxs, dys := parseFloats(tc.DeltaX), parseFloats(tc.DeltaY) - xs, ys := parseFloats(tc.X), parseFloats(tc.Y) - drawAsPath := embeddedFont || textCodePositioned(tc, xs, ys) - cx, cy := 0.0, 0.0 - if len(xs) > 0 { - cx = xs[0] - } - if len(ys) > 0 { - cy = ys[0] - } - for i, glyph := range glyphs { - str := glyph.Text - if i < len(xs) { - cx = xs[i] - } else if i > 0 { - if dx, ok := textDelta(dxs, i-1); ok { - cx += dx - } else if len(dys) == 0 { - cx += textGlyphWidth(face, glyph) * hScale - } - } - if i < len(ys) { - cy = ys[i] - } else if i > 0 { - if dy, ok := textDelta(dys, i-1); ok { - cy += dy - } - } - var canvasX, canvasY float64 - if boundaryInCTM && parentCTM != nil { - tx, ty := localCTM.Transform(cx, cy) - tx, ty = parentCTM.Transform(tx+bx, ty+by) - canvasX, canvasY = tx, pageH-ty - } else { - tx, ty := ctm.Transform(cx, cy) - canvasX, canvasY = tx+bx, pageH-(ty+by) - } - textWidth := textGlyphWidth(face, glyph) * hScale - glyphFillPaint := fillPaint - if useGlyphFillPaint { - glyphFillPaint = parseFillPaint(fillColorNode, bx, by, pageH, canvasX, canvasY) - } - advanceLimit := textGlyphAdvanceLimit(dxs, dys, xs, i, len(glyphs), cx) - if glyphFillPaint != nil { - ctx.SetFill(glyphFillPaint) - drawGlyph := func(x, y float64) { - if drawAsPath || glyph.GlyphID >= 0 { - path, width := textGlyphPath(face, glyph) - scaleX := hScale - if advanceLimit > 0 && width*scaleX > advanceLimit { - scaleX = advanceLimit / width - } - textWidth = width * scaleX - if scaleX != 1 { - ctx.Push() - ctx.Translate(x, y) - ctx.Scale(scaleX, 1) - ctx.DrawPath(0, 0, path) - ctx.Pop() - } else { - ctx.DrawPath(x, y, path) - } - } else { - scaled := hScale != 1 - if scaled { - ctx.Push() - ctx.Translate(x, y) - ctx.Scale(hScale, 1) - x, y = 0, 0 - } - textFace := face - if useGlyphFillPaint { - textFace = ff.Face(sizePt, glyphFillPaint, fontStyle, canvas.FontNormal) - } - text := canvas.NewTextLine(textFace, str, canvas.Left) - ctx.DrawText(x, y, text) - if scaled { - ctx.Pop() - } - } - } - if useTextMatrix { - ctx.Push() - ctx.Translate(canvasX, canvasY) - ctx.ComposeView(textMatrix(ctm)) - drawGlyph(0, 0) - if hasUnderline { - uw := sizeMM * 0.05 - ctx.SetStrokeWidth(uw) - ctx.SetStrokeColor(fillColor) - off := sizeMM * 0.1 - ctx.MoveTo(0, -off) - ctx.LineTo(textWidth, -off) - ctx.Stroke() - } - ctx.Pop() - continue - } - drawGlyph(canvasX, canvasY) - } - if hasUnderline { - uw := sizeMM * 0.05 - ctx.SetStrokeWidth(uw) - ctx.SetStrokeColor(fillColor) - off := sizeMM * 0.1 - ctx.MoveTo(canvasX, canvasY-off) - ctx.LineTo(canvasX+textWidth, canvasY-off) - ctx.Stroke() - } - } - codePos += len(runes) - } - ctx.Pop() -} - -// renderStamp 渲染印章 -// 入参: ctx 画布上下文, s 印章对象, pageH 页面高度 -func (r *Renderer) renderStamp(ctx *canvas.Context, s Stamp, pageH float64) { - x, y, w, h := s.Box.X, s.Box.Y, s.Box.W, s.Box.H - screenY := pageH - (y + h) - if s.Type == "ofd" && len(s.Data) > 0 { - reader, err := NewReader(bytes.NewReader(s.Data), int64(len(s.Data))) - if err == nil { - defer reader.Close() - doc, err := reader.Doc() - if err == nil { - renderer := r.childRenderer(reader) - for _, pageRef := range doc.Pages.Page { - content, err := reader.PageContent(pageRef) - if err != nil { - continue - } - sealBox, err := renderer.GetPageBox(content) - if err != nil { - continue - } - ctx.Push() - ctx.Translate(x, screenY) - ctx.Scale(w/sealBox.W, h/sealBox.H) - renderer.renderPageToContext(ctx, content, false) - ctx.Pop() - } - return - } - } - } - if len(s.Data) > 0 { - img, _, err := decodeImageData(s.Data) - if err == nil { - img = stampImageWithTransparentWhite(img) - ctx.Push() - ctx.Translate(x, screenY) - ctx.Scale(w/float64(img.Bounds().Dx()), h/float64(img.Bounds().Dy())) - ctx.DrawImage(0, 0, img, canvas.DPMM(1.0)) - ctx.Pop() - return - } - } -} - -// stampImageWithTransparentWhite 处理印章图片白色底色 -// 入参: img 印章图片对象 -// 返回: image.Image 处理后的印章图片对象 -func stampImageWithTransparentWhite(img image.Image) image.Image { - if opaque, ok := img.(interface{ Opaque() bool }); ok && !opaque.Opaque() { - return img - } - bounds := img.Bounds() - out := image.NewNRGBA(bounds) - hasAlpha := false - for y := bounds.Min.Y; y < bounds.Max.Y; y++ { - for x := bounds.Min.X; x < bounds.Max.X; x++ { - c := color.NRGBAModel.Convert(img.At(x, y)).(color.NRGBA) - if c.A < 255 { - hasAlpha = true - } - out.SetNRGBA(x, y, c) - } - } - if hasAlpha { - return img - } - for y := bounds.Min.Y; y < bounds.Max.Y; y++ { - for x := bounds.Min.X; x < bounds.Max.X; x++ { - c := out.NRGBAAt(x, y) - if c.R >= 250 && c.G >= 250 && c.B >= 250 { - c.A = 0 - out.SetNRGBA(x, y, c) - } - } - } - return out -} - -// buildPath 解析路径并返回Canvas Path -// 入参: obj 路径对象, pageH 页面高度, ctm 变换矩阵, boundaryInCTM 边界是否参与CTM变换 -// 返回: *canvas.Path 路径对象 -func (r *Renderer) buildPath(obj PathObject, pageH float64, ctm Matrix, boundaryInCTM bool) *canvas.Path { - bx, by := 0.0, 0.0 - if obj.Boundary != "" { - if box, err := ParseBox(obj.Boundary); err == nil { - bx, by = box.X, box.Y - } - } - point := func(x, y float64) (float64, float64) { - if boundaryInCTM { - tx, ty := ctm.Transform(x+bx, y+by) - return tx, pageH - ty - } - tx, ty := ctm.Transform(x, y) - return tx + bx, pageH - (ty + by) - } - p := &canvas.Path{} - tokens := strings.Fields(obj.AbbreviatedData) - for i := 0; i < len(tokens); { - cmd := tokens[i] - i++ - switch cmd { - case "M", "S": - if i+1 < len(tokens) { - x, _ := strconv.ParseFloat(tokens[i], 64) - y, _ := strconv.ParseFloat(tokens[i+1], 64) - tx, ty := point(x, y) - p.MoveTo(tx, ty) - i += 2 - } - case "L": - if i+1 < len(tokens) { - x, _ := strconv.ParseFloat(tokens[i], 64) - y, _ := strconv.ParseFloat(tokens[i+1], 64) - tx, ty := point(x, y) - p.LineTo(tx, ty) - i += 2 - } - case "B": - if i+5 < len(tokens) { - x1, _ := strconv.ParseFloat(tokens[i], 64) - y1, _ := strconv.ParseFloat(tokens[i+1], 64) - x2, _ := strconv.ParseFloat(tokens[i+2], 64) - y2, _ := strconv.ParseFloat(tokens[i+3], 64) - x3, _ := strconv.ParseFloat(tokens[i+4], 64) - y3, _ := strconv.ParseFloat(tokens[i+5], 64) - tx1, ty1 := point(x1, y1) - tx2, ty2 := point(x2, y2) - tx3, ty3 := point(x3, y3) - p.CubeTo(tx1, ty1, tx2, ty2, tx3, ty3) - i += 6 - } - case "Q": - if i+3 < len(tokens) { - x1, _ := strconv.ParseFloat(tokens[i], 64) - y1, _ := strconv.ParseFloat(tokens[i+1], 64) - x2, _ := strconv.ParseFloat(tokens[i+2], 64) - y2, _ := strconv.ParseFloat(tokens[i+3], 64) - tx1, ty1 := point(x1, y1) - tx2, ty2 := point(x2, y2) - p.QuadTo(tx1, ty1, tx2, ty2) - i += 4 - } - case "A": - if i+6 < len(tokens) { - rx, _ := strconv.ParseFloat(tokens[i], 64) - ry, _ := strconv.ParseFloat(tokens[i+1], 64) - rot, _ := strconv.ParseFloat(tokens[i+2], 64) - large, _ := strconv.ParseBool(tokens[i+3]) - sweep, _ := strconv.ParseBool(tokens[i+4]) - x, _ := strconv.ParseFloat(tokens[i+5], 64) - y, _ := strconv.ParseFloat(tokens[i+6], 64) - sx := math.Hypot(ctm.a, ctm.c) - sy := math.Hypot(ctm.b, ctm.d) - ctmRot := math.Atan2(ctm.b, ctm.a) * 180 / math.Pi - tx, ty := point(x, y) - sweep = !sweep - p.ArcTo(rx*sx, ry*sy, -(rot + ctmRot), large, sweep, tx, ty) - i += 7 - } - case "C": - p.Close() - } - } - return p -} - -// buildClipPath 构建裁剪路径 -// 入参: clips 裁剪对象, pageH 页面高度, bx 边界X坐标, by 边界Y坐标, objectCTM 对象CTM -// 返回: *canvas.Path 路径对象 -func (r *Renderer) buildClipPath(clips *Clips, pageH float64, bx, by float64, objectCTM Matrix) *canvas.Path { - if clips == nil { - return nil - } - var p *canvas.Path - for _, clip := range clips.Clip { - var clipPath *canvas.Path - for _, area := range clip.Area { - areaCTM := NewMatrix(area.CTM) - if clips.TransFlag { - areaCTM = objectCTM.Multiply(areaCTM) - } - for _, pathObj := range area.Path { - ctm := areaCTM.Multiply(NewMatrix(pathObj.CTM)) - cp := r.buildPath(pathObj, pageH, ctm, true) - cp.Translate(bx, -by) - cp.Close() - if clipPath == nil { - clipPath = cp - } else { - clipPath = clipPath.Or(cp) - } - } - } - if clipPath != nil { - if p == nil { - p = clipPath - } else { - p = p.And(clipPath) - } - } - } - return p -} - -// buildTinyFillRectPath 构建微小填充矩形路径 -// 入参: obj 路径对象, pageH 页面高度, ctm 变换矩阵, bx 边界X坐标, by 边界Y坐标 -// 返回: *canvas.Path 路径对象 -func (r *Renderer) buildTinyFillRectPath(obj PathObject, pageH float64, ctm Matrix, bx, by float64) *canvas.Path { - if obj.Fill == nil || !*obj.Fill || obj.Stroke == nil || *obj.Stroke { - return nil - } - box, err := ParseBox(obj.Boundary) - if err != nil || box.W <= 0 || box.H <= 0 || box.W > 0.6 || box.H > 0.6 { - return nil - } - tokens := strings.Fields(obj.AbbreviatedData) - if len(tokens) != 11 || tokens[0] != "M" || tokens[3] != "L" || tokens[6] != "L" || tokens[9] != "L" || tokens[10] != "C" { - return nil - } - points := make([][2]float64, 0, 4) - for i := 1; i < 10; i += 3 { - x, errX := strconv.ParseFloat(tokens[i], 64) - y, errY := strconv.ParseFloat(tokens[i+1], 64) - if errX != nil || errY != nil { - return nil - } - tx, ty := ctm.Transform(x, y) - points = append(points, [2]float64{tx + bx, pageH - (ty + by)}) - } - minX, maxX := points[0][0], points[0][0] - minY, maxY := points[0][1], points[0][1] - for _, point := range points[1:] { - minX = math.Min(minX, point[0]) - maxX = math.Max(maxX, point[0]) - minY = math.Min(minY, point[1]) - maxY = math.Max(maxY, point[1]) - } - expand := math.Min(box.W, box.H) * 0.08 - p := &canvas.Path{} - p.MoveTo(minX-expand, minY-expand) - p.LineTo(maxX+expand, minY-expand) - p.LineTo(maxX+expand, maxY+expand) - p.LineTo(minX-expand, maxY+expand) - p.Close() - return p -} diff --git a/ofdgo_render_image.go b/ofdgo_renderer_image.go similarity index 100% rename from ofdgo_render_image.go rename to ofdgo_renderer_image.go diff --git a/ofdgo_renderer_layer.go b/ofdgo_renderer_layer.go new file mode 100644 index 0000000..cf16d61 --- /dev/null +++ b/ofdgo_renderer_layer.go @@ -0,0 +1,247 @@ +// Copyright 2025-2026 肖其顿 (XIAO QI DUN) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ofdgo + +import ( + "image/color" + + "github.com/tdewolff/canvas" +) + +// renderAnnotations 渲染页面注释外观 +// 入参: ctx 画布上下文, pageID 页面ID, pageH 页面高度 +func (r *Renderer) renderAnnotations(ctx *canvas.Context, pageID string, pageH float64) { + for _, annot := range r.Reader.Annots[pageID] { + if len(annot.Appearance.Objects) == 0 { + continue + } + box, _ := ParseBox(annot.Appearance.Boundary) + ctm := Matrix{a: 1, d: 1, e: box.X, f: box.Y} + for _, obj := range annot.Appearance.Objects { + r.renderObject(ctx, obj, pageH, nil, nil, 0, &ctm, false) + } + } +} + +// renderTemplate 渲染模板 +// 入参: ctx 画布上下文, templateID 模板ID, pageH 页面高度 +func (r *Renderer) renderTemplate(ctx *canvas.Context, templateID string, pageH float64) { + var tplPage *TemplatePage + for _, tp := range r.Reader.doc.CommonData.TemplatePage { + if tp.ID == templateID { + tplPage = &tp + break + } + } + if tplPage == nil { + return + } + tplContent, err := r.Reader.PageContent(Page{BaseLoc: tplPage.BaseLoc}) + if err != nil { + return + } + if tplContent.Content.Layer != nil { + for _, layer := range tplContent.Content.Layer { + r.renderLayer(ctx, layer, pageH, nil, nil, 0, nil) + } + } +} + +// renderLayer 渲染图层 +// 入参: ctx 画布上下文, layer 图层对象, pageH 页面高度, defaultFill 默认填充色, defaultStroke 默认描边色, defaultLW 默认线宽, parentCTM 父级CTM +func (r *Renderer) renderLayer(ctx *canvas.Context, layer Layer, pageH float64, defaultFill, defaultStroke color.Color, defaultLW float64, parentCTM *Matrix) { + defaultFill, defaultStroke, defaultLW = r.drawParamDefaults(layer.DrawParam, defaultFill, defaultStroke, defaultLW) + if len(layer.Objects) > 0 { + for _, obj := range layer.Objects { + r.renderObject(ctx, obj, pageH, defaultFill, defaultStroke, defaultLW, parentCTM, false) + } + return + } + for _, textObj := range layer.TextObject { + r.renderText(ctx, textObj, pageH, defaultFill, defaultStroke, parentCTM, false) + } + for _, pathObj := range layer.PathObject { + r.renderPath(ctx, pathObj, pageH, defaultFill, defaultStroke, defaultLW, parentCTM, false) + } + for _, imgObj := range layer.ImageObject { + r.renderImage(ctx, imgObj, pageH, parentCTM, false) + } + for _, cgu := range layer.CompositeGraphicUnit { + r.renderCompositeGraphicUnit(ctx, cgu, pageH, defaultFill, defaultStroke, defaultLW, parentCTM, false) + } +} + +// renderCompositeGraphicUnit 渲染复合图元 +// 入参: ctx 画布上下文, cgu 复合图元对象, pageH 页面高度, defaultFill 默认填充色, defaultStroke 默认描边色, defaultLW 默认线宽, parentCTM 父级CTM, boundaryInCTM 边界是否参与CTM变换 +func (r *Renderer) renderCompositeGraphicUnit(ctx *canvas.Context, cgu CompositeGraphicUnit, pageH float64, defaultFill, defaultStroke color.Color, defaultLW float64, parentCTM *Matrix, boundaryInCTM bool) { + if cgu.Visible != nil && !*cgu.Visible { + return + } + ctx.Push() + currentCTM := NewMatrix(cgu.CTM) + if parentCTM != nil { + currentCTM = parentCTM.Multiply(currentCTM) + } + if cgu.ResourceID != "" { + if ref, ok := r.CompositeGraphicUnits[cgu.ResourceID]; ok { + refCopy := *ref + refCopy.Alpha = mergeAlpha(refCopy.Alpha, cgu.Alpha) + r.renderCompositeGraphicUnit(ctx, refCopy, pageH, defaultFill, defaultStroke, defaultLW, ¤tCTM, true) + } + } + defaultFill, defaultStroke, defaultLW = r.drawParamDefaults(cgu.DrawParam, defaultFill, defaultStroke, defaultLW) + if len(cgu.Objects) > 0 { + for _, obj := range cgu.Objects { + obj = mergeGraphicObjectAlpha(obj, cgu.Alpha) + r.renderObject(ctx, obj, pageH, defaultFill, defaultStroke, defaultLW, ¤tCTM, boundaryInCTM) + } + ctx.Pop() + return + } + for _, imgObj := range cgu.ImageObject { + imgObj.Alpha = mergeAlpha(imgObj.Alpha, cgu.Alpha) + r.renderImage(ctx, imgObj, pageH, ¤tCTM, boundaryInCTM) + } + for _, pathObj := range cgu.PathObject { + pathObj.Alpha = mergeAlpha(pathObj.Alpha, cgu.Alpha) + r.renderPath(ctx, pathObj, pageH, defaultFill, defaultStroke, defaultLW, ¤tCTM, boundaryInCTM) + } + for _, textObj := range cgu.TextObject { + textObj.Alpha = mergeAlpha(textObj.Alpha, cgu.Alpha) + r.renderText(ctx, textObj, pageH, defaultFill, defaultStroke, ¤tCTM, boundaryInCTM) + } + for _, subCgu := range cgu.CompositeGraphicUnit { + subCgu.Alpha = mergeAlpha(subCgu.Alpha, cgu.Alpha) + r.renderCompositeGraphicUnit(ctx, subCgu, pageH, defaultFill, defaultStroke, defaultLW, ¤tCTM, boundaryInCTM) + } + ctx.Pop() +} + +// renderObject 渲染图形对象 +// 入参: ctx 画布上下文, obj 图形对象, pageH 页面高度, defaultFill 默认填充色, defaultStroke 默认描边色, defaultLW 默认线宽, parentCTM 父级CTM, boundaryInCTM 边界是否参与CTM变换 +func (r *Renderer) renderObject(ctx *canvas.Context, obj GraphicObject, pageH float64, defaultFill, defaultStroke color.Color, defaultLW float64, parentCTM *Matrix, boundaryInCTM bool) { + switch obj.Type { + case "TextObject": + r.renderText(ctx, obj.TextObject, pageH, defaultFill, defaultStroke, parentCTM, boundaryInCTM) + case "PathObject": + r.renderPath(ctx, obj.PathObject, pageH, defaultFill, defaultStroke, defaultLW, parentCTM, boundaryInCTM) + case "ImageObject": + r.renderImage(ctx, obj.ImageObject, pageH, parentCTM, boundaryInCTM) + case "CompositeGraphicUnit", "CompositeObject": + r.renderCompositeGraphicUnit(ctx, obj.CompositeGraphicUnit, pageH, defaultFill, defaultStroke, defaultLW, parentCTM, boundaryInCTM) + } +} + +// mergeGraphicObjectAlpha 合并图形对象透明度 +// 入参: obj 图形对象, alpha 父级透明度 +// 返回: GraphicObject 合并后的图形对象 +func mergeGraphicObjectAlpha(obj GraphicObject, alpha *int) GraphicObject { + if alpha == nil { + return obj + } + switch obj.Type { + case "TextObject": + obj.TextObject.Alpha = mergeAlpha(obj.TextObject.Alpha, alpha) + case "PathObject": + obj.PathObject.Alpha = mergeAlpha(obj.PathObject.Alpha, alpha) + case "ImageObject": + obj.ImageObject.Alpha = mergeAlpha(obj.ImageObject.Alpha, alpha) + case "CompositeGraphicUnit", "CompositeObject": + obj.CompositeGraphicUnit.Alpha = mergeAlpha(obj.CompositeGraphicUnit.Alpha, alpha) + } + return obj +} + +// drawParamDefaults 合并绘制参数默认样式 +// 入参: id 绘制参数ID, defaultFill 默认填充色, defaultStroke 默认描边色, defaultLW 默认线宽 +// 返回: color.Color 默认填充色, color.Color 默认描边色, float64 默认线宽 +func (r *Renderer) drawParamDefaults(id string, defaultFill, defaultStroke color.Color, defaultLW float64) (color.Color, color.Color, float64) { + if id == "" { + return defaultFill, defaultStroke, defaultLW + } + dp := r.getDrawParam(id, nil) + if dp == nil { + return defaultFill, defaultStroke, defaultLW + } + if dp.LineWidth > 0 { + defaultLW = dp.LineWidth + } + if dp.FillColor != nil { + defaultFill = parseFillColor(dp.FillColor) + } + if dp.StrokeColor != nil { + defaultStroke = parseStrokeColor(dp.StrokeColor) + } + return defaultFill, defaultStroke, defaultLW +} + +// getDrawParam 获取绘制参数逻辑 +// 入参: id 参数ID, visited 访问记录 +// 返回: *DrawParam 绘制参数 +func (r *Renderer) getDrawParam(id string, visited map[string]bool) *DrawParam { + if visited == nil { + visited = make(map[string]bool) + } + if visited[id] { + return nil + } + visited[id] = true + if dp, ok := r.DrawParams[id]; ok { + if dp.Relative != "" { + base := r.getDrawParam(dp.Relative, visited) + if base == nil { + return dp + } + merged := *base + if dp.LineWidth > 0 { + merged.LineWidth = dp.LineWidth + } + if dp.Join != "" { + merged.Join = dp.Join + } + if dp.Cap != "" { + merged.Cap = dp.Cap + } + if dp.DashPattern != "" { + merged.DashPattern = dp.DashPattern + merged.DashOffset = dp.DashOffset + } + if dp.MiterLimit > 0 { + merged.MiterLimit = dp.MiterLimit + } + if dp.FillColor != nil { + merged.FillColor = dp.FillColor + } + if dp.StrokeColor != nil { + merged.StrokeColor = dp.StrokeColor + } + if dp.Font != "" { + merged.Font = dp.Font + } + if dp.Size > 0 { + merged.Size = dp.Size + } + if dp.Weight > 0 { + merged.Weight = dp.Weight + } + if dp.Italic { + merged.Italic = dp.Italic + } + return &merged + } + return dp + } + return nil +} diff --git a/ofdgo_renderer_output.go b/ofdgo_renderer_output.go new file mode 100644 index 0000000..aab3f6e --- /dev/null +++ b/ofdgo_renderer_output.go @@ -0,0 +1,134 @@ +// Copyright 2025-2026 肖其顿 (XIAO QI DUN) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ofdgo + +import ( + "bytes" + "fmt" + "image" + "io" + + "github.com/tdewolff/canvas" + "github.com/tdewolff/canvas/renderers" + "github.com/tdewolff/canvas/renderers/pdf" + "github.com/tdewolff/canvas/renderers/rasterizer" +) + +// RenderToImage 渲染为光栅图 +// 入参: page 页面内容 +// 返回: image.Image 图像对象, error 错误信息 +func (r *Renderer) RenderToImage(page *PageContent) (image.Image, error) { + c, err := r.RenderPage(page) + if err != nil { + return nil, err + } + dpmm := r.DPI / 25.4 + return rasterizer.Draw(c, canvas.DPMM(dpmm), canvas.DefaultColorSpace), nil +} + +// RenderToSVG 渲染为SVG +// 入参: page 页面内容, writer 输出流 +// 返回: error 错误信息 +func (r *Renderer) RenderToSVG(page *PageContent, writer io.Writer) error { + c, err := r.RenderPage(page) + if err != nil { + return err + } + return c.Write(writer, renderers.SVG()) +} + +// replacePDFProducer 替换PDF的Producer属性 +// 入参: data PDF字节数据 +// 返回: []byte 替换后的PDF字节数据 +func replacePDFProducer(data []byte) []byte { + old := []byte("/Producer(tdewolff/canvas)") + idx := bytes.LastIndex(data, old) + if idx < 0 { + return data + } + dst := []byte("/Producer(xiaoqidun/ofdgo)") + result := make([]byte, 0, len(data)-len(old)+len(dst)) + return append(append(append(result, data[:idx]...), dst...), data[idx+len(old):]...) +} + +// RenderToPDF 渲染为PDF +// 入参: page 页面内容, writer 输出流 +// 返回: error 错误信息 +func (r *Renderer) RenderToPDF(page *PageContent, writer io.Writer) error { + c, err := r.RenderPage(page) + if err != nil { + return err + } + var buf bytes.Buffer + p := pdf.New(&buf, c.W, c.H, nil) + p.SetInfo("", "", "", "", "xiaoqidun/ofdgo") + c.RenderTo(p) + if err := p.Close(); err != nil { + return err + } + _, err = writer.Write(replacePDFProducer(buf.Bytes())) + return err +} + +// RenderToEPS 渲染为EPS +// 入参: page 页面内容, writer 输出流 +// 返回: error 错误信息 +func (r *Renderer) RenderToEPS(page *PageContent, writer io.Writer) error { + c, err := r.RenderPage(page) + if err != nil { + return err + } + return c.Write(writer, renderers.EPS()) +} + +// RenderToMultiPagePDF 将整个文档导出为多页PDF +// 入参: writer 输出流 +// 返回: error 错误信息 +func (r *Renderer) RenderToMultiPagePDF(writer io.Writer) error { + doc, err := r.Reader.Doc() + if err != nil { + return err + } + if len(doc.Pages.Page) == 0 { + return fmt.Errorf("no pages found") + } + var buf bytes.Buffer + var p *pdf.PDF + for _, pgRef := range doc.Pages.Page { + page, err := r.Reader.PageContent(pgRef) + if err != nil { + continue + } + c, err := r.RenderPage(page) + if err != nil { + continue + } + if p == nil { + p = pdf.New(&buf, c.W, c.H, nil) + p.SetInfo("", "", "", "", "xiaoqidun/ofdgo") + } else { + p.NewPage(c.W, c.H) + } + c.RenderTo(p) + } + if p == nil { + return fmt.Errorf("failed to render any page") + } + if err := p.Close(); err != nil { + return err + } + _, err = writer.Write(replacePDFProducer(buf.Bytes())) + return err +} diff --git a/ofdgo_renderer_path.go b/ofdgo_renderer_path.go new file mode 100644 index 0000000..137f9a3 --- /dev/null +++ b/ofdgo_renderer_path.go @@ -0,0 +1,481 @@ +// Copyright 2025-2026 肖其顿 (XIAO QI DUN) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ofdgo + +import ( + "image/color" + "math" + "strconv" + "strings" + + "github.com/tdewolff/canvas" +) + +const defaultPathLineWidth = 0.353 + +type pathStyle struct { + fillColor color.Color + strokeColor color.Color + fillPaint any + strokePaint any + fillPattern *Pattern + fillPatternColor color.Color + lineWidth float64 + lineCap canvas.Capper + lineJoin canvas.Joiner + dashOffset float64 + dashPattern []float64 +} + +// newPathStyle 创建路径样式 +// 入参: defaultFill 默认填充色, defaultStroke 默认描边色, defaultLW 默认线宽, alpha 对象透明度 +// 返回: pathStyle 路径样式 +func newPathStyle(defaultFill, defaultStroke color.Color, defaultLW float64, alpha *int) pathStyle { + style := pathStyle{ + fillColor: colorWithAlpha(defaultFill, alpha), + strokeColor: colorWithAlpha(defaultStroke, alpha), + lineWidth: defaultLW, + lineCap: canvas.ButtCap, + lineJoin: canvas.MiterJoin, + } + if style.lineWidth == 0 { + style.lineWidth = defaultPathLineWidth + } + style.fillPaint = style.fillColor + style.strokePaint = style.strokeColor + return style +} + +// applyFillColor 应用填充颜色 +// 入参: fill 填充颜色, bx 边界X坐标, by 边界Y坐标, pageH 页面高度, alpha 对象透明度 +func (s *pathStyle) applyFillColor(fill *FillColor, bx, by, pageH float64, alpha *int) { + fillColorNode := withFillAlpha(fill, alpha) + s.fillPattern = fillColorNode.Pattern + s.fillPatternColor = patternColor(fillColorNode) + s.fillColor = parseFillColor(fillColorNode) + s.fillPaint = parseFillPaint(fillColorNode, bx, by, pageH, 0, 0) +} + +// applyStrokeColor 应用描边颜色 +// 入参: stroke 描边颜色, bx 边界X坐标, by 边界Y坐标, pageH 页面高度, alpha 对象透明度 +func (s *pathStyle) applyStrokeColor(stroke *StrokeColor, bx, by, pageH float64, alpha *int) { + strokeColorNode := withStrokeAlpha(stroke, alpha) + s.strokeColor = parseStrokeColor(strokeColorNode) + s.strokePaint = parseStrokePaint(strokeColorNode, bx, by, pageH, 0, 0) +} + +// pathLineCap 转换线帽样式 +// 入参: cap 线帽名称, fallback 默认线帽 +// 返回: canvas.Capper 线帽样式 +func pathLineCap(cap string, fallback canvas.Capper) canvas.Capper { + switch cap { + case "Round": + return canvas.RoundCap + case "Square": + return canvas.SquareCap + } + return fallback +} + +// pathLineJoin 转换线连接样式 +// 入参: join 线连接名称, fallback 默认线连接 +// 返回: canvas.Joiner 线连接样式 +func pathLineJoin(join string, fallback canvas.Joiner) canvas.Joiner { + switch join { + case "Round": + return canvas.RoundJoin + case "Bevel": + return canvas.BevelJoin + } + return fallback +} + +// applyDrawParam 应用绘制参数样式 +// 入参: dp 绘制参数, bx 边界X坐标, by 边界Y坐标, pageH 页面高度, alpha 对象透明度 +func (s *pathStyle) applyDrawParam(dp *DrawParam, bx, by, pageH float64, alpha *int) { + if dp.LineWidth > 0 { + s.lineWidth = dp.LineWidth + } + if dp.FillColor != nil { + s.applyFillColor(dp.FillColor, bx, by, pageH, alpha) + } + if dp.StrokeColor != nil { + s.applyStrokeColor(dp.StrokeColor, bx, by, pageH, alpha) + } + if dp.Cap != "" { + s.lineCap = pathLineCap(dp.Cap, s.lineCap) + } + if dp.Join != "" { + s.lineJoin = pathLineJoin(dp.Join, s.lineJoin) + } + if dp.DashPattern != "" { + s.dashPattern = parseFloats(dp.DashPattern) + s.dashOffset = dp.DashOffset + } +} + +// applyPathObject 应用路径对象样式 +// 入参: obj 路径对象, bx 边界X坐标, by 边界Y坐标, pageH 页面高度 +func (s *pathStyle) applyPathObject(obj PathObject, bx, by, pageH float64) { + if obj.LineWidth > 0 { + s.lineWidth = obj.LineWidth + } + if obj.FillColor != nil { + s.applyFillColor(obj.FillColor, bx, by, pageH, obj.Alpha) + } + if obj.StrokeColor != nil { + s.applyStrokeColor(obj.StrokeColor, bx, by, pageH, obj.Alpha) + } + if obj.Cap != "" { + s.lineCap = pathLineCap(obj.Cap, canvas.ButtCap) + } + if obj.Join != "" { + s.lineJoin = pathLineJoin(obj.Join, canvas.MiterJoin) + } + if obj.DashPattern != "" { + s.dashPattern = parseFloats(obj.DashPattern) + s.dashOffset = obj.DashOffset + } +} + +// scale 应用路径变换缩放 +// 入参: ctm 变换矩阵 +func (s *pathStyle) scale(ctm Matrix) { + if scale := math.Sqrt(math.Abs(ctm.a*ctm.d - ctm.b*ctm.c)); scale > 0 { + s.lineWidth *= scale + s.dashOffset *= scale + for i := range s.dashPattern { + s.dashPattern[i] *= scale + } + } +} + +// renderPath 渲染路径 +// 入参: ctx 画布上下文, obj 路径对象, pageH 页面高度, defaultFill 默认填充色, defaultStroke 默认描边色, defaultLW 默认线宽, parentCTM 父级CTM, boundaryInCTM 边界是否参与CTM变换 +func (r *Renderer) renderPath(ctx *canvas.Context, obj PathObject, pageH float64, defaultFill, defaultStroke color.Color, defaultLW float64, parentCTM *Matrix, boundaryInCTM bool) { + if obj.Visible != nil && !*obj.Visible { + return + } + ctx.Push() + bx, by := 0.0, 0.0 + if obj.Boundary != "" { + if box, err := ParseBox(obj.Boundary); err == nil { + bx, by = box.X, box.Y + } + } + ctm := NewMatrix(obj.CTM) + if parentCTM != nil { + ctm = parentCTM.Multiply(ctm) + } + style := newPathStyle(defaultFill, defaultStroke, defaultLW, obj.Alpha) + if obj.DrawParam != "" { + if dp := r.getDrawParam(obj.DrawParam, nil); dp != nil { + style.applyDrawParam(dp, bx, by, pageH, obj.Alpha) + } + } + style.applyPathObject(obj, bx, by, pageH) + style.scale(ctm) + p := r.buildPath(obj, pageH, ctm, boundaryInCTM) + if rectPath := r.buildTinyFillRectPath(obj, pageH, ctm, bx, by); rectPath != nil { + p = rectPath + } + clipPath := r.buildClipPath(obj.Clips, pageH, bx, by, ctm) + shouldFill := false + if obj.Fill != nil { + shouldFill = *obj.Fill + } + if style.fillPaint == nil { + style.fillPaint = style.fillColor + } + if shouldFill && style.fillPattern != nil { + fp := p + if clipPath != nil { + fp = p.Copy() + fp.Close() + fp = fp.And(clipPath) + } + r.renderPattern(ctx, style.fillPattern, style.fillPatternColor, pageH, fp, ctm, bx, by) + } else if shouldFill && style.fillPaint != nil { + ctx.SetFill(style.fillPaint) + ctx.SetStrokeColor(canvas.Transparent) + fp := p + if clipPath != nil { + fp = p.Copy() + fp.Close() + fp = fp.And(clipPath) + } + ctx.DrawPath(0, 0, fp) + } + shouldStroke := true + if obj.Stroke != nil { + shouldStroke = *obj.Stroke + } + if shouldStroke { + if style.strokePaint == nil { + style.strokePaint = style.strokeColor + } + if style.strokePaint == nil { + style.strokePaint = colorWithAlpha(canvas.Black, obj.Alpha) + } + ctx.SetFillColor(canvas.Transparent) + ctx.SetStroke(style.strokePaint) + ctx.SetStrokeWidth(style.lineWidth) + ctx.SetStrokeCapper(style.lineCap) + ctx.SetStrokeJoiner(style.lineJoin) + if len(style.dashPattern) > 0 { + ctx.SetDashes(style.dashOffset, style.dashPattern...) + } + if clipPath != nil { + sp := p.Copy() + if len(style.dashPattern) > 0 { + sp = sp.Dash(style.dashOffset, style.dashPattern...) + } + sp = sp.Stroke(style.lineWidth, style.lineCap, style.lineJoin, canvas.Tolerance) + sp = sp.And(clipPath) + ctx.SetFill(style.strokePaint) + ctx.SetStrokeColor(canvas.Transparent) + ctx.DrawPath(0, 0, sp) + } else { + ctx.DrawPath(0, 0, p) + } + } + ctx.Pop() +} + +// renderPattern 渲染图案填充 +// 入参: ctx 画布上下文, pattern 图案对象, defaultColor 默认颜色, pageH 页面高度, clip 填充区域, parentCTM 父级CTM, bx 边界X坐标, by 边界Y坐标 +func (r *Renderer) renderPattern(ctx *canvas.Context, pattern *Pattern, defaultColor color.Color, pageH float64, clip *canvas.Path, parentCTM Matrix, bx, by float64) { + if pattern == nil || clip == nil || len(pattern.CellContent.Objects) == 0 { + return + } + xStep, yStep := pattern.XStep, pattern.YStep + if xStep == 0 { + xStep = pattern.Width + } + if yStep == 0 { + yStep = pattern.Height + } + if xStep <= 0 || yStep <= 0 { + return + } + patternCTM := TranslationMatrix(bx, by).Multiply(parentCTM).Multiply(NewMatrix(pattern.CTM)) + invCTM, ok := patternCTM.Invert() + if !ok { + return + } + bounds := clip.FastBounds() + points := [][2]float64{ + {bounds.X0, pageH - bounds.Y0}, + {bounds.X1, pageH - bounds.Y0}, + {bounds.X1, pageH - bounds.Y1}, + {bounds.X0, pageH - bounds.Y1}, + } + minX, maxX := 0.0, 0.0 + minY, maxY := 0.0, 0.0 + for i, point := range points { + x, y := invCTM.Transform(point[0], point[1]) + if i == 0 { + minX, maxX = x, x + minY, maxY = y, y + continue + } + minX = math.Min(minX, x) + maxX = math.Max(maxX, x) + minY = math.Min(minY, y) + maxY = math.Max(maxY, y) + } + startX := int(math.Floor(minX/xStep)) - 1 + endX := int(math.Ceil(maxX/xStep)) + 1 + startY := int(math.Floor(minY/yStep)) - 1 + endY := int(math.Ceil(maxY/yStep)) + 1 + for ix := startX; ix <= endX; ix++ { + for iy := startY; iy <= endY; iy++ { + tileCTM := patternCTM.Multiply(TranslationMatrix(float64(ix)*xStep, float64(iy)*yStep)) + for _, obj := range pattern.CellContent.Objects { + r.renderObject(ctx, obj, pageH, defaultColor, defaultColor, 0, &tileCTM, false) + } + } + } +} + +// buildPath 解析路径并返回Canvas Path +// 入参: obj 路径对象, pageH 页面高度, ctm 变换矩阵, boundaryInCTM 边界是否参与CTM变换 +// 返回: *canvas.Path 路径对象 +func (r *Renderer) buildPath(obj PathObject, pageH float64, ctm Matrix, boundaryInCTM bool) *canvas.Path { + bx, by := 0.0, 0.0 + if obj.Boundary != "" { + if box, err := ParseBox(obj.Boundary); err == nil { + bx, by = box.X, box.Y + } + } + point := func(x, y float64) (float64, float64) { + if boundaryInCTM { + tx, ty := ctm.Transform(x+bx, y+by) + return tx, pageH - ty + } + tx, ty := ctm.Transform(x, y) + return tx + bx, pageH - (ty + by) + } + p := &canvas.Path{} + tokens := strings.Fields(obj.AbbreviatedData) + for i := 0; i < len(tokens); { + cmd := tokens[i] + i++ + switch cmd { + case "M", "S": + if i+1 < len(tokens) { + x, _ := strconv.ParseFloat(tokens[i], 64) + y, _ := strconv.ParseFloat(tokens[i+1], 64) + tx, ty := point(x, y) + p.MoveTo(tx, ty) + i += 2 + } + case "L": + if i+1 < len(tokens) { + x, _ := strconv.ParseFloat(tokens[i], 64) + y, _ := strconv.ParseFloat(tokens[i+1], 64) + tx, ty := point(x, y) + p.LineTo(tx, ty) + i += 2 + } + case "B": + if i+5 < len(tokens) { + x1, _ := strconv.ParseFloat(tokens[i], 64) + y1, _ := strconv.ParseFloat(tokens[i+1], 64) + x2, _ := strconv.ParseFloat(tokens[i+2], 64) + y2, _ := strconv.ParseFloat(tokens[i+3], 64) + x3, _ := strconv.ParseFloat(tokens[i+4], 64) + y3, _ := strconv.ParseFloat(tokens[i+5], 64) + tx1, ty1 := point(x1, y1) + tx2, ty2 := point(x2, y2) + tx3, ty3 := point(x3, y3) + p.CubeTo(tx1, ty1, tx2, ty2, tx3, ty3) + i += 6 + } + case "Q": + if i+3 < len(tokens) { + x1, _ := strconv.ParseFloat(tokens[i], 64) + y1, _ := strconv.ParseFloat(tokens[i+1], 64) + x2, _ := strconv.ParseFloat(tokens[i+2], 64) + y2, _ := strconv.ParseFloat(tokens[i+3], 64) + tx1, ty1 := point(x1, y1) + tx2, ty2 := point(x2, y2) + p.QuadTo(tx1, ty1, tx2, ty2) + i += 4 + } + case "A": + if i+6 < len(tokens) { + rx, _ := strconv.ParseFloat(tokens[i], 64) + ry, _ := strconv.ParseFloat(tokens[i+1], 64) + rot, _ := strconv.ParseFloat(tokens[i+2], 64) + large, _ := strconv.ParseBool(tokens[i+3]) + sweep, _ := strconv.ParseBool(tokens[i+4]) + x, _ := strconv.ParseFloat(tokens[i+5], 64) + y, _ := strconv.ParseFloat(tokens[i+6], 64) + sx := math.Hypot(ctm.a, ctm.c) + sy := math.Hypot(ctm.b, ctm.d) + ctmRot := math.Atan2(ctm.b, ctm.a) * 180 / math.Pi + tx, ty := point(x, y) + sweep = !sweep + p.ArcTo(rx*sx, ry*sy, -(rot + ctmRot), large, sweep, tx, ty) + i += 7 + } + case "C": + p.Close() + } + } + return p +} + +// buildClipPath 构建裁剪路径 +// 入参: clips 裁剪对象, pageH 页面高度, bx 边界X坐标, by 边界Y坐标, objectCTM 对象CTM +// 返回: *canvas.Path 路径对象 +func (r *Renderer) buildClipPath(clips *Clips, pageH float64, bx, by float64, objectCTM Matrix) *canvas.Path { + if clips == nil { + return nil + } + var p *canvas.Path + for _, clip := range clips.Clip { + var clipPath *canvas.Path + for _, area := range clip.Area { + areaCTM := NewMatrix(area.CTM) + if clips.TransFlag { + areaCTM = objectCTM.Multiply(areaCTM) + } + for _, pathObj := range area.Path { + ctm := areaCTM.Multiply(NewMatrix(pathObj.CTM)) + cp := r.buildPath(pathObj, pageH, ctm, true) + cp.Translate(bx, -by) + cp.Close() + if clipPath == nil { + clipPath = cp + } else { + clipPath = clipPath.Or(cp) + } + } + } + if clipPath != nil { + if p == nil { + p = clipPath + } else { + p = p.And(clipPath) + } + } + } + return p +} + +// buildTinyFillRectPath 构建微小填充矩形路径 +// 入参: obj 路径对象, pageH 页面高度, ctm 变换矩阵, bx 边界X坐标, by 边界Y坐标 +// 返回: *canvas.Path 路径对象 +func (r *Renderer) buildTinyFillRectPath(obj PathObject, pageH float64, ctm Matrix, bx, by float64) *canvas.Path { + if obj.Fill == nil || !*obj.Fill || obj.Stroke == nil || *obj.Stroke { + return nil + } + box, err := ParseBox(obj.Boundary) + if err != nil || box.W <= 0 || box.H <= 0 || box.W > 0.6 || box.H > 0.6 { + return nil + } + tokens := strings.Fields(obj.AbbreviatedData) + if len(tokens) != 11 || tokens[0] != "M" || tokens[3] != "L" || tokens[6] != "L" || tokens[9] != "L" || tokens[10] != "C" { + return nil + } + points := make([][2]float64, 0, 4) + for i := 1; i < 10; i += 3 { + x, errX := strconv.ParseFloat(tokens[i], 64) + y, errY := strconv.ParseFloat(tokens[i+1], 64) + if errX != nil || errY != nil { + return nil + } + tx, ty := ctm.Transform(x, y) + points = append(points, [2]float64{tx + bx, pageH - (ty + by)}) + } + minX, maxX := points[0][0], points[0][0] + minY, maxY := points[0][1], points[0][1] + for _, point := range points[1:] { + minX = math.Min(minX, point[0]) + maxX = math.Max(maxX, point[0]) + minY = math.Min(minY, point[1]) + maxY = math.Max(maxY, point[1]) + } + expand := math.Min(box.W, box.H) * 0.08 + p := &canvas.Path{} + p.MoveTo(minX-expand, minY-expand) + p.LineTo(maxX+expand, minY-expand) + p.LineTo(maxX+expand, maxY+expand) + p.LineTo(minX-expand, maxY+expand) + p.Close() + return p +} diff --git a/ofdgo_renderer_stamp.go b/ofdgo_renderer_stamp.go new file mode 100644 index 0000000..4280307 --- /dev/null +++ b/ofdgo_renderer_stamp.go @@ -0,0 +1,102 @@ +// Copyright 2025-2026 肖其顿 (XIAO QI DUN) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ofdgo + +import ( + "bytes" + "image" + "image/color" + + "github.com/tdewolff/canvas" +) + +// renderStamp 渲染印章 +// 入参: ctx 画布上下文, s 印章对象, pageH 页面高度 +func (r *Renderer) renderStamp(ctx *canvas.Context, s Stamp, pageH float64) { + x, y, w, h := s.Box.X, s.Box.Y, s.Box.W, s.Box.H + screenY := pageH - (y + h) + if s.Type == "ofd" && len(s.Data) > 0 { + reader, err := NewReader(bytes.NewReader(s.Data), int64(len(s.Data))) + if err == nil { + defer reader.Close() + doc, err := reader.Doc() + if err == nil { + renderer := r.childRenderer(reader) + for _, pageRef := range doc.Pages.Page { + content, err := reader.PageContent(pageRef) + if err != nil { + continue + } + sealBox, err := renderer.GetPageBox(content) + if err != nil { + continue + } + ctx.Push() + ctx.Translate(x, screenY) + ctx.Scale(w/sealBox.W, h/sealBox.H) + renderer.renderPageToContext(ctx, content, false) + ctx.Pop() + } + return + } + } + } + if len(s.Data) > 0 { + img, _, err := decodeImageData(s.Data) + if err == nil { + img = stampImageWithTransparentWhite(img) + ctx.Push() + ctx.Translate(x, screenY) + ctx.Scale(w/float64(img.Bounds().Dx()), h/float64(img.Bounds().Dy())) + ctx.DrawImage(0, 0, img, canvas.DPMM(1.0)) + ctx.Pop() + return + } + } +} + +// stampImageWithTransparentWhite 处理印章图片白色底色 +// 入参: img 印章图片对象 +// 返回: image.Image 处理后的印章图片对象 +func stampImageWithTransparentWhite(img image.Image) image.Image { + if opaque, ok := img.(interface{ Opaque() bool }); ok && !opaque.Opaque() { + return img + } + bounds := img.Bounds() + out := image.NewNRGBA(bounds) + hasAlpha := false + for y := bounds.Min.Y; y < bounds.Max.Y; y++ { + for x := bounds.Min.X; x < bounds.Max.X; x++ { + c := color.NRGBAModel.Convert(img.At(x, y)).(color.NRGBA) + if c.A < 255 { + hasAlpha = true + } + out.SetNRGBA(x, y, c) + } + } + if hasAlpha { + return img + } + for y := bounds.Min.Y; y < bounds.Max.Y; y++ { + for x := bounds.Min.X; x < bounds.Max.X; x++ { + c := out.NRGBAAt(x, y) + if c.R >= 250 && c.G >= 250 && c.B >= 250 { + c.A = 0 + out.SetNRGBA(x, y, c) + } + } + } + return out +} diff --git a/ofdgo_renderer_text.go b/ofdgo_renderer_text.go new file mode 100644 index 0000000..1f1dd4d --- /dev/null +++ b/ofdgo_renderer_text.go @@ -0,0 +1,248 @@ +// Copyright 2025-2026 肖其顿 (XIAO QI DUN) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ofdgo + +import ( + "image/color" + "strings" + + "github.com/tdewolff/canvas" +) + +const ptPerMM = 72.0 / 25.4 + +// renderText 渲染文本 +// 入参: ctx 画布上下文, obj 文本对象, pageH 页面高度, defaultFill 默认填充色, defaultStroke 默认描边色, parentCTM 父级CTM, boundaryInCTM 边界是否参与父级CTM +func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64, defaultFill, defaultStroke color.Color, parentCTM *Matrix, boundaryInCTM bool) { + if obj.Visible != nil && !*obj.Visible { + return + } + ctx.Push() + bx, by := 0.0, 0.0 + if obj.Boundary != "" { + if box, err := ParseBox(obj.Boundary); err == nil { + bx, by = box.X, box.Y + } + } + localCTM := NewMatrix(obj.CTM) + ctm := localCTM + if parentCTM != nil { + ctm = parentCTM.Multiply(ctm) + } + var dp *DrawParam + if obj.DrawParam != "" { + dp = r.getDrawParam(obj.DrawParam, nil) + } + sizeMM := obj.Size + if sizeMM == 0 && dp != nil && dp.Size > 0 { + sizeMM = dp.Size + } + if sizeMM == 0 { + sizeMM = 3.5 + } + if obj.VScale != 0 { + sizeMM *= obj.VScale + } + hScale := obj.HScale + if hScale == 0 { + hScale = 1 + } + useTextMatrix := hasTextMatrix(ctm) + if scale := ctm.YScale(); scale > 0 && !useTextMatrix { + sizeMM *= scale + } + sizePt := sizeMM * ptPerMM + fillColor := colorWithAlpha(defaultFill, obj.Alpha) + if fillColor == nil { + fillColor = colorWithAlpha(canvas.Black, obj.Alpha) + } + var fillPaint any = fillColor + var fillColorNode *FillColor + if dp != nil && dp.FillColor != nil { + fillColorNode = withFillAlpha(dp.FillColor, obj.Alpha) + fillColor = parseFillColor(fillColorNode) + fillPaint = parseFillPaint(fillColorNode, bx, by, pageH, 0, 0) + } + if obj.FillColor != nil { + fillColorNode = withFillAlpha(obj.FillColor, obj.Alpha) + fillColor = parseFillColor(fillColorNode) + fillPaint = parseFillPaint(fillColorNode, bx, by, pageH, 0, 0) + } + if fillPaint == nil { + fillPaint = fillColor + } + fontStyle := canvas.FontRegular + weight := obj.Weight + if weight == 0 && dp != nil && dp.Weight > 0 { + weight = dp.Weight + } + syntheticBold := weight >= 700 + italic := obj.Italic + if !italic && dp != nil && dp.Italic { + italic = true + } + if italic { + fontStyle |= canvas.FontItalic + } + fontID := r.textObjectFontID(obj) + embeddedFont := false + if of, ok := r.Reader.fontCache[fontID]; ok { + embeddedFont = of.FontFile != "" + if !embeddedFont && fontNoSyntheticBold(of.FontName, of.FamilyName) { + syntheticBold = false + } + if of.Bold { + fontStyle |= canvas.FontBold + } + if of.Italic { + fontStyle |= canvas.FontItalic + } + } + if syntheticBold { + fontStyle |= canvas.FontBold + } + ff := r.loadFont(fontID) + if ff == nil { + return + } + face := ff.Face(sizePt, fillPaint, fontStyle, canvas.FontNormal) + glyphTransforms := r.textObjectGlyphTransforms(fontID, obj) + hasUnderline := strings.Contains(obj.Decoration, "Underline") + useGlyphFillPaint := fillColorNode != nil && fillColorNode.AxialShd != nil + codePos := 0 + for _, tc := range obj.TextCode { + var runes []rune + var glyphs []textGlyph + if tc.Index != "" { + runes = r.parseIndexRunes(tc.Index, fontID) + glyphs = textRuneGlyphs(runes) + } else { + runes = textCodeRunes(tc.Value) + glyphs = textCodeGlyphs(runes, glyphTransforms, codePos) + } + dxs, dys := parseFloats(tc.DeltaX), parseFloats(tc.DeltaY) + xs, ys := parseFloats(tc.X), parseFloats(tc.Y) + drawAsPath := embeddedFont || textCodePositioned(tc, xs, ys) + cx, cy := 0.0, 0.0 + if len(xs) > 0 { + cx = xs[0] + } + if len(ys) > 0 { + cy = ys[0] + } + for i, glyph := range glyphs { + str := glyph.Text + if i < len(xs) { + cx = xs[i] + } else if i > 0 { + if dx, ok := textDelta(dxs, i-1); ok { + cx += dx + } else if len(dys) == 0 { + cx += textGlyphWidth(face, glyph) * hScale + } + } + if i < len(ys) { + cy = ys[i] + } else if i > 0 { + if dy, ok := textDelta(dys, i-1); ok { + cy += dy + } + } + var canvasX, canvasY float64 + if boundaryInCTM && parentCTM != nil { + tx, ty := localCTM.Transform(cx, cy) + tx, ty = parentCTM.Transform(tx+bx, ty+by) + canvasX, canvasY = tx, pageH-ty + } else { + tx, ty := ctm.Transform(cx, cy) + canvasX, canvasY = tx+bx, pageH-(ty+by) + } + textWidth := textGlyphWidth(face, glyph) * hScale + glyphFillPaint := fillPaint + if useGlyphFillPaint { + glyphFillPaint = parseFillPaint(fillColorNode, bx, by, pageH, canvasX, canvasY) + } + advanceLimit := textGlyphAdvanceLimit(dxs, dys, xs, i, len(glyphs), cx) + if glyphFillPaint != nil { + ctx.SetFill(glyphFillPaint) + drawGlyph := func(x, y float64) { + if drawAsPath || glyph.GlyphID >= 0 { + path, width := textGlyphPath(face, glyph) + scaleX := hScale + if advanceLimit > 0 && width*scaleX > advanceLimit { + scaleX = advanceLimit / width + } + textWidth = width * scaleX + if scaleX != 1 { + ctx.Push() + ctx.Translate(x, y) + ctx.Scale(scaleX, 1) + ctx.DrawPath(0, 0, path) + ctx.Pop() + } else { + ctx.DrawPath(x, y, path) + } + } else { + scaled := hScale != 1 + if scaled { + ctx.Push() + ctx.Translate(x, y) + ctx.Scale(hScale, 1) + x, y = 0, 0 + } + textFace := face + if useGlyphFillPaint { + textFace = ff.Face(sizePt, glyphFillPaint, fontStyle, canvas.FontNormal) + } + text := canvas.NewTextLine(textFace, str, canvas.Left) + ctx.DrawText(x, y, text) + if scaled { + ctx.Pop() + } + } + } + if useTextMatrix { + ctx.Push() + ctx.Translate(canvasX, canvasY) + ctx.ComposeView(textMatrix(ctm)) + drawGlyph(0, 0) + if hasUnderline { + uw := sizeMM * 0.05 + ctx.SetStrokeWidth(uw) + ctx.SetStrokeColor(fillColor) + off := sizeMM * 0.1 + ctx.MoveTo(0, -off) + ctx.LineTo(textWidth, -off) + ctx.Stroke() + } + ctx.Pop() + continue + } + drawGlyph(canvasX, canvasY) + } + if hasUnderline { + uw := sizeMM * 0.05 + ctx.SetStrokeWidth(uw) + ctx.SetStrokeColor(fillColor) + off := sizeMM * 0.1 + ctx.MoveTo(canvasX, canvasY-off) + ctx.LineTo(canvasX+textWidth, canvasY-off) + ctx.Stroke() + } + } + codePos += len(runes) + } + ctx.Pop() +}