feat(后端架构): 移除实验性GoGPU后端,保留多后端扩展能力
build.yaml / build (push) Successful in 40s

This commit is contained in:
xiaoqidun committed 2026-09-24 09:23:16 +08:00
1 parent 46e6d5b622
commit a1abe55726
15 files changed
+199 -1121

No files matched your search

+5 -4
View File
@@ -3339,16 +3339,17 @@ function displayMode() {
} }
function renderBackendLabel(name) { function renderBackendLabel(name) {
return { svg: "SVG", canvas: "Canvas", gg: "GoGPU", "gg+canvas": "GoGPU + Canvas" }[name] || name || "无"; return { svg: "SVG", canvas: "Canvas", ofd: "OFD" }[name] || name || "无";
} }
function updateRenderBackend() { function updateRenderBackend() {
el.renderBackend.value = state.renderBackend; el.renderBackend.value = state.renderBackend;
el.renderDPI.value = String(state.renderDPI); el.renderDPI.value = String(state.renderDPI);
el.renderBackend.disabled = !state.ready || state.exporting || document.body.hasAttribute("aria-busy"); const disabled = !state.ready || state.exporting || document.body.hasAttribute("aria-busy");
el.renderDPI.disabled = el.renderBackend.disabled || displayMode() === "svg"; el.renderBackend.disabled = disabled || Object.keys(state.renderBackends).length < 2;
el.renderDPI.disabled = disabled || displayMode() === "svg";
el.renderDPI.title = displayMode() === "svg" ? "矢量预览无需DPI" : "预览DPI(仅位图)"; el.renderDPI.title = displayMode() === "svg" ? "矢量预览无需DPI" : "预览DPI(仅位图)";
el.renderVectorButton.disabled = el.renderRasterButton.disabled = el.renderBackend.disabled || state.editing; el.renderVectorButton.disabled = el.renderRasterButton.disabled = disabled || state.editing;
el.renderVectorButton.setAttribute("aria-pressed", String(displayMode() === "svg")); el.renderVectorButton.setAttribute("aria-pressed", String(displayMode() === "svg"));
el.renderRasterButton.setAttribute("aria-pressed", String(displayMode() === "raster")); el.renderRasterButton.setAttribute("aria-pressed", String(displayMode() === "raster"));
el.renderVectorButton.title = state.editing ? "编辑使用矢量显示" : "矢量显示"; el.renderVectorButton.title = state.editing ? "编辑使用矢量显示" : "矢量显示";
-3
View File
@@ -6,7 +6,6 @@ require (
github.com/emmansun/gmsm v0.44.1 github.com/emmansun/gmsm v0.44.1
github.com/go-fonts/latin-modern v0.3.3 github.com/go-fonts/latin-modern v0.3.3
github.com/go-text/typesetting v0.3.5 github.com/go-text/typesetting v0.3.5
github.com/gogpu/gg v0.52.5
github.com/tdewolff/canvas v0.0.0-20260913163248-dd4999d1c76a github.com/tdewolff/canvas v0.0.0-20260913163248-dd4999d1c76a
github.com/tdewolff/font v0.0.0-20260913163313-54f98bb59ee6 github.com/tdewolff/font v0.0.0-20260913163313-54f98bb59ee6
github.com/xiaoqidun/jbig2 v1.1.1 github.com/xiaoqidun/jbig2 v1.1.1
@@ -25,8 +24,6 @@ require (
github.com/andybalholm/brotli v1.2.2 // indirect github.com/andybalholm/brotli v1.2.2 // indirect
github.com/benoitkugler/textlayout v0.3.2 // indirect github.com/benoitkugler/textlayout v0.3.2 // indirect
github.com/benoitkugler/textprocessing v0.0.6 // indirect github.com/benoitkugler/textprocessing v0.0.6 // indirect
github.com/gogpu/gpucontext v0.28.0 // indirect
github.com/gogpu/gputypes v0.5.2 // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect
github.com/srwiley/scanx v0.0.0-20190309010443-e94503791388 // indirect github.com/srwiley/scanx v0.0.0-20190309010443-e94503791388 // indirect
-14
View File
@@ -36,20 +36,6 @@ github.com/go-text/typesetting v0.3.5 h1:XZPUooClHY0Vf/rFyUyuPRNEkawARaFzLMQcXLS
github.com/go-text/typesetting v0.3.5/go.mod h1:XZO1hD+nQVyvVa5IicQk7FsCa4PFQaJ2soWAP1f//68= github.com/go-text/typesetting v0.3.5/go.mod h1:XZO1hD+nQVyvVa5IicQk7FsCa4PFQaJ2soWAP1f//68=
github.com/go-text/typesetting-utils v0.0.0-20260419141703-4ffe8874dabc h1:8FGo2It5K75XkavhTiCKExUfVaVDS1feBnLCru5qeoY= github.com/go-text/typesetting-utils v0.0.0-20260419141703-4ffe8874dabc h1:8FGo2It5K75XkavhTiCKExUfVaVDS1feBnLCru5qeoY=
github.com/go-text/typesetting-utils v0.0.0-20260419141703-4ffe8874dabc/go.mod h1:3/62I4La/HBRX9TcTpBj4eipLiwzf+vhI+7whTc9V7o= github.com/go-text/typesetting-utils v0.0.0-20260419141703-4ffe8874dabc/go.mod h1:3/62I4La/HBRX9TcTpBj4eipLiwzf+vhI+7whTc9V7o=
github.com/go-webgpu/goffi v0.6.3 h1:p4gKGikHBAQ/8iUiew9MV4C5M1ZGIsk8QGFGuJjMj+A=
github.com/go-webgpu/goffi v0.6.3/go.mod h1:wfoxNsJkU+5RFbV1kNN1kunhc1lFHuJKK3zpgx08/uM=
github.com/go-webgpu/webgpu v0.5.5 h1:pIrXzRg0LRlNjNmR+ZNo/ERN88YaObzbCY5oYhir5SA=
github.com/go-webgpu/webgpu v0.5.5/go.mod h1:vgIuNTa1UlZ4njCGY6Pmp/0c5T5o2Ml2fQ0znLc7B98=
github.com/gogpu/gg v0.52.5 h1:aDAxjwcUYWTaEcKiAVSX52FPtYKtMhuFYCQYK2SwJwE=
github.com/gogpu/gg v0.52.5/go.mod h1:mKiEGiR8lT7BCECfSoxRYBQ96HrDc0DHv/CNDxYQG5M=
github.com/gogpu/gpucontext v0.28.0 h1:W27+0PRnHLVRscDJ2L6r4B1JlaKjlV/EmL0Wxx8vGIc=
github.com/gogpu/gpucontext v0.28.0/go.mod h1:5rpKj+DpixgAFm8ArBXFXOZQRbeJSFncVMsqyyA9Rhc=
github.com/gogpu/gputypes v0.5.2 h1:3sbKI0+XW36Ekd3d4QhkbpdY7gbgMVgp8Q4+ZNdGhtE=
github.com/gogpu/gputypes v0.5.2/go.mod h1:cnXrDMwTpWTvJLW1Vreop3PcT6a2YP/i3s91rPaOavw=
github.com/gogpu/naga v0.18.0 h1:2y83HUcAnlwEZMuHOiYTMdNYM9J8rev40gkI4zJ96Uk=
github.com/gogpu/naga v0.18.0/go.mod h1:15sQaHKkbqXcwTN+hHYGLsA0WBBnkmYzne/eF5p5WEg=
github.com/gogpu/wgpu v0.31.6 h1:WIySgNZHQfUaNRCQ/AzaZpDkcZn5dggqXlRAgpWtVu4=
github.com/gogpu/wgpu v0.31.6/go.mod h1:r8z3uZOyjl95KXN1eyMxFnwwPMZ0yuBFrLFi8v37QGY=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
+4 -17
View File
@@ -126,32 +126,19 @@ type BackendInfo struct {
} }
// NewRenderBackends 创建内置后端组合,各项能力通过Info报告实际提供者 // NewRenderBackends 创建内置后端组合,各项能力通过Info报告实际提供者
// 入参: name canvas或gg // 入参: name 内置后端标识
// 返回: RenderBackends 后端组合, error 未知后端错误 // 返回: RenderBackends 后端组合, error 未知后端错误
func NewRenderBackends(name string) (RenderBackends, error) { func NewRenderBackends(name string) (RenderBackends, error) {
backends := defaultRenderBackends() if name != "canvas" {
switch name {
case "canvas":
case "gg":
backends.Fonts = GGBackend{}
backends.Geometry = GGGeometryBackend{GeometryBackend: backends.Geometry}
backends.Compiler = GGBackend{}
backends.Raster = GGBackend{StrokeGeometry: backends.Geometry}
default:
return RenderBackends{}, fmt.Errorf("unknown render backend %q", name) return RenderBackends{}, fmt.Errorf("unknown render backend %q", name)
} }
return backends, nil return defaultRenderBackends(), nil
} }
// RenderBackendInfos 列出内置组合及其实际能力,供原生程序和WASM共用 // RenderBackendInfos 列出内置组合及其实际能力,供原生程序和WASM共用
// 返回: map[string]BackendInfo 后端能力 // 返回: map[string]BackendInfo 后端能力
func RenderBackendInfos() map[string]BackendInfo { func RenderBackendInfos() map[string]BackendInfo {
result := make(map[string]BackendInfo, 2) return map[string]BackendInfo{"canvas": defaultRenderBackends().Info()}
for _, name := range []string{"canvas", "gg"} {
backends, _ := NewRenderBackends(name)
result[name] = backends.Info()
}
return result
} }
// defaultRenderBackends 创建独立的默认配置 // defaultRenderBackends 创建独立的默认配置
+145
View File
@@ -0,0 +1,145 @@
// 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"
"cmp"
"fmt"
"slices"
"strconv"
"strings"
"sync"
"unicode"
"unicode/utf8"
typefont "github.com/go-text/typesetting/font"
"github.com/go-text/typesetting/harfbuzz"
"github.com/go-text/typesetting/language"
xlanguage "golang.org/x/text/language"
)
// fontShaper 使用字体度量和原始数据提供可配置塑形,不依赖绘图后端
type fontShaper struct {
metrics FontMetrics
shapeMu sync.Mutex
shapeFont *harfbuzz.Font
shapeBuffer *harfbuzz.Buffer
}
// ShapeText 使用默认选项塑形单行文字,不改动原文定位
// 入参: value 单行原文, size 毫米字号
// 返回: []ShapedGlyph 定位字形, error 塑形错误
func (f *fontShaper) ShapeText(value string, size float64) ([]ShapedGlyph, error) {
return f.ShapeTextWithOptions(value, size, TextShapeOptions{})
}
// ShapeTextWithOptions 使用现有纯Go塑形引擎处理单一方向原文,保留逻辑簇和视觉位置
// 不执行混合方向分段,调用方应将双向段落拆为单向文字对象
// 入参: value 单行原文, size 毫米字号, options 塑形选项
// 返回: []ShapedGlyph 定位字形, error 字符或选项错误
func (f *fontShaper) ShapeTextWithOptions(value string, size float64, options TextShapeOptions) ([]ShapedGlyph, error) {
if !rasterPositive(size) || !utf8.ValidString(value) {
return nil, fmt.Errorf("invalid shaping text or size")
}
for _, char := range value {
if unicode.IsControl(char) || char == '\u2028' || char == '\u2029' {
return nil, fmt.Errorf("shaping requires a single text line")
}
}
props := harfbuzz.SegmentProperties{Direction: harfbuzz.LeftToRight}
switch options.Direction {
case "", "ltr":
case "rtl":
props.Direction = harfbuzz.RightToLeft
default:
return nil, fmt.Errorf("shaping direction %q: %w", options.Direction, ErrBackendUnavailable)
}
if options.Script != "" {
if len(options.Script) != 4 || strings.ContainsFunc(options.Script, func(c rune) bool { return c < 'A' || c > 'Z' && c < 'a' || c > 'z' }) {
return nil, fmt.Errorf("invalid shaping script %q", options.Script)
}
props.Script, _ = language.ParseScript(options.Script)
}
if options.Language != "" {
tag, err := xlanguage.Parse(options.Language)
if err != nil {
return nil, fmt.Errorf("shaping language: %w", err)
}
props.Language = language.NewLanguage(tag.String())
}
var features []harfbuzz.Feature
if options.Features != "" {
for _, setting := range strings.Split(options.Features, ",") {
setting = strings.TrimSpace(setting)
if strings.ContainsAny(setting, "[]") {
return nil, fmt.Errorf("shaping feature ranges: %w", ErrBackendUnavailable)
}
tag, value, hasValue := strings.Cut(setting, "=")
if len(tag) != 4 || strings.ContainsFunc(tag, func(c rune) bool { return c < '0' || c > '9' && c < 'A' || c > 'Z' && c < 'a' || c > 'z' }) {
return nil, fmt.Errorf("invalid shaping feature %q", setting)
}
if hasValue {
if _, err := strconv.ParseUint(value, 10, 32); err != nil {
return nil, fmt.Errorf("shaping feature %q: %w", setting, err)
}
}
feature, err := harfbuzz.ParseFeature(setting)
if err != nil {
return nil, fmt.Errorf("shaping feature %q: %w", setting, err)
}
if feature.Start != harfbuzz.FeatureGlobalStart || feature.End != harfbuzz.FeatureGlobalEnd {
return nil, fmt.Errorf("shaping feature ranges: %w", ErrBackendUnavailable)
}
features = append(features, feature)
}
}
f.shapeMu.Lock()
defer f.shapeMu.Unlock()
if f.shapeFont == nil {
face, err := typefont.ParseTTF(bytes.NewReader(f.metrics.Write()))
if err != nil {
return nil, fmt.Errorf("shaping font: %w", err)
}
f.shapeFont = harfbuzz.NewFont(face)
f.shapeBuffer = harfbuzz.NewBuffer()
}
buffer := f.shapeBuffer
buffer.Clear()
buffer.Props = props
runes := []rune(value)
buffer.AddRunes(runes, 0, len(runes))
buffer.GuessSegmentProperties()
buffer.Shape(f.shapeFont, features)
unit := size / float64(f.metrics.UnitsPerEm())
result := make([]ShapedGlyph, len(buffer.Info))
var x, y float64
for i, info := range buffer.Info {
if info.Glyph >= typefont.GID(f.metrics.NumGlyphs()) {
return nil, fmt.Errorf("invalid shaped glyph index %d", info.Glyph)
}
position := buffer.Pos[i]
result[i] = ShapedGlyph{Glyph: uint16(info.Glyph), Cluster: info.Cluster,
X: (x + float64(position.XOffset)) * unit, Y: -(y + float64(position.YOffset)) * unit,
Advance: float64(position.XAdvance) * unit}
if !finite(result[i].X) || !finite(result[i].Y) || !finite(result[i].Advance) {
return nil, fmt.Errorf("shaped position exceeds finite range")
}
x += float64(position.XAdvance)
y += float64(position.YAdvance)
}
slices.SortStableFunc(result, func(a, b ShapedGlyph) int { return cmp.Compare(a.Cluster, b.Cluster) })
return result, nil
}
-70
View File
@@ -16,9 +16,6 @@ package ofdgo
import ( import (
"container/list" "container/list"
"crypto/sha256"
"encoding/binary"
"math"
) )
// renderCache 按估算字节数限制派生渲染数据,淘汰不修改源资源 // renderCache 按估算字节数限制派生渲染数据,淘汰不修改源资源
@@ -85,70 +82,3 @@ func rasterCommandCost(command RasterCommand) int {
} }
return cost return cost
} }
// rasterCommandKey 按完整路径、样式、裁剪及像素配置标识非图片指令
// 入参: page 页面尺寸和DPI, command 绘制指令
// 返回: [32]byte 内容键
func rasterCommandKey(page *RasterPage, command RasterCommand) [32]byte {
h := sha256.New()
path, clip := rasterClipKey(page, command.Path), rasterClipKey(page, command.Clip)
h.Write(path[:])
h.Write(clip[:])
var data [8]byte
number := func(value float64) {
binary.LittleEndian.PutUint64(data[:], math.Float64bits(value))
h.Write(data[:])
}
flag := func(value bool) {
if value {
h.Write([]byte{1})
} else {
h.Write([]byte{0})
}
}
word := func(value string) {
number(float64(len(value)))
h.Write([]byte(value))
}
flag(command.Clip != nil)
flag(command.EvenOdd)
for _, value := range command.Transform {
number(value)
}
c := command.Paint.Color
h.Write([]byte{c.R, c.G, c.B, c.A})
flag(command.Stroke != nil)
if stroke := command.Stroke; stroke != nil {
number(stroke.Width)
number(stroke.MiterLimit)
number(stroke.Tolerance)
number(stroke.DashOffset)
word(stroke.Cap)
word(stroke.Join)
number(float64(len(stroke.Dashes)))
for _, dash := range stroke.Dashes {
number(dash)
}
}
flag(command.Paint.Gradient != nil)
if g := command.Paint.Gradient; g != nil {
h.Write([]byte{byte(g.Kind)})
for _, value := range []float64{g.Start.X, g.Start.Y, g.End.X, g.End.Y, g.R0, g.R1} {
number(value)
}
flag(g.Spread != nil)
if spread := g.Spread; spread != nil {
number(float64(spread.Extend))
number(spread.Period)
word(spread.MapType)
}
for _, stop := range g.Stops {
number(stop.Offset)
c := stop.Color
h.Write([]byte{c.R, c.G, c.B, c.A})
}
}
var key [32]byte
copy(key[:], h.Sum(nil))
return key
}
@@ -16,10 +16,17 @@ package ofdgo
import "image/color" import "image/color"
// OFDCompiler 使用库自有页面语义,字体和几何操作交给当前后端配置
type OFDCompiler struct{}
// Name 返回页面编译器标识
// 返回: string 编译器标识
func (OFDCompiler) Name() string { return "ofd" }
// CompilePage 使用公共OFD语义和配置的字形、几何能力编译页面 // CompilePage 使用公共OFD语义和配置的字形、几何能力编译页面
// 入参: r 渲染器, page 页面 // 入参: r 渲染器, page 页面
// 返回: *RasterPage 绘制页面, error 编译错误 // 返回: *RasterPage 绘制页面, error 编译错误
func (GGBackend) CompilePage(r *Renderer, page *PageContent) (*RasterPage, error) { func (OFDCompiler) CompilePage(r *Renderer, page *PageContent) (*RasterPage, error) {
box, err := r.GetPageBox(page) box, err := r.GetPageBox(page)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -46,10 +53,10 @@ func (GGBackend) CompilePage(r *Renderer, page *PageContent) (*RasterPage, error
return c.page, nil return c.page, nil
} }
// PageText 提取与GG页面绘制相同定位的原文,不读取图片像素 // PageText 提取与页面绘制相同定位的原文,不读取图片像素
// 入参: r 渲染器, page 页面 // 入参: r 渲染器, page 页面
// 返回: *PageText 页面文字, error 定位错误 // 返回: *PageText 页面文字, error 定位错误
func (GGBackend) PageText(r *Renderer, page *PageContent) (*PageText, error) { func (OFDCompiler) PageText(r *Renderer, page *PageContent) (*PageText, error) {
if _, err := r.GetPageBox(page); err != nil { if _, err := r.GetPageBox(page); err != nil {
return nil, err return nil, err
} }
@@ -67,7 +74,7 @@ func (GGBackend) PageText(r *Renderer, page *PageContent) (*PageText, error) {
// MeasureObject 复用文字和图形编译规则收集范围,不分配页面像素 // MeasureObject 复用文字和图形编译规则收集范围,不分配页面像素
// 入参: r 渲染器, object 对象, options 度量上下文 // 入参: r 渲染器, object 对象, options 度量上下文
// 返回: ObjectMeasurement 对象范围与轮廓, error 度量错误 // 返回: ObjectMeasurement 对象范围与轮廓, error 度量错误
func (GGBackend) MeasureObject(r *Renderer, object GraphicObject, options MeasureOptions) (ObjectMeasurement, error) { func (OFDCompiler) MeasureObject(r *Renderer, object GraphicObject, options MeasureOptions) (ObjectMeasurement, error) {
c, err := newSemanticCompiler(r) c, err := newSemanticCompiler(r)
if err != nil { if err != nil {
return ObjectMeasurement{}, err return ObjectMeasurement{}, err
+33 -1
View File
@@ -15,6 +15,8 @@
package ofdgo package ofdgo
import ( import (
"fmt"
"github.com/tdewolff/canvas" "github.com/tdewolff/canvas"
"github.com/tdewolff/font" "github.com/tdewolff/font"
) )
@@ -34,12 +36,15 @@ func (CanvasBackend) OpenFont(data []byte) (FontMetrics, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
return canvasFontMetrics{sfnt}, nil metrics := &canvasFontMetrics{SFNT: sfnt}
metrics.fontShaper = &fontShaper{metrics: metrics}
return metrics, nil
} }
// canvasFontMetrics 保留默认字体度量,写出时不更新原始时间 // canvasFontMetrics 保留默认字体度量,写出时不更新原始时间
type canvasFontMetrics struct { type canvasFontMetrics struct {
*font.SFNT *font.SFNT
*fontShaper
} }
// Write 返回字体数据,不修改字体元信息 // Write 返回字体数据,不修改字体元信息
@@ -52,9 +57,36 @@ func (f canvasFontMetrics) Write() []byte {
// 入参: glyph 字形编号, size 字号,单位为毫米 // 入参: glyph 字形编号, size 字号,单位为毫米
// 返回: GeometryPath 字形路径, error 字形解析错误 // 返回: GeometryPath 字形路径, error 字形解析错误
func (f canvasFontMetrics) GlyphOutline(glyph uint16, size float64) (GeometryPath, error) { func (f canvasFontMetrics) GlyphOutline(glyph uint16, size float64) (GeometryPath, error) {
if !rasterPositive(size) || glyph >= f.NumGlyphs() {
return nil, fmt.Errorf("invalid glyph or size")
}
path := &canvas.Path{} path := &canvas.Path{}
if err := f.GlyphPath(path, glyph, 0, 0, 0, size/float64(f.UnitsPerEm()), font.NoHinting); err != nil { if err := f.GlyphPath(path, glyph, 0, 0, 0, size/float64(f.UnitsPerEm()), font.NoHinting); err != nil {
return nil, err return nil, err
} }
return *geometryFromCanvasPath(path), nil return *geometryFromCanvasPath(path), nil
} }
// GlyphOutlines 批量提取字形轮廓,重复编号只解析一次
// 入参: glyphs 字形编号, size 毫米字号
// 返回: []GeometryPath 同序只读轮廓, error 轮廓错误
func (f canvasFontMetrics) GlyphOutlines(glyphs []uint16, size float64) ([]GeometryPath, error) {
if !rasterPositive(size) {
return nil, fmt.Errorf("invalid glyph size")
}
result := make([]GeometryPath, len(glyphs))
paths := make(map[uint16]GeometryPath, len(glyphs))
for i, glyph := range glyphs {
path, ok := paths[glyph]
if !ok {
var err error
path, err = f.GlyphOutline(glyph, size)
if err != nil {
return nil, err
}
paths[glyph] = path
}
result[i] = path
}
return result, nil
}
-300
View File
@@ -1,300 +0,0 @@
// 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"
"cmp"
"fmt"
"math"
"slices"
"strconv"
"strings"
"sync"
"unicode"
"unicode/utf8"
typefont "github.com/go-text/typesetting/font"
"github.com/go-text/typesetting/harfbuzz"
"github.com/go-text/typesetting/language"
ggtext "github.com/gogpu/gg/text"
xlanguage "golang.org/x/text/language"
)
// ggFontMetrics 适配GG设计单位度量与字形轮廓,按需缓存可配置塑形器
type ggFontMetrics struct {
font ggtext.ParsedFont
data []byte
source *ggtext.FontSource
shaper *ggtext.OwnShaper
shapeMu sync.Mutex
shapeFont *harfbuzz.Font
shapeBuffer *harfbuzz.Buffer
}
// ResolveFont 使用公共字体来源匹配,不创建Canvas字体
// 入参: r 渲染器, id 字体ID, exact 是否禁止无关回退
// 返回: ResolvedFont 字体及来源, error 解析错误
func (b GGBackend) ResolveFont(r *Renderer, id string, exact bool) (ResolvedFont, error) {
return r.resolveFontSource(b, id, exact)
}
// OpenFont 通过GG解析独立字体,保留原始SFNT编码
// 入参: data 字体数据
// 返回: FontMetrics 字体度量, error 解析错误
func (GGBackend) OpenFont(data []byte) (FontMetrics, error) {
data = bytes.Clone(data)
source, err := ggtext.NewFontSource(data)
if err != nil {
return nil, err
}
return &ggFontMetrics{font: source.Parsed(), data: data, source: source, shaper: ggtext.NewOwnShaper()}, nil
}
// ShapeText 使用GG执行横向字偶距与连字塑形,不改动原文定位
// 当前支持拉丁、希腊、西里尔和东亚文字,复杂文字可通过ShapeTextWithOptions指定脚本与方向
// 入参: value 单行原文, size 毫米字号
// 返回: []ShapedGlyph 定位字形, error 字符或能力错误
func (f *ggFontMetrics) ShapeText(value string, size float64) ([]ShapedGlyph, error) {
if !rasterPositive(size) || !utf8.ValidString(value) {
return nil, fmt.Errorf("invalid shaping text or size")
}
for _, char := range value {
if unicode.IsControl(char) || char == '\u2028' || char == '\u2029' {
return nil, fmt.Errorf("shaping requires a single text line")
}
if !unicode.In(char, unicode.Latin, unicode.Greek, unicode.Cyrillic, unicode.Han, unicode.Hiragana, unicode.Katakana, unicode.Hangul, unicode.Common, unicode.Inherited) {
return nil, fmt.Errorf("GG shaping for U+%04X: %w", char, ErrBackendUnavailable)
}
}
glyphs := f.shaper.Shape(value, f.source.Face(size))
result := make([]ShapedGlyph, len(glyphs))
for i, glyph := range glyphs {
result[i] = ShapedGlyph{Glyph: uint16(glyph.GID), Cluster: glyph.Cluster, X: glyph.X, Y: -glyph.Y, Advance: glyph.XAdvance}
}
return result, nil
}
// ShapeTextWithOptions 使用现有纯Go塑形引擎处理单一方向原文,保留逻辑簇和视觉位置
// 不执行混合方向分段,调用方应将双向段落拆为单向文字对象
// 入参: value 单行原文, size 毫米字号, options 塑形选项
// 返回: []ShapedGlyph 定位字形, error 字符或选项错误
func (f *ggFontMetrics) ShapeTextWithOptions(value string, size float64, options TextShapeOptions) ([]ShapedGlyph, error) {
if options == (TextShapeOptions{}) {
return f.ShapeText(value, size)
}
if !rasterPositive(size) || !utf8.ValidString(value) {
return nil, fmt.Errorf("invalid shaping text or size")
}
for _, char := range value {
if unicode.IsControl(char) || char == '\u2028' || char == '\u2029' {
return nil, fmt.Errorf("shaping requires a single text line")
}
}
props := harfbuzz.SegmentProperties{Direction: harfbuzz.LeftToRight}
switch options.Direction {
case "", "ltr":
case "rtl":
props.Direction = harfbuzz.RightToLeft
default:
return nil, fmt.Errorf("shaping direction %q: %w", options.Direction, ErrBackendUnavailable)
}
if options.Script != "" {
if len(options.Script) != 4 || strings.ContainsFunc(options.Script, func(c rune) bool { return c < 'A' || c > 'Z' && c < 'a' || c > 'z' }) {
return nil, fmt.Errorf("invalid shaping script %q", options.Script)
}
props.Script, _ = language.ParseScript(options.Script)
}
if options.Language != "" {
tag, err := xlanguage.Parse(options.Language)
if err != nil {
return nil, fmt.Errorf("shaping language: %w", err)
}
props.Language = language.NewLanguage(tag.String())
}
var features []harfbuzz.Feature
if options.Features != "" {
for _, setting := range strings.Split(options.Features, ",") {
setting = strings.TrimSpace(setting)
if strings.ContainsAny(setting, "[]") {
return nil, fmt.Errorf("shaping feature ranges: %w", ErrBackendUnavailable)
}
tag, value, hasValue := strings.Cut(setting, "=")
if len(tag) != 4 || strings.ContainsFunc(tag, func(c rune) bool { return c < '0' || c > '9' && c < 'A' || c > 'Z' && c < 'a' || c > 'z' }) {
return nil, fmt.Errorf("invalid shaping feature %q", setting)
}
if hasValue {
if _, err := strconv.ParseUint(value, 10, 32); err != nil {
return nil, fmt.Errorf("shaping feature %q: %w", setting, err)
}
}
feature, err := harfbuzz.ParseFeature(setting)
if err != nil {
return nil, fmt.Errorf("shaping feature %q: %w", setting, err)
}
if feature.Start != harfbuzz.FeatureGlobalStart || feature.End != harfbuzz.FeatureGlobalEnd {
return nil, fmt.Errorf("shaping feature ranges: %w", ErrBackendUnavailable)
}
features = append(features, feature)
}
}
f.shapeMu.Lock()
defer f.shapeMu.Unlock()
if f.shapeFont == nil {
face, err := typefont.ParseTTF(bytes.NewReader(f.data))
if err != nil {
return nil, fmt.Errorf("shaping font: %w", err)
}
f.shapeFont = harfbuzz.NewFont(face)
f.shapeBuffer = harfbuzz.NewBuffer()
}
buffer := f.shapeBuffer
buffer.Clear()
buffer.Props = props
runes := []rune(value)
buffer.AddRunes(runes, 0, len(runes))
buffer.GuessSegmentProperties()
buffer.Shape(f.shapeFont, features)
unit := size / float64(f.UnitsPerEm())
result := make([]ShapedGlyph, len(buffer.Info))
var x, y float64
for i, info := range buffer.Info {
if info.Glyph >= typefont.GID(f.NumGlyphs()) {
return nil, fmt.Errorf("invalid shaped glyph index %d", info.Glyph)
}
position := buffer.Pos[i]
result[i] = ShapedGlyph{Glyph: uint16(info.Glyph), Cluster: info.Cluster,
X: (x + float64(position.XOffset)) * unit, Y: -(y + float64(position.YOffset)) * unit,
Advance: float64(position.XAdvance) * unit}
if !finite(result[i].X) || !finite(result[i].Y) || !finite(result[i].Advance) {
return nil, fmt.Errorf("shaped position exceeds finite range")
}
x += float64(position.XAdvance)
y += float64(position.YAdvance)
}
slices.SortStableFunc(result, func(a, b ShapedGlyph) int { return cmp.Compare(a.Cluster, b.Cluster) })
return result, nil
}
// UnitsPerEm 返回字体设计单位
// 返回: uint16 每字单位数
func (f *ggFontMetrics) UnitsPerEm() uint16 { return uint16(f.font.UnitsPerEm()) }
// NumGlyphs 返回字形数量
// 返回: uint16 字形数量
func (f *ggFontMetrics) NumGlyphs() uint16 { return uint16(f.font.NumGlyphs()) }
// GlyphIndex 返回字符对应的字形编号
// 入参: character 字符
// 返回: uint16 字形编号
func (f *ggFontMetrics) GlyphIndex(character rune) uint16 { return f.font.GlyphIndex(character) }
// GlyphAdvance 返回字形设计单位推进宽度
// 入参: glyph 字形编号
// 返回: uint16 推进宽度
func (f *ggFontMetrics) GlyphAdvance(glyph uint16) uint16 {
return uint16(math.Round(f.font.GlyphAdvance(glyph, float64(f.font.UnitsPerEm()))))
}
// VerticalMetrics 返回非负的上升、下降与行间隙
// 返回: uint16 上升高度, uint16 下降高度, uint16 行间隙
func (f *ggFontMetrics) VerticalMetrics() (uint16, uint16, uint16) {
m := f.font.Metrics(float64(f.font.UnitsPerEm()))
return uint16(math.Round(math.Max(0, m.Ascent))), uint16(math.Round(math.Max(0, -m.Descent))), uint16(math.Round(math.Max(0, m.LineGap)))
}
// Write 返回独立字体数据,不更新字体元信息
// 返回: []byte SFNT数据
func (f *ggFontMetrics) Write() []byte { return bytes.Clone(f.data) }
// GlyphOutline 返回基线原点、纵轴向下的字形路径
// 入参: glyph 字形编号, size 字号,单位为毫米
// 返回: GeometryPath 字形路径, error 轮廓解析错误
func (f *ggFontMetrics) GlyphOutline(glyph uint16, size float64) (GeometryPath, error) {
if !rasterPositive(size) || int(glyph) >= f.font.NumGlyphs() {
return nil, fmt.Errorf("invalid glyph or size")
}
outline, err := ggtext.NewOutlineExtractor().ExtractOutline(f.font, ggtext.GlyphID(glyph), size)
if err != nil || outline == nil {
return nil, err
}
return ggOutlinePath(outline)
}
// GlyphOutlines 按指定编号批量提取轮廓,重复字形只解析一次
// 入参: glyphs 字形编号, size 毫米字号
// 返回: []GeometryPath 同序只读轮廓, error 轮廓解析错误
func (f *ggFontMetrics) GlyphOutlines(glyphs []uint16, size float64) ([]GeometryPath, error) {
if !rasterPositive(size) {
return nil, fmt.Errorf("invalid glyph size")
}
for _, glyph := range glyphs {
if int(glyph) >= f.font.NumGlyphs() {
return nil, fmt.Errorf("invalid glyph index %d", glyph)
}
}
result := make([]GeometryPath, len(glyphs))
paths := make(map[uint16]GeometryPath, len(glyphs))
for i, glyph := range glyphs {
path, ok := paths[glyph]
if !ok {
var err error
path, err = f.GlyphOutline(glyph, size)
if err != nil {
return nil, err
}
paths[glyph] = path
}
result[i] = path
}
return result, nil
}
// ggOutlinePath 将GG轮廓转换为闭合的公共字形路径
// 入参: outline GG字形轮廓
// 返回: GeometryPath 公共路径, error 未知轮廓指令
func ggOutlinePath(outline *ggtext.GlyphOutline) (GeometryPath, error) {
capacity := len(outline.Segments)
for _, segment := range outline.Segments {
if segment.Op == ggtext.OutlineOpMoveTo {
capacity++
}
}
result := make(GeometryPath, 0, capacity)
point := func(p ggtext.OutlinePoint) Point { return Point{X: float64(p.X), Y: float64(p.Y)} }
for _, s := range outline.Segments {
segment := GeometrySegment{End: point(s.Points[0])}
switch s.Op {
case ggtext.OutlineOpMoveTo:
if len(result) > 0 {
result = append(result, GeometrySegment{Verb: GeometryClose})
}
segment.Verb = GeometryMove
case ggtext.OutlineOpLineTo:
segment.Verb = GeometryLine
case ggtext.OutlineOpQuadTo:
segment.Verb, segment.Control1, segment.End = GeometryQuad, point(s.Points[0]), point(s.Points[1])
case ggtext.OutlineOpCubicTo:
segment.Verb, segment.Control1, segment.Control2, segment.End = GeometryCubic, point(s.Points[0]), point(s.Points[1]), point(s.Points[2])
default:
return nil, fmt.Errorf("unsupported GG outline command %d", s.Op)
}
result = append(result, segment)
}
if len(result) > 0 {
result = append(result, GeometrySegment{Verb: GeometryClose})
}
return result, nil
}
+1 -4
View File
@@ -21,13 +21,10 @@ import (
"github.com/tdewolff/canvas" "github.com/tdewolff/canvas"
) )
// canvasNativeStroke 判断描边是否仍由Canvas提供,组合几何不改变矢量描边编码 // canvasNativeStroke 判断描边是否由Canvas提供
// 入参: geometry 几何后端 // 入参: geometry 几何后端
// 返回: bool 是否采用原生描边 // 返回: bool 是否采用原生描边
func canvasNativeStroke(geometry GeometryBackend) bool { func canvasNativeStroke(geometry GeometryBackend) bool {
if combined, ok := geometry.(GGGeometryBackend); ok {
return canvasNativeStroke(combined.GeometryBackend)
}
_, ok := geometry.(CanvasBackend) _, ok := geometry.(CanvasBackend)
return ok return ok
} }
-138
View File
@@ -1,138 +0,0 @@
// 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 (
"errors"
"fmt"
)
// GGGeometryBackend 使用公共边界、变换和描边,区域运算由显式配置的几何后端承担
// 未配置GeometryBackend时仍可独立处理路径、曲线和描边
type GGGeometryBackend struct{ GeometryBackend }
// Path 解析库自有路径并使用公共几何变换
// 入参: object 路径对象
// 返回: GeometryPath 页面路径, error 参数或变换错误
func (b GGGeometryBackend) Path(object PathObject) (GeometryPath, error) {
return objectGeometryPath(b, object)
}
// Name 返回实际几何提供者组合
// 返回: string 后端组合标识
func (b GGGeometryBackend) Name() string {
if b.GeometryBackend == nil {
return "gg"
}
return "gg+" + backendName(b.GeometryBackend)
}
// Curves 使用公共弧线转换,不调用组合几何后端
// 入参: path 页面路径
// 返回: GeometryPath 贝塞尔路径, error 能力或路径错误
func (b GGGeometryBackend) Curves(path GeometryPath) (GeometryPath, error) {
return path.Curves(.0001)
}
// Bounds 使用公共解析极值,不依赖组合几何后端
// 入参: path 页面路径
// 返回: Box 受浮点舍入影响的解析范围, error 路径或数值错误
func (b GGGeometryBackend) Bounds(path GeometryPath) (Box, error) {
return path.Bounds()
}
// Transform 使用公共仿射变换,保持椭圆弧和退化投影的折返
// 入参: path 页面路径, matrix 页面变换
// 返回: GeometryPath 新路径, error 路径或变换错误
func (b GGGeometryBackend) Transform(path GeometryPath, matrix Matrix) (GeometryPath, error) {
return path.Transform(matrix)
}
// Stroke 为后续区域运算提供描边轮廓,复杂轮廓交给显式补充后端
// 仅单条实线平头描边的公共轮廓无重叠,其他分片不能可靠进入补充后端的布尔运算
// 无补充后端时仍返回公共非零填充轮廓,但不提供区域运算能力
// 入参: path 页面路径, options 描边样式
// 返回: GeometryPath 非零填充轮廓, error 参数或精度限制错误
func (b GGGeometryBackend) Stroke(path GeometryPath, options StrokeOptions) (GeometryPath, error) {
if b.GeometryBackend != nil && !geometrySimpleStroke(path, options) {
if err := path.validate(); err != nil {
return nil, err
}
if err := validateStroke(options); err != nil {
return nil, err
}
return b.GeometryBackend.Stroke(path, options)
}
return b.strokeFill(path, options)
}
// strokeFill 仅供直接填充使用公共展开,不将重叠轮廓送入布尔运算
// 入参: path 页面路径, options 描边样式
// 返回: GeometryPath 非零填充轮廓, error 参数或能力错误
func (b GGGeometryBackend) strokeFill(path GeometryPath, options StrokeOptions) (GeometryPath, error) {
outline, err := path.Stroke(options)
if errors.Is(err, ErrBackendUnavailable) && b.GeometryBackend != nil {
return b.GeometryBackend.Stroke(path, options)
}
return outline, err
}
// geometrySimpleStroke 判断公共描边是否保证生成单个无重叠矩形
// 入参: path 路径, options 描边样式
// 返回: bool 是否可直接参与区域运算
func geometrySimpleStroke(path GeometryPath, options StrokeOptions) bool {
return len(path) == 2 && path[0].Verb == GeometryMove && path[1].Verb == GeometryLine &&
(options.Cap == "" || options.Cap == "Butt") && len(options.Dashes) == 0
}
// Region 将区域解析交给显式提供者,缺失能力时返回错误
// 入参: region 区域, matrix 页面变换
// 返回: GeometryPath 区域路径, error 能力或解析错误
func (b GGGeometryBackend) Region(region *Region, matrix Matrix) (GeometryPath, error) {
if b.GeometryBackend == nil {
return nil, fmt.Errorf("GG region geometry: %w", ErrBackendUnavailable)
}
return b.GeometryBackend.Region(region, matrix)
}
// Normalize 将填充规则整理交给显式提供者
// 入参: path 路径, evenOdd 是否采用奇偶规则
// 返回: GeometryPath 非零填充轮廓, error 能力或几何错误
func (b GGGeometryBackend) Normalize(path GeometryPath, evenOdd bool) (GeometryPath, error) {
if b.GeometryBackend == nil {
return nil, fmt.Errorf("GG normalize geometry: %w", ErrBackendUnavailable)
}
return b.GeometryBackend.Normalize(path, evenOdd)
}
// Combine 将布尔运算交给显式提供者
// 入参: left、right 操作数, operation 运算类型
// 返回: GeometryPath 结果路径, error 能力或几何错误
func (b GGGeometryBackend) Combine(left, right GeometryPath, operation GeometryOperation) (GeometryPath, error) {
if b.GeometryBackend == nil {
return nil, fmt.Errorf("GG boolean geometry: %w", ErrBackendUnavailable)
}
return b.GeometryBackend.Combine(left, right, operation)
}
// Clip 将裁剪解析交给显式提供者
// 入参: renderer 渲染器, clips 裁剪, matrix 页面变换, parent 父裁剪
// 返回: *GeometryPath 裁剪路径, error 能力或几何错误
func (b GGGeometryBackend) Clip(renderer *Renderer, clips *Clips, matrix Matrix, parent *GeometryPath) (*GeometryPath, error) {
if b.GeometryBackend == nil {
return nil, fmt.Errorf("GG clip geometry: %w", ErrBackendUnavailable)
}
return b.GeometryBackend.Clip(renderer, clips, matrix, parent)
}
-258
View File
@@ -1,258 +0,0 @@
// 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}
}
-116
View File
@@ -1,116 +0,0 @@
// 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/color"
"math"
"github.com/gogpu/gg"
)
// ggGradientBrush 仅在颜色插值及延伸等价时使用原生渐变,否则保留OFD采样
// 入参: gradient 局部渐变, matrix 局部到像素变换
// 返回: gg.Brush 画刷, error 类型或变换错误
func ggGradientBrush(gradient *RasterGradient, matrix RasterMatrix) (gg.Brush, error) {
if gradient.Kind != RasterLinear && gradient.Kind != RasterRadial {
return nil, fmt.Errorf("unsupported gradient %d", gradient.Kind)
}
inverse, err := matrix.Inverse()
if err != nil {
return nil, err
}
if brush := ggNativeGradient(gradient, matrix, inverse); brush != nil {
return brush, nil
}
return gg.NewCustomBrush(func(x, y float64) gg.RGBA {
p := inverse.Apply(RasterPoint{X: x, Y: y})
return ggRGBA(gradient.At(p.X, p.Y))
}), nil
}
// ggNativeGradient 将恒定非预乘色的透明度渐变映射为GG原生画刷
// GG使用线性光颜色插值,不接受改变色相、重复节点或单侧透明延伸的OFD渐变
// 入参: g 局部渐变, matrix 正变换, inverse 逆变换
// 返回: gg.Brush 等价原生画刷,不匹配时为空
func ggNativeGradient(g *RasterGradient, matrix, inverse RasterMatrix) gg.Brush {
if len(g.Stops) < 2 {
return nil
}
var reference color.RGBA
for i, stop := range g.Stops {
if !finite(stop.Offset) || stop.Offset < 0 || stop.Offset > 1 || i > 0 && stop.Offset <= g.Stops[i-1].Offset {
return nil
}
if stop.Color.A > reference.A {
reference = stop.Color
}
}
if reference.A == 0 {
return nil
}
for _, stop := range g.Stops {
c := stop.Color
if uint32(c.R)*uint32(reference.A) != uint32(reference.R)*uint32(c.A) ||
uint32(c.G)*uint32(reference.A) != uint32(reference.G)*uint32(c.A) ||
uint32(c.B)*uint32(reference.A) != uint32(reference.B)*uint32(c.A) {
return nil
}
}
extend, period := gg.ExtendPad, 1.0
if spread := g.Spread; spread != nil {
if spread.Extend != 3 {
return nil
}
switch spread.MapType {
case "Repeat":
extend = gg.ExtendRepeat
case "Reflect":
extend = gg.ExtendReflect
}
if extend != gg.ExtendPad && spread.Period > 0 {
period = spread.Period
}
}
stops := make([]gg.ColorStop, len(g.Stops))
for i, stop := range g.Stops {
c := ggRGBA(reference)
c.A = float64(stop.Color.A) / 255
stops[i] = gg.ColorStop{Offset: stop.Offset, Color: c}
}
start := matrix.Apply(g.Start)
if g.Kind == RasterLinear {
dx, dy := g.End.X-g.Start.X, g.End.Y-g.Start.Y
length := dx*dx + dy*dy
if !rasterPositive(length) {
return nil
}
// 渐变参数为协向量,非等比缩放或错切不能直接变换两个端点
x, y := (inverse[0]*dx+inverse[1]*dy)/length, (inverse[2]*dx+inverse[3]*dy)/length
factor := period / (x*x + y*y)
brush := gg.NewLinearGradientBrush(start.X, start.Y, start.X+x*factor, start.Y+y*factor)
brush.Stops, brush.Extend = stops, extend
return brush
}
xscale, yscale := math.Hypot(matrix[0], matrix[1]), math.Hypot(matrix[2], matrix[3])
if g.Start != g.End || g.R0 != 0 || !rasterPositive(g.R1) ||
math.Abs(xscale-yscale) > 1e-12*xscale || math.Abs(matrix[0]*matrix[2]+matrix[1]*matrix[3]) > 1e-12*xscale*yscale {
return nil
}
brush := gg.NewRadialGradientBrush(start.X, start.Y, 0, g.R1*xscale*period)
brush.Stops, brush.Extend = stops, extend
return brush
}
-86
View File
@@ -1,86 +0,0 @@
// 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"
"math"
"github.com/gogpu/gg"
)
// ggNativeImage 判断GG最近邻采样与公共高质量采样是否等价
// 仅接受整数像素平移及不会重复预乘的八位图片,缩放、错切和旋转沿用公共路径
// 入参: src 源像素, matrix 像素变换
// 返回: bool 是否可使用原生图片
func ggNativeImage(src image.Image, matrix RasterMatrix) bool {
if matrix[0] != 1 || matrix[1] != 0 || matrix[2] != 0 || matrix[3] != 1 ||
!finite(matrix[4]) || !finite(matrix[5]) || matrix[4] != math.Trunc(matrix[4]) || matrix[5] != math.Trunc(matrix[5]) || src.Bounds().Empty() {
return false
}
switch src := src.(type) {
case *image.NRGBA, *image.Gray:
return true
case *image.RGBA:
return src.Opaque()
}
return false
}
// ggPrepareImage 复用GG图片采样器和裁剪,不能等价采样时保留公共图像指令
// 工厂方法只构造采样器,不创建上下文设备或调用可能隐藏错误的DrawImageEx
// 入参: page 页面, command 图片指令, masks 裁剪缓存, images 图片适配缓存
// 返回: ggRasterCommand 预编译图片, error 裁剪错误
func ggPrepareImage(page *RasterPage, command RasterCommand, masks *renderCache[[32]byte, *image.Alpha], images *renderCache[image.Image, *gg.ImageBuf]) (ggRasterCommand, error) {
result := ggRasterCommand{command: command}
_, height, _ := page.PixelSize()
matrix := page.PixelTransform(command.Transform, height)
src := imagePixelSource(command.Image)
if !ggNativeImage(src, matrix) {
return result, nil
}
buffer, ok := images.get(src)
if !ok {
buffer = gg.ImageBufFromImage(src)
images.put(src, buffer, src.Bounds().Dx()*src.Bounds().Dy()*4+256)
}
bounds := src.Bounds()
x, y := matrix[4]+float64(bounds.Min.X), matrix[5]+float64(bounds.Min.Y)
pattern := new(gg.Context).CreateImagePattern(buffer, 0, 0, bounds.Dx(), bounds.Dy()).(*gg.ImagePattern)
pattern.SetClamp(true)
pattern.SetTransform(gg.Translate(x, y))
paint := gg.NewPaint()
paint.SetFillBrush(gg.BrushFromPattern(pattern))
if command.Clip != nil {
mask, err := rasterClipMask(GGBackend{}, page, command.Clip, masks)
if err != nil {
return result, err
}
if mask.Rect.Empty() {
result.path, result.paint = gg.NewPath(), paint
return result, nil
}
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
}
path := gg.NewPath()
path.MoveTo(x, y)
path.LineTo(x+float64(bounds.Dx()), y)
path.LineTo(x+float64(bounds.Dx()), y+float64(bounds.Dy()))
path.LineTo(x, y+float64(bounds.Dy()))
path.Close()
result.path, result.paint = path, paint
return result, nil
}
-106
View File
@@ -1,106 +0,0 @@
// 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"
// ggStrokeFiller 将仅用于光栅填充的描边与可参与区域运算的轮廓分开
type ggStrokeFiller interface {
strokeFill(path GeometryPath, options StrokeOptions) (GeometryPath, error)
}
// ggNativeStroke 判断GG当前可保真处理的描边范围
// 入参: command 描边指令, scale 每毫米像素数
// 返回: bool 是否可使用原生描边
func ggNativeStroke(command RasterCommand, scale float64) bool {
if command.Paint.Gradient != nil && command.Stroke.Width*scale < 1 {
return false
}
joins := command.Stroke.Join == "" || command.Stroke.Join == "Miter"
dashed := false
for _, value := range command.Stroke.Dashes {
dashed = dashed || value > 0
}
segments := 0
for _, segment := range command.Path {
switch segment.Verb {
case RasterMove:
segments = 0
case RasterLine, RasterQuad, RasterCubic:
segments++
if joins && segments > 1 {
return false
}
if dashed && segment.Verb != RasterLine {
return false
}
case RasterClose:
if dashed || joins {
return false
}
}
}
return true
}
// expandStroke 使用显式几何提供者展开GG尚不可靠的描边,不切换光栅后端
// 入参: command 描边指令
// 返回: RasterCommand 等价填充指令, error 能力或几何错误
func (b GGBackend) expandStroke(command RasterCommand) (RasterCommand, error) {
if b.StrokeGeometry == nil {
return RasterCommand{}, fmt.Errorf("GG stroke geometry: %w", ErrBackendUnavailable)
}
values := command.Transform
matrix := Matrix{a: values[0], b: values[1], c: values[2], d: values[3], e: values[4], f: values[5]}
path, err := b.StrokeGeometry.Transform(geometryFromRaster(command.Path), matrix)
if err != nil {
return RasterCommand{}, err
}
if filler, ok := b.StrokeGeometry.(ggStrokeFiller); ok {
path, err = filler.strokeFill(path, *command.Stroke)
} else {
path, err = b.StrokeGeometry.Stroke(path, *command.Stroke)
}
if err != nil {
return RasterCommand{}, err
}
path, err = path.Curves(.0001)
if err != nil {
return RasterCommand{}, err
}
if command.Paint.Gradient != nil {
inverse, ok := matrix.Invert()
if !ok {
return RasterCommand{}, fmt.Errorf("invalid stroke gradient transform")
}
path, err = b.StrokeGeometry.Transform(path, inverse)
if err != nil {
return RasterCommand{}, err
}
} else {
command.Transform = RasterMatrix{1, 0, 0, 1, 0, 0}
}
command.Path = make([]RasterSegment, len(path))
for i, s := range path {
verb := RasterVerb(s.Verb)
if s.Verb == GeometryClose {
verb = RasterClose
}
command.Path[i] = RasterSegment{Verb: verb, End: RasterPoint{s.End.X, s.End.Y}, Control1: RasterPoint{s.Control1.X, s.Control1.Y}, Control2: RasterPoint{s.Control2.X, s.Control2.Y}}
}
command.Stroke = nil
command.EvenOdd = false
return command, nil
}