mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 16:44:23 +08:00
309 lines
8.1 KiB
Go
309 lines
8.1 KiB
Go
// 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 (
|
|
"math"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
"unicode"
|
|
)
|
|
|
|
// TextPositioner 按OFD坐标及增量定位连续字形,不依赖字体或绘图库
|
|
type TextPositioner struct {
|
|
xs, ys, dxs, dys textPositionValues
|
|
current Point
|
|
started bool
|
|
}
|
|
|
|
// textPositionValues 按需读取定位数值及重复位移,不展开重复数组
|
|
type textPositionValues struct {
|
|
data string
|
|
value float64
|
|
repeat int
|
|
compressed bool
|
|
present bool
|
|
}
|
|
|
|
// textLineScanner 顺序判断文本段换行,复用继承坐标和字符到字形的变换
|
|
type textLineScanner struct {
|
|
size float64
|
|
enabled bool
|
|
started bool
|
|
firstX string
|
|
previous TextCode
|
|
offset int
|
|
pending []CGTransform
|
|
transforms map[int][2]int
|
|
}
|
|
|
|
// textCodeOrigins 按标准继承文本段原点,首段缺失坐标时以零横坐标和字体上升高度容错恢复
|
|
// 入参: codes 文本段, ascent 对象坐标系下的字体上升高度
|
|
// 返回: []TextCode 定位文本段,不修改原始数据
|
|
func textCodeOrigins(codes []TextCode, ascent float64) []TextCode {
|
|
for i, code := range codes {
|
|
if code.X != "" && code.Y != "" {
|
|
continue
|
|
}
|
|
result := slices.Clone(codes)
|
|
var x, y string
|
|
if i > 0 {
|
|
x, y = codes[i-1].X, codes[i-1].Y
|
|
} else {
|
|
x, y = "0", ofdNumber(ascent)
|
|
}
|
|
for j := i; j < len(result); j++ {
|
|
if result[j].X == "" {
|
|
result[j].X = x
|
|
}
|
|
if result[j].Y == "" {
|
|
result[j].Y = y
|
|
}
|
|
x, y = result[j].X, result[j].Y
|
|
}
|
|
return result
|
|
}
|
|
return codes
|
|
}
|
|
|
|
// NewTextPositioner 创建单个TextCode的定位器,缺省位移不改变字形原点
|
|
// 入参: code 文本定位数据
|
|
// 返回: TextPositioner 字形定位器
|
|
func NewTextPositioner(code TextCode) TextPositioner {
|
|
return TextPositioner{
|
|
xs: textPositionValues{data: code.X, compressed: strings.Contains(code.X, "g")},
|
|
ys: textPositionValues{data: code.Y, compressed: strings.Contains(code.Y, "g")},
|
|
dxs: textPositionValues{data: code.DeltaX, compressed: strings.Contains(code.DeltaX, "g")},
|
|
dys: textPositionValues{data: code.DeltaY, compressed: strings.Contains(code.DeltaY, "g")},
|
|
}
|
|
}
|
|
|
|
// Next 根据坐标与位移返回下一个字形原点
|
|
// 返回: Point 字形原点
|
|
func (p *TextPositioner) Next() Point {
|
|
if p.started {
|
|
if p.dxs.repeat > 0 || p.dxs.data != "" {
|
|
p.dxs.next()
|
|
}
|
|
if p.dys.repeat > 0 || p.dys.data != "" {
|
|
p.dys.next()
|
|
}
|
|
if p.dxs.present {
|
|
p.current.X += p.dxs.value
|
|
}
|
|
if p.dys.present {
|
|
p.current.Y += p.dys.value
|
|
}
|
|
}
|
|
if p.xs.repeat > 0 || p.xs.data != "" {
|
|
if x, ok := p.xs.next(); ok {
|
|
p.current.X = x
|
|
}
|
|
}
|
|
if p.ys.repeat > 0 || p.ys.data != "" {
|
|
if y, ok := p.ys.next(); ok {
|
|
p.current.Y = y
|
|
}
|
|
}
|
|
p.started = true
|
|
return p.current
|
|
}
|
|
|
|
// next 读取下一个定位数值
|
|
// 返回: float64 数值, bool 是否存在
|
|
func (v *textPositionValues) next() (float64, bool) {
|
|
if v.repeat > 0 {
|
|
v.repeat--
|
|
return v.value, true
|
|
}
|
|
if v.data == "" {
|
|
return v.value, false
|
|
}
|
|
return v.read()
|
|
}
|
|
|
|
// read 解析剩余定位字段并保存重复计数
|
|
// 返回: float64 数值, bool 是否存在
|
|
func (v *textPositionValues) read() (float64, bool) {
|
|
count, pending := 0, false
|
|
for field := v.field(); field != ""; field = v.field() {
|
|
if v.compressed && field == "g" {
|
|
pending = true
|
|
continue
|
|
}
|
|
if pending {
|
|
var err error
|
|
count, err = strconv.Atoi(field)
|
|
if err != nil {
|
|
count = 0
|
|
}
|
|
pending = false
|
|
continue
|
|
}
|
|
value, err := strconv.ParseFloat(field, 64)
|
|
if count > 0 {
|
|
v.value, v.repeat, v.present = value, count-1, true
|
|
return value, true
|
|
}
|
|
if err == nil {
|
|
v.value, v.present = value, true
|
|
return value, true
|
|
}
|
|
}
|
|
return v.value, false
|
|
}
|
|
|
|
// field 读取定位数组中的下一个字段
|
|
// 返回: string 数值或重复标记,无剩余字段时为空
|
|
func (v *textPositionValues) field() string {
|
|
start := -1
|
|
for end, r := range v.data {
|
|
if unicode.IsSpace(r) || !v.compressed && r == ',' {
|
|
if start >= 0 {
|
|
field := v.data[start:end]
|
|
v.data = v.data[end:]
|
|
return field
|
|
}
|
|
} else if start < 0 {
|
|
start = end
|
|
}
|
|
}
|
|
var field string
|
|
if start >= 0 {
|
|
field = v.data[start:]
|
|
}
|
|
v.data = ""
|
|
return field
|
|
}
|
|
|
|
// newTextLineScanner 创建独立的换行扫描器,不修改文字对象
|
|
// 入参: obj 定位文字
|
|
// 返回: textLineScanner 扫描状态
|
|
func newTextLineScanner(obj TextObject) textLineScanner {
|
|
return textLineScanner{size: obj.Size, enabled: len(obj.TextCode) > 1 && obj.ReadDirection == 0 && obj.CharDirection == 0, previous: TextCode{X: "0", Y: "0"}, pending: obj.CGTransform}
|
|
}
|
|
|
|
// glyphCount 首次需要字形范围时解析变换,后续文本段复用结果
|
|
// 入参: code 文本段, offset 对象内字符起点
|
|
// 返回: int 字形数量
|
|
func (s *textLineScanner) glyphCount(code TextCode, offset int) int {
|
|
if len(s.pending) != 0 {
|
|
s.transforms = make(map[int][2]int, len(s.pending))
|
|
}
|
|
for _, transform := range s.pending {
|
|
if transform.CodePosition < 0 {
|
|
continue
|
|
}
|
|
glyphs := len(parseInts(transform.Glyphs))
|
|
if transform.GlyphCount > 0 {
|
|
glyphs = min(glyphs, transform.GlyphCount)
|
|
}
|
|
if glyphs > 0 {
|
|
s.transforms[transform.CodePosition] = [2]int{max(1, transform.CodeCount), glyphs}
|
|
}
|
|
}
|
|
s.pending = nil
|
|
return textCodeGlyphCount(code, offset, s.transforms)
|
|
}
|
|
|
|
// next 继承文本段原点并根据实际字形范围判断是否换行
|
|
// 入参: code 按文档顺序传入的文本段
|
|
// 返回: bool 当前段前是否换行
|
|
func (s *textLineScanner) next(code TextCode) bool {
|
|
if !s.enabled {
|
|
return false
|
|
}
|
|
previous, offset := s.previous, s.offset
|
|
if s.started && (len(s.pending) != 0 || len(s.transforms) != 0) {
|
|
s.offset += textCodeRuneCount(previous.Value)
|
|
}
|
|
if code.X == "" {
|
|
code.X = previous.X
|
|
}
|
|
if code.Y == "" {
|
|
code.Y = previous.Y
|
|
}
|
|
s.previous = code
|
|
if !s.started {
|
|
s.started, s.firstX = true, code.X
|
|
return false
|
|
}
|
|
x, ex := strconv.ParseFloat(code.X, 64)
|
|
start, es := strconv.ParseFloat(s.firstX, 64)
|
|
y, ey := strconv.ParseFloat(code.Y, 64)
|
|
prev, ep := strconv.ParseFloat(previous.Y, 64)
|
|
if ex != nil || es != nil || ey != nil || ep != nil {
|
|
return false
|
|
}
|
|
if x == start && y != prev {
|
|
return true
|
|
}
|
|
left, err := strconv.ParseFloat(previous.X, 64)
|
|
if err != nil || s.size <= 0 || y-prev < s.size/2 || previous.DeltaY != "" || code.DeltaY != "" {
|
|
return false
|
|
}
|
|
count := s.glyphCount(previous, offset)
|
|
if count == 0 {
|
|
return false
|
|
}
|
|
right := left + s.size
|
|
positioner := NewTextPositioner(previous)
|
|
for range count {
|
|
right = math.Max(right, positioner.Next().X+s.size)
|
|
}
|
|
return x <= right
|
|
}
|
|
|
|
// textCodeGlyphCount 按文本段的字符起点统计实际字形数量
|
|
// 入参: code 文本段, offset 对象内字符起点, transforms 已解析的字符到字形变换
|
|
// 返回: int 字形数量
|
|
func textCodeGlyphCount(code TextCode, offset int, transforms map[int][2]int) int {
|
|
if code.Index != "" {
|
|
count := 0
|
|
for _, field := range strings.Fields(code.Index) {
|
|
first, last, interval := strings.Cut(field, "-")
|
|
start, err := strconv.ParseUint(first, 10, 16)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if !interval {
|
|
count++
|
|
continue
|
|
}
|
|
end, err := strconv.ParseUint(last, 10, 16)
|
|
if err == nil && end >= start {
|
|
count += int(end-start) + 1
|
|
}
|
|
}
|
|
return count
|
|
}
|
|
count := textCodeRuneCount(code.Value)
|
|
if len(transforms) == 0 || count == 0 {
|
|
return count
|
|
}
|
|
glyphs := 0
|
|
for i := 0; i < count; {
|
|
if transform, ok := transforms[offset+i]; ok && transform[0] <= count-i {
|
|
i += transform[0]
|
|
glyphs += transform[1]
|
|
} else {
|
|
i++
|
|
glyphs++
|
|
}
|
|
}
|
|
return glyphs
|
|
}
|