Files
ofdgo/ofdgo_renderer_compile_canvas.go
T
2026-10-12 23:04:11 +08:00

247 lines
8.3 KiB
Go

// 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 = canvasStrokeFillPath(c.geometry, stroke, options)
if err != nil {
c.err = err
return
}
c.fill(stroke, style.Stroke, canvas.NonZero, 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
}
var err error
img, err = imagePixelData(img)
if err != nil {
c.err = err
return
}
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)
}
bounds := img.Bounds()
h := float64(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}
t[4] -= t[0]*float64(bounds.Min.X) + t[2]*float64(bounds.Min.Y)
t[5] -= t[1]*float64(bounds.Min.X) + t[3]*float64(bounds.Min.Y)
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 {
if imageHighPrecision(s.source) {
return color.RGBA64Model
}
return color.RGBAModel
}
// At 读取补边图像的预乘像素,边距及图像外透明
// 入参: x 横向坐标, y 纵向坐标
// 返回: color.Color 像素颜色
func (s *rasterPaddedImage) At(x, y int) color.Color { return s.RGBA64At(x, y) }
// RGBA64At 保留原始预乘精度,支持采样接口的图像不进行颜色装箱
// 入参: x 横向坐标, y 纵向坐标
// 返回: color.RGBA64 像素颜色
func (s *rasterPaddedImage) RGBA64At(x, y int) color.RGBA64 {
if !image.Pt(x, y).In(s.rect.Inset(4)) {
return color.RGBA64{}
}
bounds := s.source.Bounds()
x, y = x-4+bounds.Min.X, y-4+bounds.Min.Y
if source, ok := s.source.(image.RGBA64Image); ok {
return source.RGBA64At(x, y)
}
r, g, b, a := s.source.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 = &copy
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) {
segments := make([]RasterSegment, 0, path.Len())
parts := path.Split()
if len(parts) == 0 {
parts = []*canvas.Path{path}
}
for _, part := range parts {
scanner := part.ReplaceArcs().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)
}
}
data := path.Data()
last := parts[len(parts)-1].Data()
if len(data) >= 4 && data[len(data)-4] == canvas.MoveToCmd && len(last) != 0 && last[len(last)-1] != canvas.MoveToCmd {
segments = append(segments, RasterSegment{Verb: RasterMove, End: RasterPoint{X: data[len(data)-3], Y: data[len(data)-2]}})
}
return segments, nil
}
// rasterPoint 转换局部坐标点
// 入参: p 默认引擎坐标点
// 返回: RasterPoint 独立坐标点
func rasterPoint(p canvas.Point) RasterPoint { return RasterPoint{X: p.X, Y: p.Y} }