From f2dbf72885a0a35c04f76e43a122d87b56fbdd2a Mon Sep 17 00:00:00 2001 From: xiaoqidun Date: Sat, 3 Oct 2026 10:51:50 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E6=B8=B2=E6=9F=93=E5=BC=95=E6=93=8E):=20?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=96=87=E5=AD=97=E6=B8=B2=E6=9F=93=E5=B9=B6?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=9B=BE=E5=83=8F=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go.mod | 2 +- go.sum | 4 +- ofdgo_editor_text.go | 2 + ofdgo_image.go | 2 +- ofdgo_pdf_composite_draw.go | 2 +- ofdgo_pdf_composite_pattern.go | 2 +- ofdgo_pdf_object.go | 16 ++- ofdgo_raster_image.go | 12 +- ofdgo_raster_image_scale.go | 225 +++++++++++++++++++++++++++++++++ ofdgo_render_compile.go | 6 +- ofdgo_render_text.go | 8 ++ ofdgo_renderer_pdf_canvas.go | 11 ++ ofdgo_renderer_text_canvas.go | 16 ++- ofdgo_text.go | 26 +++- ofdgo_text_position.go | 31 +++++ 15 files changed, 344 insertions(+), 21 deletions(-) create mode 100644 ofdgo_raster_image_scale.go diff --git a/go.mod b/go.mod index 0d5bbf7..2a99bd0 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/tdewolff/canvas v0.0.0-20260923214215-09804640d00c github.com/tdewolff/font v0.0.0-20260913163313-54f98bb59ee6 github.com/xiaoqidun/jbig2 v1.1.1 - github.com/xiaoqidun/pdfgo v0.0.0-20261002164637-41541220b1c3 + github.com/xiaoqidun/pdfgo v0.0.0-20261003024624-bf2ee2f33c58 golang.org/x/crypto v0.57.0 golang.org/x/image v0.46.0 golang.org/x/text v0.42.0 diff --git a/go.sum b/go.sum index 1b4d88c..917f762 100644 --- a/go.sum +++ b/go.sum @@ -62,8 +62,8 @@ github.com/tdewolff/test v1.0.12 h1:7F21DqIajswxuche0geHdrUZRCWE4oko4b7bcmkkrxk= github.com/tdewolff/test v1.0.12/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8= github.com/xiaoqidun/jbig2 v1.1.1 h1:SjliS9ZO0e9HX5oYUfBuXrfFsXhWricrSz+XY4zORDI= github.com/xiaoqidun/jbig2 v1.1.1/go.mod h1:8QonVWtENmh2kc6l+leqeGl7psb79DWlHDkCpdChjfQ= -github.com/xiaoqidun/pdfgo v0.0.0-20261002164637-41541220b1c3 h1:mrWCBsxLuBf1sjeEK9r+1KZa5WpER5J3sDPVicajM1E= -github.com/xiaoqidun/pdfgo v0.0.0-20261002164637-41541220b1c3/go.mod h1:4Pf0JO0BEf1uzRY+uZjlMjN/OPn3PYa0YMi6cqPPTLQ= +github.com/xiaoqidun/pdfgo v0.0.0-20261003024624-bf2ee2f33c58 h1:n2RFxdeo5fmtvgkRKeMSgJHqmVEGMoS45SppUk4laaE= +github.com/xiaoqidun/pdfgo v0.0.0-20261003024624-bf2ee2f33c58/go.mod h1:4Pf0JO0BEf1uzRY+uZjlMjN/OPn3PYa0YMi6cqPPTLQ= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA= diff --git a/ofdgo_editor_text.go b/ofdgo_editor_text.go index 9613180..d59ce78 100644 --- a/ofdgo_editor_text.go +++ b/ofdgo_editor_text.go @@ -138,6 +138,7 @@ func (e *Editor) UpdateTextContent(page int, id, value string, style TextStyle) // RewriteText 保留原文的字形定位,不修改对象样式或文档 // 等长替换保留各字原位;单段增删保留前缀和后缀步进,新增字符沿用局部字距 // 方向、缩放和修饰原样保留;复杂布局仅支持等长替换,合并字形需显式重排 +// 首段缺失坐标仅在临时校验中恢复,不改写源坐标 // 入参: obj 原文字对象, value 新内容 // 返回: error 错误信息 func (e *Editor) RewriteText(obj *TextObject, value string) error { @@ -147,6 +148,7 @@ func (e *Editor) RewriteText(obj *TextObject, value string) error { } check := cloneEditorData(*obj) check.VScale, check.Decoration = 0, "" + check.TextCode = textCodeOrigins(check.TextCode, 0) if err := e.prepareText(&check); err != nil { var missing *MissingGlyphError if !errors.As(err, &missing) { diff --git a/ofdgo_image.go b/ofdgo_image.go index b5c24b5..56e4b28 100644 --- a/ofdgo_image.go +++ b/ofdgo_image.go @@ -436,7 +436,7 @@ func imageNRGBAAt(img image.Image, x, y int) color.NRGBA { // 返回: color.NRGBA64 NRGBA64像素 func imageNRGBA64At(img image.Image, x, y int) color.NRGBA64 { switch src := img.(type) { - case *image.NRGBA64: + case interface{ NRGBA64At(int, int) color.NRGBA64 }: return src.NRGBA64At(x, y) case *image.Gray16: c := src.Gray16At(x, y) diff --git a/ofdgo_pdf_composite_draw.go b/ofdgo_pdf_composite_draw.go index 81398a1..2db3fb1 100644 --- a/ofdgo_pdf_composite_draw.go +++ b/ofdgo_pdf_composite_draw.go @@ -509,7 +509,7 @@ func (c *pdfCompositor) imageColor(mark *pdfgo.ImageMark, point pdfgo.Point, spa source := c.cache.images[mark.Image] if source == nil { var err error - source, err = mark.Image.DecodeComponents() + source, err = mark.Image.DecodeComponentsContext(c.importer.ctx) if err != nil { return [4]float64{}, 0, err } diff --git a/ofdgo_pdf_composite_pattern.go b/ofdgo_pdf_composite_pattern.go index 5d6ef63..2a10b17 100644 --- a/ofdgo_pdf_composite_pattern.go +++ b/ofdgo_pdf_composite_pattern.go @@ -187,7 +187,7 @@ func (p *pdfImporter) directCompositeNode(node pdfCompositeNode, space *pdfgo.Co source := p.compositingCache().images[node.image.Image] if source == nil { var err error - source, err = node.image.Image.DecodeComponents() + source, err = node.image.Image.DecodeComponentsContext(p.ctx) if err != nil { return false, err } diff --git a/ofdgo_pdf_object.go b/ofdgo_pdf_object.go index 56dc391..ddd9501 100644 --- a/ofdgo_pdf_object.go +++ b/ofdgo_pdf_object.go @@ -19,7 +19,6 @@ import ( "fmt" "image" "image/color" - "image/png" "math" "strconv" "unicode/utf8" @@ -57,12 +56,19 @@ func (s *pdfStencilImage) Bounds() image.Rectangle { return s.source.Bounds() } // 入参: x 横向坐标, y 纵向坐标 // 返回: color.Color 非预乘颜色 func (s *pdfStencilImage) At(x, y int) color.Color { + return s.NRGBA64At(x, y) +} + +// NRGBA64At 直接读取模板覆盖率,保留填充色的有效精度 +// 入参: x 横向坐标, y 纵向坐标 +// 返回: color.NRGBA64 非预乘颜色 +func (s *pdfStencilImage) NRGBA64At(x, y int) color.NRGBA64 { if !image.Pt(x, y).In(s.Bounds()) { return color.NRGBA64{} } - gray, _, _, _ := s.source.At(x, y).RGBA() + gray := imageNRGBA64At(s.source, x, y) pixel := s.fill - pixel.A = uint16(65535 - gray) + pixel.A = uint16(65535 - gray.R) return pixel } @@ -112,7 +118,7 @@ func (p *pdfImporter) image(mark pdfgo.ImageMark) error { } var decoded image.Image if len(jbig2Original) == 0 { - decoded, err = source.DecodeImage() + decoded, err = source.DecodeImageContext(p.ctx) if err != nil { return err } @@ -139,7 +145,7 @@ func (p *pdfImporter) image(mark pdfgo.ImageMark) error { } else if len(jpegOriginal) != 0 { encoded.Write(jpegOriginal) } else { - if err := png.Encode(&encoded, decoded); err != nil { + if err := pdfgo.EncodePNG(p.ctx, &encoded, decoded); err != nil { return err } } diff --git a/ofdgo_raster_image.go b/ofdgo_raster_image.go index f0b8e01..064037b 100644 --- a/ofdgo_raster_image.go +++ b/ofdgo_raster_image.go @@ -22,6 +22,9 @@ import ( "golang.org/x/image/math/f64" ) +// rasterScaleBufferLimit 限制整像素缩放器的浮点中间缓冲,超出时使用逐行采样 +const rasterScaleBufferLimit = 8 << 20 + // PixelTransform 将页面毫米变换转换为像素变换,保留尺寸取整后的原点位置 // 入参: m 局部到页面变换, height 目标像素高度 // 返回: RasterMatrix 局部到像素变换 @@ -40,7 +43,7 @@ func drawRasterImage(dst draw.Image, src image.Image, m RasterMatrix) { drawRasterImagePixels(dst, src, m, draw.Over) } -// drawRasterImagePixels 对整像素边界的轴向缩小使用可分离采样,其余变换保留通用路径 +// drawRasterImagePixels 对轴向缩小使用可分离采样,其余变换保留通用路径 // 入参: dst 目标图像, src 源图像, m 像素变换, op 合成操作 func drawRasterImagePixels(dst draw.Image, src image.Image, m RasterMatrix, op draw.Op) { b := src.Bounds() @@ -51,10 +54,13 @@ func drawRasterImagePixels(dst draw.Image, src image.Image, m RasterMatrix, op d draw.CatmullRom.Scale(dst, dr, src, b, op, nil) return } + if drawRasterAxisImage(dst, src, m, op) { + return + } draw.CatmullRom.Transform(dst, f64.Aff3{m[0], m[2], m[4], m[1], m[3], m[5]}, src, b, op, nil) } -// rasterScaleBounds 检查变换是否可无几何取整地使用缩放器,并避开其整数溢出范围 +// rasterScaleBounds 检查变换是否可无几何取整地使用缩放器,并限制其中间缓冲 // 入参: b 源边界, m 像素变换 // 返回: image.Rectangle 缩放边界, bool 是否适用 func rasterScaleBounds(b image.Rectangle, m RasterMatrix) (image.Rectangle, bool) { @@ -68,7 +74,7 @@ func rasterScaleBounds(b image.Rectangle, m RasterMatrix) (image.Rectangle, bool return image.Rectangle{}, false } } - if x1 <= x0 || y1 <= y0 || b.Dx() > math.MaxInt32 || b.Dy() > math.MaxInt32 || (x1-x0)*float64(b.Dy()) > float64(min(math.MaxInt32, int(^uint(0)>>1)/32)) { + if x1 <= x0 || y1 <= y0 || b.Dx() > math.MaxInt32 || b.Dy() > math.MaxInt32 || (x1-x0)*float64(b.Dy()) > rasterScaleBufferLimit/32 { return image.Rectangle{}, false } return image.Rect(int(x0), int(y0), int(x1), int(y1)), true diff --git a/ofdgo_raster_image_scale.go b/ofdgo_raster_image_scale.go new file mode 100644 index 0000000..7002be9 --- /dev/null +++ b/ofdgo_raster_image_scale.go @@ -0,0 +1,225 @@ +// 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 ( + "image" + "image/color" + "math" + "slices" + + "golang.org/x/image/draw" +) + +// rasterAxisSample 保存目标像素及其归一化源采样权重 +type rasterAxisSample struct { + pixel int + first int + weights []float64 +} + +// rasterImagePixel 保存未取整或裁切的预乘颜色累积值 +type rasterImagePixel struct { + r, g, b, a float64 +} + +// rasterImageRow 保存正在累积的目标行 +type rasterImageRow struct { + sample rasterAxisSample + values []rasterImagePixel +} + +// rasterImageSource 为通用图片提供预乘十六位采样 +type rasterImageSource struct{ image.Image } + +// RGBA64At 读取通用颜色的原始预乘分量 +// 入参: x 横向坐标, y 纵向坐标 +// 返回: color.RGBA64 预乘颜色 +func (s rasterImageSource) RGBA64At(x, y int) color.RGBA64 { + r, g, b, a := s.At(x, y).RGBA() + return color.RGBA64{R: uint16(r), G: uint16(g), B: uint16(b), A: uint16(a)} +} + +// drawRasterAxisImage 按像素中心进行轴向缩小,逐行复用横向采样并保留镜像与小数坐标 +// 入参: dst 目标图像, src 源图像, m 像素变换, op 合成操作 +// 返回: bool 是否使用轴向采样 +func drawRasterAxisImage(dst draw.Image, src image.Image, m RasterMatrix, op draw.Op) bool { + sx, sy := math.Abs(m[0]), math.Abs(m[3]) + if m[1] != 0 || m[2] != 0 || sx == 0 || sy == 0 || sx > 1 || sy > 1 || sx == 1 && sy == 1 { + return false + } + for _, v := range m { + if math.IsNaN(v) || math.IsInf(v, 0) { + return false + } + } + b := src.Bounds() + if b.Empty() { + return true + } + r := rasterImageBounds(b, m, dst.Bounds()) + if r.Empty() { + return true + } + xs := rasterImageAxis(r.Min.X, r.Max.X, b.Min.X, b.Max.X, m[0], m[4]) + ys := rasterImageAxis(r.Min.Y, r.Max.Y, b.Min.Y, b.Max.Y, m[3], m[5]) + if len(xs) == 0 || len(ys) == 0 { + return true + } + if m[3] < 0 { + slices.Reverse(ys) + } + pixels, ok := src.(image.RGBA64Image) + if !ok { + pixels = rasterImageSource{src} + } + width := len(xs) + horizontal := make([]rasterImagePixel, width) + columns := make([]int, width) + output := image.NewRGBA64(image.Rect(0, 0, width, 1)) + var active []rasterImageRow + var free [][]rasterImagePixel + next := 0 + for y := ys[0].first; next < len(ys) || len(active) != 0; y++ { + if len(active) == 0 && y < ys[next].first { + y = ys[next].first + } + for next < len(ys) && ys[next].first == y { + var row []rasterImagePixel + if n := len(free); n != 0 { + row, free = free[n-1], free[:n-1] + clear(row) + } else { + row = make([]rasterImagePixel, width) + } + active = append(active, rasterImageRow{sample: ys[next], values: row}) + next++ + } + rasterImageHorizontal(horizontal, pixels, xs, y, columns) + n := 0 + for _, row := range active { + weight := row.sample.weights[y-row.sample.first] + if weight != 0 { + for x, value := range horizontal { + p := &row.values[x] + p.r += value.r * weight + p.g += value.g * weight + p.b += value.b * weight + p.a += value.a * weight + } + } + if y+1 == row.sample.first+len(row.sample.weights) { + rasterImageWriteRow(dst, output, row.values, xs[0].pixel, row.sample.pixel, op) + free = append(free, row.values) + } else { + active[n] = row + n++ + } + } + active = active[:n] + } + return true +} + +// rasterImageAxis 按连续源坐标生成CatmullRom权重,不对图像几何边界取整 +// 入参: first 目标起点, end 目标终点, source 源起点, limit 源终点, scale 缩放比例, offset 平移距离 +// 返回: []rasterAxisSample 轴向采样表 +func rasterImageAxis(first, end, source, limit int, scale, offset float64) []rasterAxisSample { + result := make([]rasterAxisSample, 0, end-first) + argument := math.Abs(scale) + support := draw.CatmullRom.Support / argument + for pixel := first; pixel < end; pixel++ { + center := (float64(pixel) + .5 - offset) / scale + if center < float64(source) || center >= float64(limit) { + continue + } + center -= .5 + start := int(max(float64(source), math.Floor(center-support))) + stop := int(min(float64(limit), math.Ceil(center+support))) + weights := make([]float64, stop-start) + total := 0.0 + for i := range weights { + distance := math.Abs((float64(start+i) - center) * argument) + if distance < draw.CatmullRom.Support { + weights[i] = draw.CatmullRom.At(distance) + total += weights[i] + } + } + for i := range weights { + weights[i] /= total + } + result = append(result, rasterAxisSample{pixel: pixel, first: start, weights: weights}) + } + return result +} + +// rasterImageHorizontal 按源列复用像素,保留各目标像素的权重累加顺序与中间精度 +// 入参: row 横向缓冲, src 源像素, samples 采样表, y 源行坐标, columns 活动列缓冲 +func rasterImageHorizontal(row []rasterImagePixel, src image.RGBA64Image, samples []rasterAxisSample, y int, columns []int) { + clear(row) + reversed := samples[0].first > samples[len(samples)-1].first + start, end := samples[0].first, samples[0].first+len(samples[0].weights) + for _, sample := range samples[1:] { + start = min(start, sample.first) + end = max(end, sample.first+len(sample.weights)) + } + next, count := 0, 0 + for x := start; x < end; x++ { + for next < len(samples) { + index := next + if reversed { + index = len(samples) - next - 1 + } + if samples[index].first > x { + break + } + columns[count] = index + count++ + next++ + } + p := src.RGBA64At(x, y) + n := 0 + for _, index := range columns[:count] { + sample := samples[index] + weight := sample.weights[x-sample.first] + if weight != 0 { + value := &row[index] + value.r += float64(p.R) * weight + value.g += float64(p.G) * weight + value.b += float64(p.B) * weight + value.a += float64(p.A) * weight + } + if x+1 < sample.first+len(sample.weights) { + columns[n] = index + n++ + } + } + count = n + } +} + +// rasterImageWriteRow 在两轴采样完成后取整并使用标准预乘合成 +// 入参: dst 目标图像, output 行像素缓冲, values 累积颜色, x 横向起点, y 目标行坐标, op 合成操作 +func rasterImageWriteRow(dst draw.Image, output *image.RGBA64, values []rasterImagePixel, x, y int, op draw.Op) { + for i, value := range values { + output.SetRGBA64(i, 0, color.RGBA64{ + R: uint16(min(max(min(value.r, value.a)+.5, 0), 65535)), + G: uint16(min(max(min(value.g, value.a)+.5, 0), 65535)), + B: uint16(min(max(min(value.b, value.a)+.5, 0), 65535)), + A: uint16(min(max(value.a+.5, 0), 65535)), + }) + } + draw.Draw(dst, image.Rect(x, y, x+len(values), y+1), output, image.Point{}, op) +} diff --git a/ofdgo_render_compile.go b/ofdgo_render_compile.go index ec9851d..2ff4c44 100644 --- a/ofdgo_render_compile.go +++ b/ofdgo_render_compile.go @@ -324,9 +324,6 @@ func (c *semanticCompiler) objectStyle(object PathObject, defaults *DrawParam, l if object.DashOffset != nil { style.options.DashOffset = *object.DashOffset } - if style.stroke == nil { - style.stroke = &FillColor{} - } style.fill = withFillAlpha(style.fill, object.Alpha) style.stroke = withFillAlpha(style.stroke, object.Alpha) if scale := math.Sqrt(math.Abs(linear.a*linear.d - linear.b*linear.c)); scale > 0 { @@ -379,6 +376,9 @@ func (c *semanticCompiler) pathObject(object PathObject, state RenderState) erro return err } style := c.objectStyle(object, state.Defaults, linear) + if style.stroke == nil { + style.stroke = withFillAlpha(&FillColor{}, object.Alpha) + } shading, _ := renderObjectMatrix(object.Boundary, IdentityMatrix, state) if object.Fill != nil && *object.Fill { paint, err := c.renderer.ResolvePaint(style.fill) diff --git a/ofdgo_render_text.go b/ofdgo_render_text.go index f69284f..3c25e84 100644 --- a/ofdgo_render_text.go +++ b/ofdgo_render_text.go @@ -128,6 +128,14 @@ func (r *Renderer) PositionText(object TextObject, state RenderState) (*Position } ascent, descent, _ := metrics.VerticalMetrics() unit := size / float64(metrics.UnitsPerEm()) + recoverBaseline := len(object.TextCode) > 0 && object.TextCode[0].Y == "" + object.TextCode = textCodeOrigins(object.TextCode, float64(ascent)*unit) + if recoverBaseline { + if text := object.Text(); text != result.Run.Text { + result.Run.Text = text + result.Run.Boxes = make([]Box, len([]rune(text))) + } + } transforms := r.textObjectGlyphTransforms(id, object) codeOffset, textOffset := 0, 0 angle := float64(object.CharDirection) * math.Pi / 180 diff --git a/ofdgo_renderer_pdf_canvas.go b/ofdgo_renderer_pdf_canvas.go index 8387f8d..0b6477f 100644 --- a/ofdgo_renderer_pdf_canvas.go +++ b/ofdgo_renderer_pdf_canvas.go @@ -16,6 +16,7 @@ package ofdgo import ( "fmt" + "image" "github.com/tdewolff/canvas" "github.com/tdewolff/canvas/renderers/pdf" @@ -54,6 +55,16 @@ type pdfOutline struct { Y float64 } +// RenderImage 在图像局部保存状态前同步不透明画笔,避免恢复后的透明度与后端缓存不一致 +// 入参: img 图像, matrix 图像变换 +func (r *pdfRenderer) RenderImage(img image.Image, matrix canvas.Matrix) { + style := canvas.DefaultStyle + style.Fill = canvas.Paint{Color: canvas.White} + style.Stroke = canvas.Paint{Color: canvas.Black} + r.PDF.RenderPath(&canvas.Path{}, style, canvas.Identity) + r.PDF.RenderImage(img, matrix) +} + // glyphPath 复用相似变换下的PDF字形圆弧转换 // 入参: path 缓存字形路径, matrix 字形变换 // 返回: *canvas.Path PDF字形路径 diff --git a/ofdgo_renderer_text_canvas.go b/ofdgo_renderer_text_canvas.go index c66b26d..ba2b3b1 100644 --- a/ofdgo_renderer_text_canvas.go +++ b/ofdgo_renderer_text_canvas.go @@ -115,7 +115,7 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64 } strokeStyle.applyLineJoin(obj.Join, obj.MiterLimit) if strokeStyle.strokePaint == nil { - strokeStyle.strokePaint = colorWithAlpha(canvas.Black, obj.Alpha) + strokeStyle.strokePaint = canvas.Transparent } strokeStyle.scale(ctm) if strokeStyle.lineWidth == 0 { @@ -182,10 +182,22 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64 return } face := ff.Face(sizePt, fillPaint, fontStyle, canvas.FontNormal) + recoverBaseline := len(obj.TextCode) > 0 && obj.TextCode[0].Y == "" var metrics canvas.FontMetrics - if textRun != nil { + if textRun != nil || recoverBaseline { metrics = face.Metrics() } + ascent := metrics.Ascent + if scale := ctm.YScale(); scale > 0 && !useTextMatrix { + ascent /= scale + } + obj.TextCode = textCodeOrigins(obj.TextCode, ascent) + if textRun != nil && recoverBaseline { + if text := obj.Text(); text != textRun.Text { + textRun.Text = text + textRun.Boxes = make([]Box, len([]rune(text))) + } + } glyphTransforms := r.textObjectGlyphTransforms(fontID, obj) hasUnderline := strings.Contains(obj.Decoration, "Underline") _, shadedFill := fillPaint.(canvas.Gradient) diff --git a/ofdgo_text.go b/ofdgo_text.go index ac2140f..af82090 100644 --- a/ofdgo_text.go +++ b/ofdgo_text.go @@ -36,6 +36,7 @@ type textGlyphTransform struct { // Text 获取定位后的文字,横向换行以换行符分隔,字形索引以占位字符表示 // 返回: string 原文 func (obj TextObject) Text() string { + obj.TextCode = textCodeOrigins(obj.TextCode, 0) var text strings.Builder for i, code := range obj.TextCode { if obj.textCodeLineBreak(i) { @@ -66,12 +67,33 @@ func (tc *TextCode) GetDeltaY() []float64 { // 入参: index 文本编码索引 // 返回: bool 是否换行 func (obj TextObject) textCodeLineBreak(index int) bool { - if index == 0 || obj.ReadDirection != 0 || obj.CharDirection != 0 { + if index == 0 || obj.ReadDirection != 0 || obj.CharDirection != 0 || obj.TextCode[index].Y == "" { return false } code, previous := obj.TextCode[index], obj.TextCode[index-1] + for i := index - 2; i >= 0 && (previous.X == "" || previous.Y == ""); i-- { + if previous.X == "" { + previous.X = obj.TextCode[i].X + } + if previous.Y == "" { + previous.Y = obj.TextCode[i].Y + } + } + if previous.X == "" { + previous.X = "0" + } + if previous.Y == "" { + previous.Y = "0" + } + if code.X == "" { + code.X = previous.X + } + startX := obj.TextCode[0].X + if startX == "" { + startX = "0" + } x, ex := strconv.ParseFloat(code.X, 64) - start, es := strconv.ParseFloat(obj.TextCode[0].X, 64) + start, es := strconv.ParseFloat(startX, 64) y, ey := strconv.ParseFloat(code.Y, 64) prev, ep := strconv.ParseFloat(previous.Y, 64) if ex != nil || es != nil || ey != nil || ep != nil { diff --git a/ofdgo_text_position.go b/ofdgo_text_position.go index 821d013..2e6dc41 100644 --- a/ofdgo_text_position.go +++ b/ofdgo_text_position.go @@ -14,6 +14,8 @@ package ofdgo +import "slices" + // TextPositioner 按OFD坐标及增量定位连续字形,不依赖字体或绘图库 type TextPositioner struct { xs, ys, dxs, dys []float64 @@ -23,6 +25,35 @@ type TextPositioner struct { index 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的定位器,方向采用OFD角度 // 入参: code 文本定位数据, direction 阅读方向 // 返回: TextPositioner 字形定位器