feat(渲染引擎): 修复文字渲染并优化图像处理
build.yaml / build (push) Successful in 45s

This commit is contained in:
xiaoqidun committed 2026-10-03 10:51:50 +08:00
1 parent 1945ea355a
commit f2dbf72885
15 files changed
+344 -21

No files matched your search

+1 -1
View File
@@ -10,7 +10,7 @@ require (
github.com/tdewolff/canvas v0.0.0-20260923214215-09804640d00c github.com/tdewolff/canvas v0.0.0-20260923214215-09804640d00c
github.com/tdewolff/font v0.0.0-20260913163313-54f98bb59ee6 github.com/tdewolff/font v0.0.0-20260913163313-54f98bb59ee6
github.com/xiaoqidun/jbig2 v1.1.1 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/crypto v0.57.0
golang.org/x/image v0.46.0 golang.org/x/image v0.46.0
golang.org/x/text v0.42.0 golang.org/x/text v0.42.0
+2 -2
View File
@@ -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/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 h1:SjliS9ZO0e9HX5oYUfBuXrfFsXhWricrSz+XY4zORDI=
github.com/xiaoqidun/jbig2 v1.1.1/go.mod h1:8QonVWtENmh2kc6l+leqeGl7psb79DWlHDkCpdChjfQ= 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-20261003024624-bf2ee2f33c58 h1:n2RFxdeo5fmtvgkRKeMSgJHqmVEGMoS45SppUk4laaE=
github.com/xiaoqidun/pdfgo v0.0.0-20261002164637-41541220b1c3/go.mod h1:4Pf0JO0BEf1uzRY+uZjlMjN/OPn3PYa0YMi6cqPPTLQ= 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 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA= github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA=
+2
View File
@@ -138,6 +138,7 @@ func (e *Editor) UpdateTextContent(page int, id, value string, style TextStyle)
// RewriteText 保留原文的字形定位,不修改对象样式或文档 // RewriteText 保留原文的字形定位,不修改对象样式或文档
// 等长替换保留各字原位;单段增删保留前缀和后缀步进,新增字符沿用局部字距 // 等长替换保留各字原位;单段增删保留前缀和后缀步进,新增字符沿用局部字距
// 方向、缩放和修饰原样保留;复杂布局仅支持等长替换,合并字形需显式重排 // 方向、缩放和修饰原样保留;复杂布局仅支持等长替换,合并字形需显式重排
// 首段缺失坐标仅在临时校验中恢复,不改写源坐标
// 入参: obj 原文字对象, value 新内容 // 入参: obj 原文字对象, value 新内容
// 返回: error 错误信息 // 返回: error 错误信息
func (e *Editor) RewriteText(obj *TextObject, value string) 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 := cloneEditorData(*obj)
check.VScale, check.Decoration = 0, "" check.VScale, check.Decoration = 0, ""
check.TextCode = textCodeOrigins(check.TextCode, 0)
if err := e.prepareText(&check); err != nil { if err := e.prepareText(&check); err != nil {
var missing *MissingGlyphError var missing *MissingGlyphError
if !errors.As(err, &missing) { if !errors.As(err, &missing) {
+1 -1
View File
@@ -436,7 +436,7 @@ func imageNRGBAAt(img image.Image, x, y int) color.NRGBA {
// 返回: color.NRGBA64 NRGBA64像素 // 返回: color.NRGBA64 NRGBA64像素
func imageNRGBA64At(img image.Image, x, y int) color.NRGBA64 { func imageNRGBA64At(img image.Image, x, y int) color.NRGBA64 {
switch src := img.(type) { switch src := img.(type) {
case *image.NRGBA64: case interface{ NRGBA64At(int, int) color.NRGBA64 }:
return src.NRGBA64At(x, y) return src.NRGBA64At(x, y)
case *image.Gray16: case *image.Gray16:
c := src.Gray16At(x, y) c := src.Gray16At(x, y)
+1 -1
View File
@@ -509,7 +509,7 @@ func (c *pdfCompositor) imageColor(mark *pdfgo.ImageMark, point pdfgo.Point, spa
source := c.cache.images[mark.Image] source := c.cache.images[mark.Image]
if source == nil { if source == nil {
var err error var err error
source, err = mark.Image.DecodeComponents() source, err = mark.Image.DecodeComponentsContext(c.importer.ctx)
if err != nil { if err != nil {
return [4]float64{}, 0, err return [4]float64{}, 0, err
} }
+1 -1
View File
@@ -187,7 +187,7 @@ func (p *pdfImporter) directCompositeNode(node pdfCompositeNode, space *pdfgo.Co
source := p.compositingCache().images[node.image.Image] source := p.compositingCache().images[node.image.Image]
if source == nil { if source == nil {
var err error var err error
source, err = node.image.Image.DecodeComponents() source, err = node.image.Image.DecodeComponentsContext(p.ctx)
if err != nil { if err != nil {
return false, err return false, err
} }
+11 -5
View File
@@ -19,7 +19,6 @@ import (
"fmt" "fmt"
"image" "image"
"image/color" "image/color"
"image/png"
"math" "math"
"strconv" "strconv"
"unicode/utf8" "unicode/utf8"
@@ -57,12 +56,19 @@ func (s *pdfStencilImage) Bounds() image.Rectangle { return s.source.Bounds() }
// 入参: x 横向坐标, y 纵向坐标 // 入参: x 横向坐标, y 纵向坐标
// 返回: color.Color 非预乘颜色 // 返回: color.Color 非预乘颜色
func (s *pdfStencilImage) At(x, y int) 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()) { if !image.Pt(x, y).In(s.Bounds()) {
return color.NRGBA64{} return color.NRGBA64{}
} }
gray, _, _, _ := s.source.At(x, y).RGBA() gray := imageNRGBA64At(s.source, x, y)
pixel := s.fill pixel := s.fill
pixel.A = uint16(65535 - gray) pixel.A = uint16(65535 - gray.R)
return pixel return pixel
} }
@@ -112,7 +118,7 @@ func (p *pdfImporter) image(mark pdfgo.ImageMark) error {
} }
var decoded image.Image var decoded image.Image
if len(jbig2Original) == 0 { if len(jbig2Original) == 0 {
decoded, err = source.DecodeImage() decoded, err = source.DecodeImageContext(p.ctx)
if err != nil { if err != nil {
return err return err
} }
@@ -139,7 +145,7 @@ func (p *pdfImporter) image(mark pdfgo.ImageMark) error {
} else if len(jpegOriginal) != 0 { } else if len(jpegOriginal) != 0 {
encoded.Write(jpegOriginal) encoded.Write(jpegOriginal)
} else { } else {
if err := png.Encode(&encoded, decoded); err != nil { if err := pdfgo.EncodePNG(p.ctx, &encoded, decoded); err != nil {
return err return err
} }
} }
+9 -3
View File
@@ -22,6 +22,9 @@ import (
"golang.org/x/image/math/f64" "golang.org/x/image/math/f64"
) )
// rasterScaleBufferLimit 限制整像素缩放器的浮点中间缓冲,超出时使用逐行采样
const rasterScaleBufferLimit = 8 << 20
// PixelTransform 将页面毫米变换转换为像素变换,保留尺寸取整后的原点位置 // PixelTransform 将页面毫米变换转换为像素变换,保留尺寸取整后的原点位置
// 入参: m 局部到页面变换, height 目标像素高度 // 入参: m 局部到页面变换, height 目标像素高度
// 返回: RasterMatrix 局部到像素变换 // 返回: RasterMatrix 局部到像素变换
@@ -40,7 +43,7 @@ func drawRasterImage(dst draw.Image, src image.Image, m RasterMatrix) {
drawRasterImagePixels(dst, src, m, draw.Over) drawRasterImagePixels(dst, src, m, draw.Over)
} }
// drawRasterImagePixels 对整像素边界的轴向缩小使用可分离采样,其余变换保留通用路径 // drawRasterImagePixels 对轴向缩小使用可分离采样,其余变换保留通用路径
// 入参: dst 目标图像, src 源图像, m 像素变换, op 合成操作 // 入参: dst 目标图像, src 源图像, m 像素变换, op 合成操作
func drawRasterImagePixels(dst draw.Image, src image.Image, m RasterMatrix, op draw.Op) { func drawRasterImagePixels(dst draw.Image, src image.Image, m RasterMatrix, op draw.Op) {
b := src.Bounds() 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) draw.CatmullRom.Scale(dst, dr, src, b, op, nil)
return 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) 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 像素变换 // 入参: b 源边界, m 像素变换
// 返回: image.Rectangle 缩放边界, bool 是否适用 // 返回: image.Rectangle 缩放边界, bool 是否适用
func rasterScaleBounds(b image.Rectangle, m RasterMatrix) (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 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.Rectangle{}, false
} }
return image.Rect(int(x0), int(y0), int(x1), int(y1)), true return image.Rect(int(x0), int(y0), int(x1), int(y1)), true
+225
View File
@@ -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)
}
+3 -3
View File
@@ -324,9 +324,6 @@ func (c *semanticCompiler) objectStyle(object PathObject, defaults *DrawParam, l
if object.DashOffset != nil { if object.DashOffset != nil {
style.options.DashOffset = *object.DashOffset style.options.DashOffset = *object.DashOffset
} }
if style.stroke == nil {
style.stroke = &FillColor{}
}
style.fill = withFillAlpha(style.fill, object.Alpha) style.fill = withFillAlpha(style.fill, object.Alpha)
style.stroke = withFillAlpha(style.stroke, 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 { 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 return err
} }
style := c.objectStyle(object, state.Defaults, linear) style := c.objectStyle(object, state.Defaults, linear)
if style.stroke == nil {
style.stroke = withFillAlpha(&FillColor{}, object.Alpha)
}
shading, _ := renderObjectMatrix(object.Boundary, IdentityMatrix, state) shading, _ := renderObjectMatrix(object.Boundary, IdentityMatrix, state)
if object.Fill != nil && *object.Fill { if object.Fill != nil && *object.Fill {
paint, err := c.renderer.ResolvePaint(style.fill) paint, err := c.renderer.ResolvePaint(style.fill)
+8
View File
@@ -128,6 +128,14 @@ func (r *Renderer) PositionText(object TextObject, state RenderState) (*Position
} }
ascent, descent, _ := metrics.VerticalMetrics() ascent, descent, _ := metrics.VerticalMetrics()
unit := size / float64(metrics.UnitsPerEm()) 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) transforms := r.textObjectGlyphTransforms(id, object)
codeOffset, textOffset := 0, 0 codeOffset, textOffset := 0, 0
angle := float64(object.CharDirection) * math.Pi / 180 angle := float64(object.CharDirection) * math.Pi / 180
+11
View File
@@ -16,6 +16,7 @@ package ofdgo
import ( import (
"fmt" "fmt"
"image"
"github.com/tdewolff/canvas" "github.com/tdewolff/canvas"
"github.com/tdewolff/canvas/renderers/pdf" "github.com/tdewolff/canvas/renderers/pdf"
@@ -54,6 +55,16 @@ type pdfOutline struct {
Y float64 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字形圆弧转换 // glyphPath 复用相似变换下的PDF字形圆弧转换
// 入参: path 缓存字形路径, matrix 字形变换 // 入参: path 缓存字形路径, matrix 字形变换
// 返回: *canvas.Path PDF字形路径 // 返回: *canvas.Path PDF字形路径
+14 -2
View File
@@ -115,7 +115,7 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64
} }
strokeStyle.applyLineJoin(obj.Join, obj.MiterLimit) strokeStyle.applyLineJoin(obj.Join, obj.MiterLimit)
if strokeStyle.strokePaint == nil { if strokeStyle.strokePaint == nil {
strokeStyle.strokePaint = colorWithAlpha(canvas.Black, obj.Alpha) strokeStyle.strokePaint = canvas.Transparent
} }
strokeStyle.scale(ctm) strokeStyle.scale(ctm)
if strokeStyle.lineWidth == 0 { if strokeStyle.lineWidth == 0 {
@@ -182,10 +182,22 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64
return return
} }
face := ff.Face(sizePt, fillPaint, fontStyle, canvas.FontNormal) face := ff.Face(sizePt, fillPaint, fontStyle, canvas.FontNormal)
recoverBaseline := len(obj.TextCode) > 0 && obj.TextCode[0].Y == ""
var metrics canvas.FontMetrics var metrics canvas.FontMetrics
if textRun != nil { if textRun != nil || recoverBaseline {
metrics = face.Metrics() 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) glyphTransforms := r.textObjectGlyphTransforms(fontID, obj)
hasUnderline := strings.Contains(obj.Decoration, "Underline") hasUnderline := strings.Contains(obj.Decoration, "Underline")
_, shadedFill := fillPaint.(canvas.Gradient) _, shadedFill := fillPaint.(canvas.Gradient)
+24 -2
View File
@@ -36,6 +36,7 @@ type textGlyphTransform struct {
// Text 获取定位后的文字,横向换行以换行符分隔,字形索引以占位字符表示 // Text 获取定位后的文字,横向换行以换行符分隔,字形索引以占位字符表示
// 返回: string 原文 // 返回: string 原文
func (obj TextObject) Text() string { func (obj TextObject) Text() string {
obj.TextCode = textCodeOrigins(obj.TextCode, 0)
var text strings.Builder var text strings.Builder
for i, code := range obj.TextCode { for i, code := range obj.TextCode {
if obj.textCodeLineBreak(i) { if obj.textCodeLineBreak(i) {
@@ -66,12 +67,33 @@ func (tc *TextCode) GetDeltaY() []float64 {
// 入参: index 文本编码索引 // 入参: index 文本编码索引
// 返回: bool 是否换行 // 返回: bool 是否换行
func (obj TextObject) textCodeLineBreak(index int) 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 return false
} }
code, previous := obj.TextCode[index], obj.TextCode[index-1] 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) 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) y, ey := strconv.ParseFloat(code.Y, 64)
prev, ep := strconv.ParseFloat(previous.Y, 64) prev, ep := strconv.ParseFloat(previous.Y, 64)
if ex != nil || es != nil || ey != nil || ep != nil { if ex != nil || es != nil || ey != nil || ep != nil {
+31
View File
@@ -14,6 +14,8 @@
package ofdgo package ofdgo
import "slices"
// TextPositioner 按OFD坐标及增量定位连续字形,不依赖字体或绘图库 // TextPositioner 按OFD坐标及增量定位连续字形,不依赖字体或绘图库
type TextPositioner struct { type TextPositioner struct {
xs, ys, dxs, dys []float64 xs, ys, dxs, dys []float64
@@ -23,6 +25,35 @@ type TextPositioner struct {
index int 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角度 // NewTextPositioner 创建单个TextCode的定位器,方向采用OFD角度
// 入参: code 文本定位数据, direction 阅读方向 // 入参: code 文本定位数据, direction 阅读方向
// 返回: TextPositioner 字形定位器 // 返回: TextPositioner 字形定位器