mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 08:46:55 +08:00
This commit is contained in:
1 parent
46e6d5b622
commit
a1abe55726
15 files changed
+199
-1121
No files matched your search
@@ -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 ? "编辑使用矢量显示" : "矢量显示";
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
+4
-17
@@ -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 创建独立的默认配置
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in new issue
Block a user