// 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 ( "fmt" "image" "image/color" "github.com/tdewolff/canvas" ) // canvasPageCompiler 将已解释的页面绘制转换为自有数据,不保存第三方绘图对象 type canvasPageCompiler struct { page *RasterPage geometry GeometryBackend err error } // rasterPaddedImage 为旋转采样提供透明边距,共享原图而不复制整幅像素 type rasterPaddedImage struct { source image.Image rect image.Rectangle } // CompilePage 使用Canvas页面解释器和配置的字体、几何后端生成独立绘制数据 // 入参: r 渲染器, page 页面内容 // 返回: *RasterPage 绘制页面, error 编译错误 func (CanvasBackend) CompilePage(r *Renderer, page *PageContent) (*RasterPage, error) { box, err := r.GetPageBox(page) if err != nil { return nil, err } result := &RasterPage{Width: box.W, Height: box.H, DPI: r.DPI} if _, _, err := result.PixelSize(); err != nil { return nil, err } geometry, err := r.Geometry() if err != nil { return nil, err } compiler := &canvasPageCompiler{page: result, geometry: geometry} if err := r.renderCanvasContext(canvas.NewContext(compiler), page); err != nil { return nil, err } if compiler.err != nil { return nil, compiler.err } return result, nil } // Size 返回物理页面尺寸 // 返回: float64 宽度, float64 高度 func (c *canvasPageCompiler) Size() (float64, float64) { return c.page.Width, c.page.Height } // RenderPath 记录填充并按当前DPI展开描边轮廓 // 入参: path 路径, style 绘制样式, m 变换矩阵 func (c *canvasPageCompiler) RenderPath(path *canvas.Path, style canvas.Style, m canvas.Matrix) { if c.err != nil { return } if style.HasFill() { c.fill(path, style.Fill, style.FillRule, m) } if style.HasStroke() { stroke := path options := canvasStrokeOptions(style.StrokeWidth, style.StrokeCapper, style.StrokeJoiner, canvas.PixelTolerance/(c.page.DPI/25.4)) if len(style.Dashes) > 0 { options.DashOffset, options.Dashes = canvas.ScaleDash(style.StrokeWidth, style.DashOffset, style.Dashes) } var err error stroke, err = canvasStrokePath(c.geometry, stroke, options) if err != nil { c.err = err return } c.fill(stroke, style.Stroke, style.FillRule, m) } } // RenderText 展开已定位字形,使用与目标DPI一致的字形精度 // 入参: text 文字, m 变换矩阵 func (c *canvasPageCompiler) RenderText(text *canvas.Text, m canvas.Matrix) { if c.err == nil { text.RenderTo(c, m, canvas.DPMM(c.page.DPI/25.4)) } } // RenderImage 记录按像素左上原点定位的图片,旋转或斜切时补充透明采样边距 // 入参: img 图片, m 变换矩阵 func (c *canvasPageCompiler) RenderImage(img image.Image, m canvas.Matrix) { if c.err != nil { return } img = imagePixelSource(img) if (m[0][1] != 0 || m[1][0] != 0) && (m[0][0] != 0 || m[1][1] == 0) { bounds := img.Bounds() img = &rasterPaddedImage{source: img, rect: image.Rect(0, 0, bounds.Dx()+8, bounds.Dy()+8)} m = m.Translate(-4, -4) } h := float64(img.Bounds().Dy()) t := RasterMatrix{m[0][0], -m[1][0], -m[0][1], m[1][1], m[0][2] + m[0][1]*h, c.page.Height - m[1][2] - m[1][1]*h} c.page.Commands = append(c.page.Commands, RasterCommand{Image: img, Transform: t}) } // Bounds 返回含四像素边距的图像边界 // 返回: image.Rectangle 图像边界 func (s *rasterPaddedImage) Bounds() image.Rectangle { return s.rect } // ColorModel 返回预乘八位颜色模型,与实体补边图像保持一致 // 返回: color.Model 颜色模型 func (s *rasterPaddedImage) ColorModel() color.Model { return color.RGBAModel } // At 读取补边图像的预乘像素,边距及图像外透明 // 入参: x 横向坐标, y 纵向坐标 // 返回: color.Color 像素颜色 func (s *rasterPaddedImage) At(x, y int) color.Color { if !image.Pt(x, y).In(s.rect.Inset(4)) { return color.RGBA{} } b := s.source.Bounds() return color.RGBAModel.Convert(s.source.At(x-4+b.Min.X, y-4+b.Min.Y)) } // RGBA64At 返回预乘十六位像素供采样器读取 // 入参: x 横向坐标, y 纵向坐标 // 返回: color.RGBA64 像素颜色 func (s *rasterPaddedImage) RGBA64At(x, y int) color.RGBA64 { r, g, b, a := s.At(x, y).RGBA() return color.RGBA64{R: uint16(r), G: uint16(g), B: uint16(b), A: uint16(a)} } // fill 转换路径和画刷,未知类型返回错误而非丢弃内容 // 入参: path 路径, paint 画刷, rule 填充规则, m 变换矩阵 func (c *canvasPageCompiler) fill(path *canvas.Path, paint canvas.Paint, rule canvas.FillRule, m canvas.Matrix) { if c.err != nil || path.Empty() { return } if paint.Pattern != nil { c.err = fmt.Errorf("unsupported raster pattern %T", paint.Pattern) return } cmd := RasterCommand{Paint: RasterPaint{Color: paint.Color}, EvenOdd: rule == canvas.EvenOdd, Transform: RasterMatrix{m[0][0], -m[1][0], m[0][1], -m[1][1], m[0][2], c.page.Height - m[1][2]}} if paint.Gradient != nil { g := &RasterGradient{} var stops canvas.Grad switch source := paint.Gradient.(type) { case rasterGradientCanvas: copy := *source.gradient g = © stops = canvasGradient(g.Stops) case *canvas.LinearGradient: g.Kind, g.Start, g.End = RasterLinear, rasterPoint(source.Start), rasterPoint(source.End) stops = source.Grad case *canvas.RadialGradient: g.Kind, g.Start, g.End = RasterRadial, rasterPoint(source.C0), rasterPoint(source.C1) g.R0, g.R1, stops = source.R0, source.R1, source.Grad default: c.err = fmt.Errorf("unsupported raster gradient %T", paint.Gradient) return } g.Stops = make([]ColorStop, len(stops)) for i, stop := range stops { g.Stops[i] = ColorStop{Offset: stop.Offset, Color: stop.Color} } cmd.Paint.Gradient = g } var err error cmd.Path, err = canvasRasterPath(path) if err != nil { c.err = err return } c.page.Commands = append(c.page.Commands, cmd) } // canvasRasterPath 转换曲线路径,保留各子路径和闭合标记 // 入参: path Canvas路径 // 返回: []RasterSegment 独立路径, error 未知指令错误 func canvasRasterPath(path *canvas.Path) ([]RasterSegment, error) { path = path.ReplaceArcs() segments := make([]RasterSegment, 0, path.Len()) scanner := path.Scanner() for scanner.Scan() { s := RasterSegment{End: rasterPoint(scanner.End())} switch scanner.Cmd() { case canvas.MoveToCmd: s.Verb = RasterMove case canvas.LineToCmd: s.Verb = RasterLine case canvas.QuadToCmd: s.Verb, s.Control1 = RasterQuad, rasterPoint(scanner.CP1()) case canvas.CubeToCmd: s.Verb, s.Control1, s.Control2 = RasterCubic, rasterPoint(scanner.CP1()), rasterPoint(scanner.CP2()) case canvas.CloseCmd: s.Verb = RasterClose default: return nil, fmt.Errorf("unsupported raster path command %g", scanner.Cmd()) } segments = append(segments, s) } return segments, nil } // rasterPoint 转换局部坐标点 // 入参: p 默认引擎坐标点 // 返回: RasterPoint 独立坐标点 func rasterPoint(p canvas.Point) RasterPoint { return RasterPoint{X: p.X, Y: p.Y} }