feat(归并文件): 归并根包文件

This commit is contained in:
xiaoqidun committed 2026-10-19 10:17:59 +08:00
1 parent ec24b6a17d
commit 1a8de5a3f1
23 files changed
+1724 -1981

No files matched your search

+299
View File
@@ -18,12 +18,20 @@ import (
"fmt"
"math"
"slices"
"strconv"
"strings"
"unicode/utf8"
"github.com/go-text/typesetting/segmenter"
"golang.org/x/text/unicode/bidi"
)
// configuredTextShaper 将会话选项传递给每次换行后的塑形
type configuredTextShaper struct {
FontShaperOptions
options TextShapeOptions
}
// LayoutText 按自有字体度量重排横向文字,保留绘制属性,可显式启用字体塑形
// 不修改文档,通过AddObject或UpdateObject提交;选项仅供当前编辑过程使用,保存为标准文字定位
// 入参: obj 文字对象, value 原文, options 段落排版选项, metrics 字体度量
@@ -141,3 +149,294 @@ func LayoutText(obj *TextObject, value string, options TextLayout, metrics FontM
}
return nil
}
// ShapeText 使用固定选项塑形单行原文
// 入参: value 原文, size 毫米字号
// 返回: []ShapedGlyph 字形, error 塑形错误
func (s configuredTextShaper) ShapeText(value string, size float64) ([]ShapedGlyph, error) {
return s.ShapeTextWithOptions(value, size, s.options)
}
// 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)
}
if options.Shaping != (TextShapeOptions{}) {
configured, ok := metrics.(FontShaperOptions)
if !ok {
return fmt.Errorf("font shaping options: %w", ErrBackendUnavailable)
}
shaper = configuredTextShaper{FontShaperOptions: configured, options: options.Shaping}
}
var codes []TextCode
var transforms []CGTransform
offset := 0
for _, paragraph := range strings.Split(value, "\n") {
runes := []rune(paragraph)
paragraphGlyphs, 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 := paragraphGlyphs, advances
if line[0] != 0 || line[1] != len(runes) {
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)
if options.Shaping.Direction == "rtl" {
for j := len(chars) - 1; j >= 0 && chars[j] == ' '; j-- {
x -= lineAdvances[j]
}
}
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
var visualShifts map[int]float64
if options.Shaping.Bidi {
visualShifts = bidiClusterShifts(glyphs, extra)
}
for j, glyph := range glyphs {
shift := extra[glyph.Cluster]
if options.Shaping.Bidi {
shift = visualShifts[glyph.Cluster]
} else if options.Shaping.Direction == "rtl" {
shift = extra[len(chars)] - extra[glyph.Cluster] - options.LetterSpacing
}
gx, gy := x+glyph.X*hScale+shift, 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
}
// bidiClusterShifts 按视觉簇顺序累计字距和两端对齐产生的偏移
// 入参: glyphs 逻辑顺序字形, extra 逻辑字符累计偏移
// 返回: map[int]float64 各簇的视觉偏移
func bidiClusterShifts(glyphs []ShapedGlyph, extra []float64) map[int]float64 {
type cluster struct {
index, order int
extra float64
}
var clusters []cluster
for i := 0; i < len(glyphs); {
end := i + 1
for end < len(glyphs) && glyphs[end].Cluster == glyphs[i].Cluster {
end++
}
next := len(extra) - 1
if end < len(glyphs) {
next = glyphs[end].Cluster
}
clusters = append(clusters, cluster{glyphs[i].Cluster, glyphs[i].VisualOrder, extra[next] - extra[glyphs[i].Cluster]})
i = end
}
slices.SortFunc(clusters, func(a, b cluster) int { return a.order - b.order })
shifts := make(map[int]float64, len(clusters))
var offset float64
for _, cluster := range clusters {
shifts[cluster.index] = offset
offset += cluster.extra
}
return shifts
}
// 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 || glyph.Glyph >= shaper.NumGlyphs() || !finite(glyph.X) || !finite(glyph.Y) || !finite(glyph.Advance) {
return nil, nil, fmt.Errorf("invalid shaped glyph")
}
advances[glyph.Cluster] += glyph.Advance * scale
}
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
if slices.ContainsFunc(glyphs[i:end], func(glyph ShapedGlyph) bool { return glyph.Glyph == 0 }) {
before := len(missing)
for _, char := range runes[start:next] {
if shaper.GlyphIndex(char) == 0 {
missing = append(missing, char)
}
}
if len(missing) == before {
missing = append(missing, runes[start:next]...)
}
}
advance := advances[start] / float64(next-start)
for j := start; j < next; j++ {
advances[j] = advance
}
i = end
}
if err := missingGlyphError(font, missing); err != nil {
return nil, nil, err
}
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 int) 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, boundary := 0, 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 ends[boundary] <= start {
boundary++
}
if end > ends[boundary] {
return fmt.Errorf("glyph mapping crosses text codes")
}
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
}