From 246d3fd01658544162786a8814db21d26faefa8a Mon Sep 17 00:00:00 2001 From: xiaoqidun Date: Sat, 10 Oct 2026 13:25:56 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E6=96=87=E5=AD=97=E6=80=A7=E8=83=BD):=20?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=96=87=E5=AD=97=E5=AE=9A=E4=BD=8D=E4=B8=8E?= =?UTF-8?q?=E6=8F=90=E5=8F=96=E6=80=A7=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ofdgo_renderer_text_canvas.go | 6 +- ofdgo_text.go | 19 +++-- ofdgo_text_position.go | 127 +++++++++++++++++++++++++++++----- 3 files changed, 126 insertions(+), 26 deletions(-) diff --git a/ofdgo_renderer_text_canvas.go b/ofdgo_renderer_text_canvas.go index 15e4c07..c83e0e4 100644 --- a/ofdgo_renderer_text_canvas.go +++ b/ofdgo_renderer_text_canvas.go @@ -238,8 +238,8 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64 spans = textGlyphSpans(runes, glyphTransforms, codePos) } positioner := NewTextPositioner(tc) - xs, ys := positioner.xs, positioner.ys - drawAsPath := embeddedFont || face.FauxBold > 0 || textCodePositioned(tc, xs, ys) || fillClip != nil || shadedFill || fillPattern != nil || shouldStroke + positioned := textCodePositioned(tc, positioner.xs, positioner.ys) + drawAsPath := embeddedFont || face.FauxBold > 0 || positioned || fillClip != nil || shadedFill || fillPattern != nil || shouldStroke for i, glyph := range glyphs { str := glyph.Text drawAsGlyphPath := drawAsPath || glyph.GlyphID >= 0 @@ -343,7 +343,7 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64 if pdfText { glyphClip = nil } - if !pdfText && fillClip != nil && !embeddedFont && face.FauxBold == 0 && !textCodePositioned(tc, xs, ys) && !shadedFill && fillPattern == nil && glyph.GlyphID < 0 && !hasUnderline { + if !pdfText && fillClip != nil && !embeddedFont && face.FauxBold == 0 && !positioned && !shadedFill && fillPattern == nil && glyph.GlyphID < 0 && !hasUnderline { if rect, ok := rectangularPath(fillClip); ok { transform := canvas.Identity.Translate(canvasX, canvasY) if useTextMatrix { diff --git a/ofdgo_text.go b/ofdgo_text.go index 302c8b6..575c215 100644 --- a/ofdgo_text.go +++ b/ofdgo_text.go @@ -107,7 +107,8 @@ func (obj TextObject) textCodeLineBreak(index int) bool { return false } right := left + obj.Size - for _, dx := range previous.GetDeltaX() { + deltas := textPositionValues{data: previous.DeltaX, compressed: strings.Contains(previous.DeltaX, "g")} + for dx, ok := deltas.next(); ok; dx, ok = deltas.next() { right += dx } return x <= right @@ -266,13 +267,17 @@ func (r *Renderer) textIndexRune(fontID string, glyphID int) rune { } // textCodePositioned 判断文本编码是否带显式定位 -// 入参: textCode 文本编码, xs X坐标列表, ys Y坐标列表 +// 入参: textCode 文本编码, xs 横坐标读取器, ys 纵坐标读取器 // 返回: bool 是否带显式定位 -func textCodePositioned(textCode TextCode, xs, ys []float64) bool { - return strings.TrimSpace(textCode.DeltaX) != "" || - strings.TrimSpace(textCode.DeltaY) != "" || - len(xs) > 1 || - len(ys) > 1 +func textCodePositioned(textCode TextCode, xs, ys textPositionValues) bool { + if strings.TrimSpace(textCode.DeltaX) != "" || strings.TrimSpace(textCode.DeltaY) != "" { + return true + } + xs.next() + ys.next() + _, multipleX := xs.next() + _, multipleY := ys.next() + return multipleX || multipleY } // textDelta 获取文本偏移量 diff --git a/ofdgo_text_position.go b/ofdgo_text_position.go index 99ff150..69db231 100644 --- a/ofdgo_text_position.go +++ b/ofdgo_text_position.go @@ -14,13 +14,27 @@ package ofdgo -import "slices" +import ( + "slices" + "strconv" + "strings" + "unicode" +) // TextPositioner 按OFD坐标及增量定位连续字形,不依赖字体或绘图库 type TextPositioner struct { - xs, ys, dxs, dys []float64 + xs, ys, dxs, dys textPositionValues current Point - index int + started bool +} + +// textPositionValues 按需读取定位数值及重复位移,不展开重复数组 +type textPositionValues struct { + data string + value float64 + repeat int + compressed bool + present bool } // textCodeOrigins 按标准继承文本段原点,首段缺失坐标时以零横坐标和字体上升高度容错恢复 @@ -56,27 +70,108 @@ func textCodeOrigins(codes []TextCode, ascent float64) []TextCode { // 入参: code 文本定位数据 // 返回: TextPositioner 字形定位器 func NewTextPositioner(code TextCode) TextPositioner { - return TextPositioner{xs: parseFloats(code.X), ys: parseFloats(code.Y), dxs: parseFloats(code.DeltaX), dys: parseFloats(code.DeltaY)} + 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 { - i := p.index - if i < len(p.xs) { - p.current.X = p.xs[i] - } else if i > 0 { - if dx, ok := textDelta(p.dxs, i-1); ok { - p.current.X += dx + 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 i < len(p.ys) { - p.current.Y = p.ys[i] - } else if i > 0 { - if dy, ok := textDelta(p.dys, i-1); ok { - p.current.Y += dy + if p.xs.repeat > 0 || p.xs.data != "" { + if x, ok := p.xs.next(); ok { + p.current.X = x } } - p.index++ + 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 +}