mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 15:06:46 +08:00
1 parent
1945ea355a
commit
f2dbf72885
15 files changed
+344
-21
No files matched your search
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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) {
|
||||
|
||||
+1
-1
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
+11
-5
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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字形路径
|
||||
|
||||
@@ -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)
|
||||
|
||||
+24
-2
@@ -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 {
|
||||
|
||||
@@ -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 字形定位器
|
||||
|
||||
Reference in new issue
Block a user