// 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 }