feat(后端架构): 优化多后端渲染,修复描边与字形校验
build.yaml / build (push) Successful in 40s

This commit is contained in:
xiaoqidun committed 2026-09-23 20:04:48 +08:00
1 parent f44d0d8a60
commit 580e7043c4
31 files changed
+1488 -204

No files matched your search

+246
View File
@@ -0,0 +1,246 @@
// 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"
"slices"
"strconv"
"strings"
"github.com/go-text/typesetting/segmenter"
)
// layoutShapedText 将显式塑形结果保存为标准文字定位,成功前不修改对象
// 入参: obj 文字对象, value 原文, options 排版选项, metrics 字体度量, width 行宽, lineHeight 行高, hScale 横向比例, baseline 首行基线
// 返回: error 塑形或排版错误
func layoutShapedText(obj *TextObject, value string, options TextLayout, metrics FontMetrics, width, lineHeight, hScale, baseline float64) error {
shaper, ok := metrics.(FontShaper)
if !ok {
return fmt.Errorf("font shaping: %w", ErrBackendUnavailable)
}
var codes []TextCode
var transforms []CGTransform
offset := 0
for _, paragraph := range strings.Split(value, "\n") {
runes := []rune(paragraph)
_, advances, err := shapeTextLine(shaper, runes, obj.Font, obj.Size, hScale, options.LetterSpacing)
if err != nil {
return err
}
lines := breakTextLines(runes, advances, width, options.Wrap, options.LetterSpacing, options.FirstLineIndent)
for i := 0; i < len(lines); i++ {
line := lines[i]
indent := 0.0
if i == 0 {
indent = options.FirstLineIndent
}
available := width - indent
glyphs, lineAdvances, err := shapeTextLine(shaper, runes[line[0]:line[1]], obj.Font, obj.Size, hScale, options.LetterSpacing)
if err != nil {
return err
}
for options.Wrap && shapedLineWidth(runes[line[0]:line[1]], lineAdvances, options.LetterSpacing) > available {
var seg segmenter.Segmenter
seg.Init(runes[line[0]:line[1]])
previous := 0
for it := seg.GraphemeIterator(); it.Next(); {
previous = it.Grapheme().Offset
}
if previous == 0 {
break
}
oldEnd := line[1]
line[1] = line[0] + previous
if i+1 < len(lines) {
lines[i+1][0] = line[1]
} else {
lines = append(lines, [2]int{line[1], oldEnd})
}
lines[i] = line
glyphs, lineAdvances, err = shapeTextLine(shaper, runes[line[0]:line[1]], obj.Font, obj.Size, hScale, options.LetterSpacing)
if err != nil {
return err
}
}
chars := runes[line[0]:line[1]]
x, delta := alignTextLine(chars, lineAdvances, available, options.Align, i+1 < len(lines), options.LetterSpacing)
x += options.LeftIndent + indent
y := baseline + float64(len(codes))*lineHeight
if !finite(x) || !finite(y) {
return fmt.Errorf("text position exceeds finite range")
}
adjusted := parseFloats(delta)
extra := make([]float64, len(chars)+1)
raw := make([]float64, len(chars))
for _, glyph := range glyphs {
raw[glyph.Cluster] += glyph.Advance * hScale
}
for j := range chars {
advance := lineAdvances[j]
if j < len(adjusted) {
advance = adjusted[j]
}
extra[j+1] = extra[j] + advance - raw[j]
}
code := TextCode{X: ofdNumber(x), Y: ofdNumber(y), Value: escapeOFDText(string(chars))}
var dx, dy []string
var lastX, lastY float64
for j, glyph := range glyphs {
gx, gy := x+glyph.X*hScale+extra[glyph.Cluster], y+glyph.Y
if !finite(gx) || !finite(gy) {
return fmt.Errorf("shaped position exceeds finite range")
}
if j == 0 {
code.X, code.Y = ofdNumber(gx), ofdNumber(gy)
} else {
dx, dy = append(dx, ofdNumber(gx-lastX)), append(dy, ofdNumber(gy-lastY))
}
lastX, lastY = gx, gy
}
code.DeltaX, code.DeltaY = strings.Join(dx, " "), strings.Join(dy, " ")
for j := 0; j < len(glyphs); {
end := j + 1
for end < len(glyphs) && glyphs[end].Cluster == glyphs[j].Cluster {
end++
}
next := len(chars)
if end < len(glyphs) {
next = glyphs[end].Cluster
}
ids := make([]string, end-j)
for k := j; k < end; k++ {
ids[k-j] = strconv.Itoa(int(glyphs[k].Glyph))
}
transforms = append(transforms, CGTransform{CodePosition: offset + glyphs[j].Cluster, CodeCount: next - glyphs[j].Cluster, GlyphCount: end - j, Glyphs: strings.Join(ids, " ")})
j = end
}
codes = append(codes, code)
offset += len(chars)
}
}
obj.TextCode, obj.CGTransform = codes, transforms
obj.layout = &textLayout{value: value, options: options}
return nil
}
// shapeTextLine 检查可选后端的簇映射,按原文字符分配塑形步进
// 入参: shaper 字体塑形器, runes 单行字符, font 字体标识, size 字号, scale 横向比例, spacing 字距
// 返回: []ShapedGlyph 字形, []float64 含字距的字符步进, error 塑形错误
func shapeTextLine(shaper FontShaper, runes []rune, font string, size, scale, spacing float64) ([]ShapedGlyph, []float64, error) {
glyphs, err := shaper.ShapeText(string(runes), size)
if err != nil {
return nil, nil, err
}
if len(runes) > 0 && (len(glyphs) == 0 || glyphs[0].Cluster != 0) {
return nil, nil, fmt.Errorf("shaper omitted source text")
}
advances := make([]float64, len(runes))
var missing []rune
for i, glyph := range glyphs {
if glyph.Cluster < 0 || glyph.Cluster >= len(runes) || i > 0 && glyph.Cluster < glyphs[i-1].Cluster || !finite(glyph.X) || !finite(glyph.Y) || !finite(glyph.Advance) {
return nil, nil, fmt.Errorf("invalid shaped glyph")
}
if glyph.Glyph == 0 {
missing = append(missing, runes[glyph.Cluster])
}
advances[glyph.Cluster] += glyph.Advance * scale
}
if err := missingGlyphError(font, missing); err != nil {
return nil, nil, err
}
for i := 0; i < len(glyphs); {
end := i + 1
for end < len(glyphs) && glyphs[end].Cluster == glyphs[i].Cluster {
end++
}
next := len(runes)
if end < len(glyphs) {
next = glyphs[end].Cluster
}
start := glyphs[i].Cluster
advance := advances[start] / float64(next-start)
for j := start; j < next; j++ {
advances[j] = advance
}
i = end
}
spaceTextAdvances(runes, advances, spacing)
if slices.ContainsFunc(advances, func(value float64) bool { return !finite(value) }) {
return nil, nil, fmt.Errorf("shaped advance exceeds finite range")
}
return glyphs, advances, nil
}
// shapedLineWidth 计算不含尾随空格和末尾字距的行宽
// 入参: runes 行内字符, advances 字符步进, spacing 字距
// 返回: float64 行宽
func shapedLineWidth(runes []rune, advances []float64, spacing float64) float64 {
end := len(runes)
for end > 0 && runes[end-1] == ' ' {
end--
}
width := 0.0
for _, advance := range advances[:end] {
width += advance
}
if end > 0 {
width -= spacing
}
return width
}
// validateTextGlyphs 校验显式字形编号及原文区间,不允许跨TextCode或重叠映射
// 入参: obj 文字对象, glyphCount 字体字形数量
// 返回: error 无效映射
func validateTextGlyphs(obj TextObject, glyphCount uint16) error {
if len(obj.CGTransform) == 0 {
return nil
}
var ends []int
total := 0
for _, code := range obj.TextCode {
total += len(textCodeRunes(code.Value))
ends = append(ends, total)
}
previous := 0
for _, transform := range obj.CGTransform {
start, count := transform.CodePosition, transform.CodeCount
if start < previous || count <= 0 || start >= total || count > total-start {
return fmt.Errorf("invalid glyph character range")
}
end := start + count
for _, boundary := range ends {
if start < boundary {
if end > boundary {
return fmt.Errorf("glyph mapping crosses text codes")
}
break
}
}
ids := strings.Fields(transform.Glyphs)
if len(ids) == 0 || len(ids) != transform.GlyphCount {
return fmt.Errorf("invalid glyph count")
}
for _, value := range ids {
id, err := strconv.ParseUint(value, 10, 16)
if err != nil || id >= uint64(glyphCount) {
return fmt.Errorf("invalid glyph index %q", value)
}
}
previous = end
}
return nil
}