Files
ofdgo/ofdgo_renderer_gg.go
T
2026-09-23 20:04:48 +08:00

212 lines
6.5 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"
"sync"
"github.com/gogpu/gg"
)
// GGBackend 提供字体、页面编译和CPU绘制,不注册全局GPU设备或隐式切换后端
// StrokeGeometry用于GG暂不能准确处理的尖角连接、闭合或曲线虚线及亚像素渐变描边,未配置时返回能力错误
type GGBackend struct{ StrokeGeometry GeometryBackend }
// Name 返回后端标识
// 返回: string 后端标识
func (GGBackend) Name() string { return "gg" }
// Prepare 将路径、样式和裁剪准备为固定DPI的GG场景
// 入参: page 只读页面
// 返回: RasterScene 可重复绘制场景, error 准备错误
func (b GGBackend) Prepare(page *RasterPage) (RasterScene, error) {
w, h, err := page.PixelSize()
if err != nil {
return nil, err
}
page = cloneRasterPage(page)
scene := &ggRasterScene{page: page, width: w, height: h, masks: renderCache[[32]byte, *image.Alpha]{limit: 16 << 20}}
for i, cmd := range page.Commands {
if cmd.Stroke != nil && !ggNativeStroke(cmd, page.DPI/25.4) {
cmd, err = b.expandStroke(cmd)
if err != nil {
return nil, fmt.Errorf("gg command %d: %w", i, err)
}
}
m := page.PixelTransform(cmd.Transform, h)
if cmd.Image != nil {
scene.commands = append(scene.commands, ggRasterCommand{command: cmd})
continue
}
path := gg.NewPath()
for _, s := range cmd.Path {
end, c1, c2 := m.Apply(s.End), m.Apply(s.Control1), m.Apply(s.Control2)
switch s.Verb {
case RasterMove:
path.MoveTo(end.X, end.Y)
case RasterLine:
path.LineTo(end.X, end.Y)
case RasterQuad:
path.QuadraticTo(c1.X, c1.Y, end.X, end.Y)
case RasterCubic:
path.CubicTo(c1.X, c1.Y, c2.X, c2.Y, end.X, end.Y)
case RasterClose:
path.Close()
default:
return nil, fmt.Errorf("gg command %d: unsupported path verb %d", i, s.Verb)
}
}
paint := gg.NewPaint()
paint.SetFillBrush(gg.SolidBrush{Color: ggRGBA(cmd.Paint.Color)})
if g := cmd.Paint.Gradient; g != nil {
if g.Kind != RasterLinear && g.Kind != RasterRadial {
return nil, fmt.Errorf("gg command %d: unsupported gradient %d", i, g.Kind)
}
inverse, err := m.Inverse()
if err != nil {
return nil, fmt.Errorf("gg command %d: %w", i, err)
}
paint.SetFillBrush(gg.NewCustomBrush(func(x, y float64) gg.RGBA {
p := inverse.Apply(RasterPoint{X: x, Y: y})
return ggRGBA(g.At(p.X, p.Y))
}))
}
if cmd.EvenOdd && cmd.Stroke == nil {
paint.FillRule = gg.FillRuleEvenOdd
}
if cmd.Clip != nil {
mask, err := rasterClipMask(GGBackend{}, page, cmd.Clip, &scene.masks)
if err != nil {
return nil, err
}
if mask.Rect.Empty() {
continue
}
paint.ClipMask, paint.ClipMaskW, paint.ClipMaskH = mask.Pix, mask.Stride, mask.Rect.Dy()
paint.ClipMaskX, paint.ClipMaskY = mask.Rect.Min.X, mask.Rect.Min.Y
}
if cmd.Stroke != nil {
style, err := ggStroke(*cmd.Stroke, page.DPI/25.4)
if err != nil {
return nil, err
}
paint.SetStroke(style)
paint.SetStrokeBrush(paint.FillBrush())
}
scene.commands = append(scene.commands, ggRasterCommand{command: cmd, path: path, paint: paint})
}
return scene, nil
}
// ggRasterCommand 保存已转换的像素路径和样式
type ggRasterCommand struct {
command RasterCommand
path *gg.Path
paint *gg.Paint
}
// ggRasterScene 复用路径和局部裁剪,串行保护掩码缓存
type ggRasterScene struct {
mu sync.Mutex
page *RasterPage
width, height int
commands []ggRasterCommand
masks renderCache[[32]byte, *image.Alpha]
}
// Render 绘制独立像素缓冲,不累积上次内容
// 返回: image.Image 图像, error 绘制错误
func (s *ggRasterScene) Render() (image.Image, error) {
s.mu.Lock()
defer s.mu.Unlock()
pixmap := gg.NewPixmap(s.width, s.height)
img := pixmap.ImageView()
raster := gg.NewSoftwareRenderer(s.width, s.height)
for i, cmd := range s.commands {
var err error
switch {
case cmd.command.Image != nil:
err = drawRasterImageCommand(GGBackend{}, s.page, img, imagePixelSource(cmd.command.Image), cmd.command, &s.masks)
case cmd.command.Stroke != nil:
err = raster.Stroke(pixmap, cmd.path, cmd.paint)
default:
err = raster.Fill(pixmap, cmd.path, cmd.paint)
}
if err != nil {
return nil, fmt.Errorf("gg command %d: %w", i, err)
}
}
return img, nil
}
// Render 准备并绘制页面,重复绘制可使用Prepare复用场景
// 入参: page 只读页面
// 返回: image.Image 图像, error 绘制错误
func (b GGBackend) Render(page *RasterPage) (image.Image, error) {
scene, err := b.Prepare(page)
if err != nil {
return nil, err
}
return scene.Render()
}
// ggStroke 将页面毫米样式转换为GG像素描边,虚线与线宽同步换算
// 入参: options 描边样式, scale 每毫米像素数
// 返回: gg.Stroke 像素样式, error 样式错误
func ggStroke(options StrokeOptions, scale float64) (gg.Stroke, error) {
if err := validateStroke(options); err != nil {
return gg.Stroke{}, err
}
style := gg.DefaultStroke().WithWidth(options.Width * scale)
style.MiterLimit = options.MiterLimit
if style.MiterLimit == 0 {
style.MiterLimit = defaultMiterLimit
}
switch options.Cap {
case "Round":
style.Cap = gg.LineCapRound
case "Square":
style.Cap = gg.LineCapSquare
}
switch options.Join {
case "Round":
style.Join = gg.LineJoinRound
case "Bevel":
style.Join = gg.LineJoinBevel
}
if len(options.Dashes) > 0 {
dashes := make([]float64, len(options.Dashes))
for i, value := range options.Dashes {
dashes[i] = value * scale
}
style = style.WithDashPattern(dashes...).WithDashOffset(options.DashOffset * scale)
}
return style, nil
}
// ggRGBA 将标准库预乘颜色转换为gg的非预乘颜色
// 入参: c 预乘颜色
// 返回: gg.RGBA 非预乘颜色
func ggRGBA(c color.RGBA) gg.RGBA {
if c.A == 0 {
return gg.Transparent
}
a := float64(c.A)
return gg.RGBA{R: float64(c.R) / a, G: float64(c.G) / a, B: float64(c.B) / a, A: a / 255}
}