mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 16:25:49 +08:00
259 lines
8.2 KiB
Go
259 lines
8.2 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) {
|
|
return b.prepareShared(page, nil)
|
|
}
|
|
|
|
// PrepareReuse 复用同配置GG场景的派生数据,其他后端场景不参与复用
|
|
// 入参: page 新页面, previous 前次场景
|
|
// 返回: RasterScene 独立快照, error 准备错误
|
|
func (b GGBackend) PrepareReuse(page *RasterPage, previous RasterScene) (RasterScene, error) {
|
|
scene, _ := previous.(*ggRasterScene)
|
|
return b.prepareShared(page, scene)
|
|
}
|
|
|
|
// prepareShared 为新快照复用同配置的派生缓存,不改变旧快照
|
|
// 入参: page 新页面, previous 前次同渲染器场景
|
|
// 返回: *ggRasterScene 独立场景, error 准备错误
|
|
func (b GGBackend) prepareShared(page *RasterPage, previous *ggRasterScene) (*ggRasterScene, error) {
|
|
scene := &ggRasterScene{
|
|
mu: &sync.Mutex{},
|
|
backend: b,
|
|
masks: &renderCache[[32]byte, *image.Alpha]{limit: 16 << 20},
|
|
prepared: &renderCache[[32]byte, ggRasterCommand]{limit: 16 << 20},
|
|
images: &renderCache[image.Image, *gg.ImageBuf]{limit: 16 << 20},
|
|
}
|
|
if previous != nil && sameBackend(b.StrokeGeometry, previous.backend.StrokeGeometry) {
|
|
scene.mu, scene.masks, scene.prepared, scene.images = previous.mu, previous.masks, previous.prepared, previous.images
|
|
}
|
|
if err := scene.Update(page); err != nil {
|
|
return nil, err
|
|
}
|
|
return scene, nil
|
|
}
|
|
|
|
// Update 按内容复用未编辑的指令,成功后原子替换场景快照
|
|
// 入参: page 编辑后的只读页面
|
|
// 返回: error 准备错误,失败时保留原场景
|
|
func (scene *ggRasterScene) Update(page *RasterPage) error {
|
|
scene.mu.Lock()
|
|
defer scene.mu.Unlock()
|
|
w, h, err := page.PixelSize()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
page = cloneRasterPage(page)
|
|
commands := make([]ggRasterCommand, 0, len(page.Commands))
|
|
for i, cmd := range page.Commands {
|
|
if cmd.Image != nil {
|
|
cmd.Stroke = nil
|
|
prepared, err := ggPrepareImage(page, cmd, scene.masks, scene.images)
|
|
if err != nil {
|
|
return fmt.Errorf("gg command %d: %w", i, err)
|
|
}
|
|
commands = append(commands, prepared)
|
|
continue
|
|
}
|
|
key := rasterCommandKey(page, cmd)
|
|
if prepared, ok := scene.prepared.get(key); ok {
|
|
commands = append(commands, prepared)
|
|
continue
|
|
}
|
|
if cmd.Stroke != nil && !ggNativeStroke(cmd, page.DPI/25.4) {
|
|
cmd, err = scene.backend.expandStroke(cmd)
|
|
if err != nil {
|
|
return fmt.Errorf("gg command %d: %w", i, err)
|
|
}
|
|
}
|
|
m := page.PixelTransform(cmd.Transform, h)
|
|
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 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 {
|
|
brush, err := ggGradientBrush(g, m)
|
|
if err != nil {
|
|
return fmt.Errorf("gg command %d: %w", i, err)
|
|
}
|
|
paint.SetFillBrush(brush)
|
|
}
|
|
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 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 err
|
|
}
|
|
paint.SetStroke(style)
|
|
paint.SetStrokeBrush(paint.FillBrush())
|
|
}
|
|
prepared := ggRasterCommand{command: cmd, path: path, paint: paint}
|
|
scene.prepared.put(key, prepared, rasterCommandCost(cmd)+len(paint.ClipMask))
|
|
commands = append(commands, prepared)
|
|
}
|
|
scene.page, scene.width, scene.height, scene.commands = page, w, h, commands
|
|
return nil
|
|
}
|
|
|
|
// ggRasterCommand 保存已转换的像素路径和样式
|
|
type ggRasterCommand struct {
|
|
command RasterCommand
|
|
path *gg.Path
|
|
paint *gg.Paint
|
|
}
|
|
|
|
// ggRasterScene 复用路径和局部裁剪,串行保护掩码缓存
|
|
type ggRasterScene struct {
|
|
mu *sync.Mutex
|
|
backend GGBackend
|
|
page *RasterPage
|
|
width, height int
|
|
commands []ggRasterCommand
|
|
masks *renderCache[[32]byte, *image.Alpha]
|
|
prepared *renderCache[[32]byte, ggRasterCommand]
|
|
images *renderCache[image.Image, *gg.ImageBuf]
|
|
}
|
|
|
|
// 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 && cmd.path == 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}
|
|
}
|