mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 16:44:23 +08:00
feat(归并文件): 归并根包文件
This commit is contained in:
1 parent
ec24b6a17d
commit
1a8de5a3f1
23 files changed
+1724
-1981
No files matched your search
@@ -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
|
||||
}
|
||||
Reference in new issue
Block a user