diff --git a/assets/webui/ofdgo.js b/assets/webui/ofdgo.js index 81acfc1..82be778 100644 --- a/assets/webui/ofdgo.js +++ b/assets/webui/ofdgo.js @@ -3339,16 +3339,17 @@ function displayMode() { } 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() { el.renderBackend.value = state.renderBackend; el.renderDPI.value = String(state.renderDPI); - el.renderBackend.disabled = !state.ready || state.exporting || document.body.hasAttribute("aria-busy"); - el.renderDPI.disabled = el.renderBackend.disabled || displayMode() === "svg"; + const disabled = !state.ready || state.exporting || document.body.hasAttribute("aria-busy"); + el.renderBackend.disabled = disabled || Object.keys(state.renderBackends).length < 2; + el.renderDPI.disabled = disabled || displayMode() === "svg"; 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.renderRasterButton.setAttribute("aria-pressed", String(displayMode() === "raster")); el.renderVectorButton.title = state.editing ? "编辑使用矢量显示" : "矢量显示"; diff --git a/go.mod b/go.mod index b51b0ce..b30e978 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,6 @@ require ( github.com/emmansun/gmsm v0.44.1 github.com/go-fonts/latin-modern v0.3.3 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/font v0.0.0-20260913163313-54f98bb59ee6 github.com/xiaoqidun/jbig2 v1.1.1 @@ -25,8 +24,6 @@ require ( github.com/andybalholm/brotli v1.2.2 // indirect github.com/benoitkugler/textlayout v0.3.2 // 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/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect github.com/srwiley/scanx v0.0.0-20190309010443-e94503791388 // indirect diff --git a/go.sum b/go.sum index fdea165..09f566e 100644 --- a/go.sum +++ b/go.sum @@ -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-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-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/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= diff --git a/ofdgo_backend.go b/ofdgo_backend.go index 0e48b4c..0365b57 100644 --- a/ofdgo_backend.go +++ b/ofdgo_backend.go @@ -126,32 +126,19 @@ type BackendInfo struct { } // NewRenderBackends 创建内置后端组合,各项能力通过Info报告实际提供者 -// 入参: name canvas或gg +// 入参: name 内置后端标识 // 返回: RenderBackends 后端组合, error 未知后端错误 func NewRenderBackends(name string) (RenderBackends, error) { - backends := defaultRenderBackends() - 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: + if name != "canvas" { return RenderBackends{}, fmt.Errorf("unknown render backend %q", name) } - return backends, nil + return defaultRenderBackends(), nil } // RenderBackendInfos 列出内置组合及其实际能力,供原生程序和WASM共用 // 返回: map[string]BackendInfo 后端能力 func RenderBackendInfos() map[string]BackendInfo { - result := make(map[string]BackendInfo, 2) - for _, name := range []string{"canvas", "gg"} { - backends, _ := NewRenderBackends(name) - result[name] = backends.Info() - } - return result + return map[string]BackendInfo{"canvas": defaultRenderBackends().Info()} } // defaultRenderBackends 创建独立的默认配置 diff --git a/ofdgo_font_shape.go b/ofdgo_font_shape.go new file mode 100644 index 0000000..91a2f37 --- /dev/null +++ b/ofdgo_font_shape.go @@ -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 +} diff --git a/ofdgo_render_cache.go b/ofdgo_render_cache.go index ccca6c2..3e03cc8 100644 --- a/ofdgo_render_cache.go +++ b/ofdgo_render_cache.go @@ -16,9 +16,6 @@ package ofdgo import ( "container/list" - "crypto/sha256" - "encoding/binary" - "math" ) // renderCache 按估算字节数限制派生渲染数据,淘汰不修改源资源 @@ -85,70 +82,3 @@ func rasterCommandCost(command RasterCommand) int { } 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 -} diff --git a/ofdgo_renderer_compile_gg.go b/ofdgo_renderer_compile.go similarity index 81% rename from ofdgo_renderer_compile_gg.go rename to ofdgo_renderer_compile.go index d4f8185..1e336f5 100644 --- a/ofdgo_renderer_compile_gg.go +++ b/ofdgo_renderer_compile.go @@ -16,10 +16,17 @@ package ofdgo import "image/color" +// OFDCompiler 使用库自有页面语义,字体和几何操作交给当前后端配置 +type OFDCompiler struct{} + +// Name 返回页面编译器标识 +// 返回: string 编译器标识 +func (OFDCompiler) Name() string { return "ofd" } + // CompilePage 使用公共OFD语义和配置的字形、几何能力编译页面 // 入参: r 渲染器, page 页面 // 返回: *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) if err != nil { return nil, err @@ -46,10 +53,10 @@ func (GGBackend) CompilePage(r *Renderer, page *PageContent) (*RasterPage, error return c.page, nil } -// PageText 提取与GG页面绘制相同定位的原文,不读取图片像素 +// PageText 提取与页面绘制相同定位的原文,不读取图片像素 // 入参: r 渲染器, page 页面 // 返回: *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 { return nil, err } @@ -67,7 +74,7 @@ func (GGBackend) PageText(r *Renderer, page *PageContent) (*PageText, error) { // MeasureObject 复用文字和图形编译规则收集范围,不分配页面像素 // 入参: r 渲染器, object 对象, options 度量上下文 // 返回: 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) if err != nil { return ObjectMeasurement{}, err diff --git a/ofdgo_renderer_font_canvas.go b/ofdgo_renderer_font_canvas.go index 20450dc..86a49a8 100644 --- a/ofdgo_renderer_font_canvas.go +++ b/ofdgo_renderer_font_canvas.go @@ -15,6 +15,8 @@ package ofdgo import ( + "fmt" + "github.com/tdewolff/canvas" "github.com/tdewolff/font" ) @@ -34,12 +36,15 @@ func (CanvasBackend) OpenFont(data []byte) (FontMetrics, error) { if err != nil { return nil, err } - return canvasFontMetrics{sfnt}, nil + metrics := &canvasFontMetrics{SFNT: sfnt} + metrics.fontShaper = &fontShaper{metrics: metrics} + return metrics, nil } // canvasFontMetrics 保留默认字体度量,写出时不更新原始时间 type canvasFontMetrics struct { *font.SFNT + *fontShaper } // Write 返回字体数据,不修改字体元信息 @@ -52,9 +57,36 @@ func (f canvasFontMetrics) Write() []byte { // 入参: glyph 字形编号, size 字号,单位为毫米 // 返回: 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{} if err := f.GlyphPath(path, glyph, 0, 0, 0, size/float64(f.UnitsPerEm()), font.NoHinting); err != nil { return nil, err } 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 +} diff --git a/ofdgo_renderer_font_gg.go b/ofdgo_renderer_font_gg.go deleted file mode 100644 index f234bf2..0000000 --- a/ofdgo_renderer_font_gg.go +++ /dev/null @@ -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 -} diff --git a/ofdgo_renderer_geom_canvas.go b/ofdgo_renderer_geom_canvas.go index 416eec4..974e163 100644 --- a/ofdgo_renderer_geom_canvas.go +++ b/ofdgo_renderer_geom_canvas.go @@ -21,13 +21,10 @@ import ( "github.com/tdewolff/canvas" ) -// canvasNativeStroke 判断描边是否仍由Canvas提供,组合几何不改变矢量描边编码 +// canvasNativeStroke 判断描边是否由Canvas提供 // 入参: geometry 几何后端 // 返回: bool 是否采用原生描边 func canvasNativeStroke(geometry GeometryBackend) bool { - if combined, ok := geometry.(GGGeometryBackend); ok { - return canvasNativeStroke(combined.GeometryBackend) - } _, ok := geometry.(CanvasBackend) return ok } diff --git a/ofdgo_renderer_geom_gg.go b/ofdgo_renderer_geom_gg.go deleted file mode 100644 index c59cd94..0000000 --- a/ofdgo_renderer_geom_gg.go +++ /dev/null @@ -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) -} diff --git a/ofdgo_renderer_gg.go b/ofdgo_renderer_gg.go deleted file mode 100644 index 81c9429..0000000 --- a/ofdgo_renderer_gg.go +++ /dev/null @@ -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} -} diff --git a/ofdgo_renderer_gradient_gg.go b/ofdgo_renderer_gradient_gg.go deleted file mode 100644 index 86c59f4..0000000 --- a/ofdgo_renderer_gradient_gg.go +++ /dev/null @@ -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 -} diff --git a/ofdgo_renderer_image_gg.go b/ofdgo_renderer_image_gg.go deleted file mode 100644 index 478a30d..0000000 --- a/ofdgo_renderer_image_gg.go +++ /dev/null @@ -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 -} diff --git a/ofdgo_renderer_stroke_gg.go b/ofdgo_renderer_stroke_gg.go deleted file mode 100644 index 4f0cfd3..0000000 --- a/ofdgo_renderer_stroke_gg.go +++ /dev/null @@ -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 -}