From bb3abdea1d20fc4e68ce5d0a4bbe9a3535c4763b Mon Sep 17 00:00:00 2001 From: xiaoqidun Date: Fri, 16 Oct 2026 08:39:54 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E5=AD=97=E4=BD=93=E5=9B=BE=E5=83=8F):=20?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=AD=97=E4=BD=93=E5=9B=BE=E5=83=8F=E4=B8=8E?= =?UTF-8?q?=E5=AF=BC=E5=87=BA=E7=A8=B3=E5=AE=9A=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go.mod | 6 ++-- go.sum | 12 +++---- internal/base/font/cff.go | 52 ++++++++++++++++----------- internal/base/geom/arc.go | 15 ++++++-- internal/base/geom/matrix.go | 7 ++-- internal/base/geom/transform.go | 5 +-- internal/base/image/image.go | 41 ++++++++++++++++++--- internal/base/image/tiff_mask.go | 44 +++++++++++++++++------ ofdgo_compression.go | 7 +--- ofdgo_compression_parallel.go | 5 ++- ofdgo_convert.go | 31 ++++++++++++++-- ofdgo_pdf.go | 36 ++++++++----------- ofdgo_pdf_composite_draw.go | 32 +++++++++++++---- ofdgo_pdf_composite_scratch.go | 12 +++++++ ofdgo_pdf_shading.go | 3 +- ofdgo_raster.go | 9 +++-- ofdgo_raster_clip.go | 9 +++-- ofdgo_render_output.go | 16 +++++++-- ofdgo_render_svg.go | 4 ++- ofdgo_renderer_canvas.go | 59 ++++++++++++++++++++++--------- ofdgo_renderer_eps_compression.go | 25 +++++++++++-- ofdgo_renderer_image_canvas.go | 14 +++++++- 22 files changed, 322 insertions(+), 122 deletions(-) diff --git a/go.mod b/go.mod index 6f5bca3..05fc51d 100644 --- a/go.mod +++ b/go.mod @@ -8,9 +8,9 @@ require ( github.com/go-text/typesetting v0.3.5 github.com/tdewolff/canvas v0.0.0-20260923214215-09804640d00c github.com/tdewolff/font v0.0.0-20261010204837-88a7f6b56918 - github.com/xiaoqidun/j2kgo v1.0.1 - github.com/xiaoqidun/jbig2 v1.1.7 - github.com/xiaoqidun/pdfgo v0.0.0-20261015160218-fa5114475eb5 + github.com/xiaoqidun/j2kgo v1.0.2 + github.com/xiaoqidun/jbig2 v1.1.8 + github.com/xiaoqidun/pdfgo v0.0.0-20261016003351-18348c283ac1 golang.org/x/crypto v0.58.0 golang.org/x/image v0.47.0 golang.org/x/text v0.43.0 diff --git a/go.sum b/go.sum index 4ba2c51..2cd3bfb 100644 --- a/go.sum +++ b/go.sum @@ -58,12 +58,12 @@ github.com/tdewolff/parse/v2 v2.8.16 h1:bLk5svUOQRkW/Y2SJ+DeENSIkZBcTIkq+Atyv5D8 github.com/tdewolff/parse/v2 v2.8.16/go.mod h1:XdsoSFThlVIRIajAuqz1evNY7bagZS8LBOPA3aVopwQ= github.com/tdewolff/test v1.0.12 h1:7F21DqIajswxuche0geHdrUZRCWE4oko4b7bcmkkrxk= github.com/tdewolff/test v1.0.12/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8= -github.com/xiaoqidun/j2kgo v1.0.1 h1:W0SytzjvgiOhXwq+JNRSVKLB847fjGOvGWTNKqmwvo0= -github.com/xiaoqidun/j2kgo v1.0.1/go.mod h1:DCwAfuDzCDYqUiAlZ7BDwigTjaXw7o9L1FtEma/D1aA= -github.com/xiaoqidun/jbig2 v1.1.7 h1:118d2lQn2ZsupmXazK+2p23Jp5xQ09btBmPUoYoa/+Y= -github.com/xiaoqidun/jbig2 v1.1.7/go.mod h1:9jromqVwMqlptUDEt6Yip8vqYA5I1YjhG7FgMl/Z2dA= -github.com/xiaoqidun/pdfgo v0.0.0-20261015160218-fa5114475eb5 h1:eJuai2A2q9R8Mo/oKXiIpNc41uzolkKplPWXUmYiHcU= -github.com/xiaoqidun/pdfgo v0.0.0-20261015160218-fa5114475eb5/go.mod h1:FUQ8+TfNoec6tZCdsckx+rrD25JACm7EzuZyFH37QHg= +github.com/xiaoqidun/j2kgo v1.0.2 h1:OAjjEOfaBaQzBNcj9wx+eM68UI5pewR6SzNZa1R1DIw= +github.com/xiaoqidun/j2kgo v1.0.2/go.mod h1:DCwAfuDzCDYqUiAlZ7BDwigTjaXw7o9L1FtEma/D1aA= +github.com/xiaoqidun/jbig2 v1.1.8 h1:dERPW7F8gs18a/DOD9gXHR3fP1Vl6KhCNsLnq5l3BIo= +github.com/xiaoqidun/jbig2 v1.1.8/go.mod h1:9jromqVwMqlptUDEt6Yip8vqYA5I1YjhG7FgMl/Z2dA= +github.com/xiaoqidun/pdfgo v0.0.0-20261016003351-18348c283ac1 h1:PNucIHG+qid5/GCN3wUGEqfsyrykhoazBbtPaGysWHk= +github.com/xiaoqidun/pdfgo v0.0.0-20261016003351-18348c283ac1/go.mod h1:AaabzsObZ8Z76AcaWbo6swRROrJsblyIGB7HbieeJA4= 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/internal/base/font/cff.go b/internal/base/font/cff.go index 05d7a57..f95c129 100644 --- a/internal/base/font/cff.go +++ b/internal/base/font/cff.go @@ -1036,6 +1036,9 @@ func getCFFCharsetInfo(data []byte, numGlyphs int) ([]int, string, string, int, if topDictData == nil { return nil, "", "", 0, false } + if _, _, _, err := scanCFFIndex(data, stringIndexOff); err != nil { + return nil, "", "", 0, false + } td := parseCFFDict(topDictData) registry, ordering := getCFFROS(data, stringIndexOff, td) actual, err := parseCFFAndCountGlyphs(data) @@ -1067,7 +1070,18 @@ func getCmapFromCFF(data []byte, numGlyphs int) map[rune]uint16 { if !ok { return nil } - mapping := make(map[rune]uint16) + return cffCharsetMapping(data, sids, registry, ordering, stringIndexOff) +} + +// cffCharsetMapping 根据已解析的CFF字符集恢复Unicode映射 +// 入参: data CFF数据, sids 字符标识, registry 注册机构, ordering 字符集, stringIndexOff 字符串索引偏移 +// 返回: map[rune]uint16 恢复的映射表 +func cffCharsetMapping(data []byte, sids []int, registry, ordering string, stringIndexOff int) map[rune]uint16 { + capacity := max(0, len(sids)-1) + if registry == "" || registry == "Adobe" && ordering == "GB1" { + capacity *= 2 + } + mapping := make(map[rune]uint16, capacity) if registry == "Adobe" && ordering == "GB1" { for gid, cid := range sids { if gid == 0 { @@ -1100,7 +1114,7 @@ func getCmapFromCFF(data []byte, numGlyphs int) map[rune]uint16 { } } else { idx := sid - 391 - name = readStringIndexItem(data, stringIndexOff, idx) + name = string(readStringIndexItem(data, stringIndexOff, idx)) } r := rune(0) if name != "" { @@ -1127,11 +1141,11 @@ func GetCFFCIDRuneMap(data []byte) map[uint16]rune { if err != nil { return nil } - sids, registry, _, _, ok := getCFFCharsetInfo(cffData, numGlyphs) + sids, registry, ordering, stringIndexOff, ok := getCFFCharsetInfo(cffData, numGlyphs) if !ok || registry == "" { return nil } - mapping := getCmapFromCFF(cffData, numGlyphs) + mapping := cffCharsetMapping(cffData, sids, registry, ordering, stringIndexOff) if len(mapping) == 0 { return nil } @@ -1187,7 +1201,7 @@ func getCFFData(data []byte) []byte { } offset := int(binary.BigEndian.Uint32(data[pos+8 : pos+12])) length := int(binary.BigEndian.Uint32(data[pos+12 : pos+16])) - if offset < 0 || length < 0 || offset+length > len(data) { + if offset < 0 || length < 0 || offset > len(data) || length > len(data)-offset { return nil } return data[offset : offset+length] @@ -1269,31 +1283,29 @@ func parseCFFCharset(data []byte, offset, count int) ([]int, error) { // readStringIndexItem 读取CFF字符串索引项 // 入参: data CFF数据, offset 索引偏移, idx 索引号 -// 返回: string 读取的字符串 -func readStringIndexItem(data []byte, offset int, idx int) string { - if offset >= len(data) { - return "" +// 返回: []byte 共享原始数据的只读字符串,索引无效时为nil +func readStringIndexItem(data []byte, offset int, idx int) []byte { + if offset < 0 || offset > len(data)-2 || idx < 0 { + return nil } count := int(binary.BigEndian.Uint16(data[offset:])) + if idx >= count || offset > len(data)-3 { + return nil + } offSize := int(data[offset+2]) - if idx >= count { - return "" + if offSize < 1 || offSize > 4 || count+1 > (len(data)-offset-3)/offSize { + return nil } offArrayStart := offset + 3 p1 := offArrayStart + idx*offSize p2 := p1 + offSize - if p2+offSize > len(data) { - return "" - } loc1 := readCFFOffset(data, p1, offSize) loc2 := readCFFOffset(data, p2, offSize) dataStart := offArrayStart + (count+1)*offSize - start := dataStart + loc1 - 1 - length := loc2 - loc1 - if start < 0 || start+length > len(data) { - return "" + if loc1 < 1 || loc2 < loc1 || loc2-1 > len(data)-dataStart { + return nil } - return string(data[start : start+length]) + return data[dataStart+loc1-1 : dataStart+loc2-1] } // getCFFROS 读取CID字体ROS信息 @@ -1317,7 +1329,7 @@ func getCFFSIDString(data []byte, stringIndexOff int, sid int) string { return cffStandardStrings[sid] } if sid > 390 { - return readStringIndexItem(data, stringIndexOff, sid-391) + return string(readStringIndexItem(data, stringIndexOff, sid-391)) } return "" } diff --git a/internal/base/geom/arc.go b/internal/base/geom/arc.go index d668f46..685c27c 100644 --- a/internal/base/geom/arc.go +++ b/internal/base/geom/arc.go @@ -116,7 +116,7 @@ func centerArc(start Point, arc Segment) (geometryArc, error) { } angle := math.Mod(arc.Rotation, 360) * math.Pi / 180 sine, cosine := math.Sincos(angle) - dx, dy := (start.X-arc.End.X)/2, (start.Y-arc.End.Y)/2 + dx, dy := arcCoordinateMean(start.X, -arc.End.X), arcCoordinateMean(start.Y, -arc.End.Y) x, y := cosine*dx+sine*dy, -sine*dx+cosine*dy scale := math.Hypot(x/rx, y/ry) if scale > 1 { @@ -139,13 +139,24 @@ func centerArc(start Point, arc Segment) (geometryArc, error) { if !arc.Sweep && delta > 0 { delta -= 2 * math.Pi } - center := Point{cosine*cx - sine*cy + (start.X+arc.End.X)/2, sine*cx + cosine*cy + (start.Y+arc.End.Y)/2} + center := Point{cosine*cx - sine*cy + arcCoordinateMean(start.X, arc.End.X), sine*cx + cosine*cy + arcCoordinateMean(start.Y, arc.End.Y)} if !finite(center.X) || !finite(center.Y) || !finite(rx) || !finite(ry) || !finite(delta) || !finite(theta) || delta == 0 { return geometryArc{}, fmt.Errorf("invalid arc extent") } return geometryArc{center, Point{rx * cosine, rx * sine}, Point{-ry * sine, ry * cosine}, theta, delta}, nil } +// arcCoordinateMean 计算有限坐标均值,仅在求和溢出时先减半,避免微小坐标提前下溢 +// 入参: a 第一个坐标, b 第二个坐标 +// 返回: float64 坐标均值 +func arcCoordinateMean(a, b float64) float64 { + value := (a + b) / 2 + if math.IsInf(value, 0) { + return a/2 + b/2 + } + return value +} + // point 计算椭圆参数对应的坐标 // 入参: angle 参数角 // 返回: Point 椭圆上的点 diff --git a/internal/base/geom/matrix.go b/internal/base/geom/matrix.go index 77a7d62..ca88ba1 100644 --- a/internal/base/geom/matrix.go +++ b/internal/base/geom/matrix.go @@ -125,7 +125,7 @@ func (m Matrix) Invert() (Matrix, bool) { return n, true } -// matrixProductDifference 计算两个乘积之差,检查溢出、下溢及相消造成的精度损失 +// matrixProductDifference 补偿乘积相减的舍入残差,检查溢出、下溢及严重相消 // 入参: a 第一个乘数, b 第二个乘数, c 第三个乘数, d 第四个乘数 // 返回: float64 乘积之差, bool 是否可直接用于求逆 func matrixProductDifference(a, b, c, d float64) (float64, bool) { @@ -134,6 +134,9 @@ func matrixProductDifference(a, b, c, d float64) (float64, bool) { if !finite(value) || x == 0 && a != 0 && b != 0 || y == 0 && c != 0 && d != 0 { return value, false } + if x != 0 && y != 0 { + value = math.FMA(a, b, -y) + math.FMA(-c, d, y) + } return value, x == 0 && y == 0 || math.Abs(value) >= 0x1p-1022 && math.Abs(value) >= 0x1p-48*math.Max(math.Abs(x), math.Abs(y)) } @@ -169,7 +172,7 @@ func TranslationMatrix(x, y float64) Matrix { // YScale 获取Y轴缩放比例 // 返回: float64 缩放比例 func (m Matrix) YScale() float64 { - return math.Sqrt(m.c*m.c + m.d*m.d) + return math.Hypot(m.c, m.d) } // Equal 比较毫米坐标,使用固定绝对误差,不受绘图库全局精度影响 diff --git a/internal/base/geom/transform.go b/internal/base/geom/transform.go index 9b4899c..8ab2ced 100644 --- a/internal/base/geom/transform.go +++ b/internal/base/geom/transform.go @@ -33,10 +33,7 @@ func (p Path) Transform(matrix Matrix) (Path, error) { } } matrixScale := math.Max(math.Max(math.Abs(matrix.a), math.Abs(matrix.b)), math.Max(math.Abs(matrix.c), math.Abs(matrix.d))) - determinant := 0.0 - if matrixScale != 0 { - determinant = (matrix.a/matrixScale)*(matrix.d/matrixScale) - (matrix.b/matrixScale)*(matrix.c/matrixScale) - } + determinant, _ := matrixProductSum(matrix.a, matrix.d, -matrix.b, matrix.c) transform := func(p Point) Point { x, y := matrix.Transform(p.X, p.Y) return Point{x, y} diff --git a/internal/base/image/image.go b/internal/base/image/image.go index a6de09f..3dc2a42 100644 --- a/internal/base/image/image.go +++ b/internal/base/image/image.go @@ -451,13 +451,11 @@ func GrayAt(img image.Image, x, y int) color.Gray { case *colorImage: return GrayAt(src.source, x, y) case *maskedTIFFImage: - if src.NRGBA64At(x, y).A == 0 { + if src.alphaAt(x, y) == 0 { return color.Gray{} } - bounds, size := src.source.Bounds(), src.bounds.Size() - sx := bounds.Min.X + int((uint64(x-src.bounds.Min.X)*uint64(bounds.Dx())+uint64(bounds.Dx()/2))/uint64(size.X)) - sy := bounds.Min.Y + int((uint64(y-src.bounds.Min.Y)*uint64(bounds.Dy())+uint64(bounds.Dy()/2))/uint64(size.Y)) - return GrayAt(src.source, sx, sy) + point := src.samplePoint(src.source.Bounds(), x, y) + return GrayAt(src.source, point.X, point.Y) case *transparentEdgeImage: if x <= src.rect.Min.X || y <= src.rect.Min.Y || x >= src.rect.Max.X-1 || y >= src.rect.Max.Y-1 { return color.Gray{} @@ -534,6 +532,39 @@ func NRGBAAt(img image.Image, x, y int) color.NRGBA { // 返回: uint32 十六位透明度 func AlphaAt(img image.Image, x, y int) uint32 { switch source := img.(type) { + case *maskedTIFFImage: + return source.alphaAt(x, y) + case *image.YCbCr, *image.CMYK, *image.Gray, *image.Gray16: + return 65535 + case *image.NYCbCrA: + if !image.Pt(x, y).In(source.Rect) { + return 0 + } + return uint32(source.A[source.AOffset(x, y)]) * 257 + case *orientedTIFFImage: + point, ok := source.sourcePoint(x, y) + if !ok { + return 0 + } + return AlphaAt(source.source, point.X, point.Y) + case *planarTIFFImage: + if !image.Pt(x, y).In(source.bounds) { + return 0 + } + if source.alpha != 0 { + index := 3 + if source.cmyk { + index = 4 + } else if source.gray || len(source.palette) != 0 { + index = 1 + } + return uint32(source.sampleAt(index, x, y)) + } + if len(source.palette) != 0 { + index := uint32(source.sampleAt(0, x, y)) * uint32(len(source.palette)-1) / 65535 + return uint32(source.palette[index].A) + } + return 65535 case *image.RGBA: if !(image.Point{X: x, Y: y}).In(source.Rect) { return 0 diff --git a/internal/base/image/tiff_mask.go b/internal/base/image/tiff_mask.go index 8573912..008e460 100644 --- a/internal/base/image/tiff_mask.go +++ b/internal/base/image/tiff_mask.go @@ -49,6 +49,36 @@ func (s *maskedTIFFImage) Bounds() image.Rectangle { return s.bounds } // 返回: color.Model 颜色模型 func (s *maskedTIFFImage) ColorModel() color.Model { return color.NRGBA64Model } +// samplePoint 将显示坐标映射到主图或蒙版的像素中心 +// 入参: bounds 采样图像范围, x 横坐标, y 纵坐标 +// 返回: image.Point 采样坐标 +func (s *maskedTIFFImage) samplePoint(bounds image.Rectangle, x, y int) image.Point { + size := s.bounds.Size() + x, y = x-s.bounds.Min.X, y-s.bounds.Min.Y + if bounds.Size() == size { + return bounds.Min.Add(image.Pt(x, y)) + } + return image.Pt( + bounds.Min.X+int((uint64(x)*uint64(bounds.Dx())+uint64(bounds.Dx()/2))/uint64(size.X)), + bounds.Min.Y+int((uint64(y)*uint64(bounds.Dy())+uint64(bounds.Dy()/2))/uint64(size.Y)), + ) +} + +// alphaAt 读取蒙版与主图透明度,不解算主图颜色 +// 入参: x 横坐标, y 纵坐标 +// 返回: uint32 十六位透明度 +func (s *maskedTIFFImage) alphaAt(x, y int) uint32 { + if !image.Pt(x, y).In(s.bounds) { + return 0 + } + mask := s.samplePoint(s.mask.Bounds(), x, y) + if NRGBA64At(s.mask, mask.X, mask.Y).R == 0 { + return 0 + } + source := s.samplePoint(s.source.Bounds(), x, y) + return AlphaAt(s.source, source.X, source.Y) +} + // NRGBA64At 按像素中心读取主图及蒙版,零位隐藏原图并保留隐藏颜色 // 入参: x 横坐标, y 纵坐标 // 返回: color.NRGBA64 非预乘十六位颜色 @@ -56,16 +86,10 @@ func (s *maskedTIFFImage) NRGBA64At(x, y int) color.NRGBA64 { if !image.Pt(x, y).In(s.bounds) { return color.NRGBA64{} } - size := s.bounds.Size() - x, y = x-s.bounds.Min.X, y-s.bounds.Min.Y - b := s.source.Bounds() - sx := b.Min.X + int((uint64(x)*uint64(b.Dx())+uint64(b.Dx()/2))/uint64(size.X)) - sy := b.Min.Y + int((uint64(y)*uint64(b.Dy())+uint64(b.Dy()/2))/uint64(size.Y)) - pixel := NRGBA64At(s.source, sx, sy) - b = s.mask.Bounds() - mx := b.Min.X + int((uint64(x)*uint64(b.Dx())+uint64(b.Dx()/2))/uint64(size.X)) - my := b.Min.Y + int((uint64(y)*uint64(b.Dy())+uint64(b.Dy()/2))/uint64(size.Y)) - if NRGBA64At(s.mask, mx, my).R == 0 { + source := s.samplePoint(s.source.Bounds(), x, y) + pixel := NRGBA64At(s.source, source.X, source.Y) + mask := s.samplePoint(s.mask.Bounds(), x, y) + if NRGBA64At(s.mask, mask.X, mask.Y).R == 0 { pixel.A = 0 } return pixel diff --git a/ofdgo_compression.go b/ofdgo_compression.go index 1a5f1e2..c9699a8 100644 --- a/ofdgo_compression.go +++ b/ofdgo_compression.go @@ -130,12 +130,7 @@ func (e *Editor) outputSnapshot(ctx context.Context, options WriteOptions) (*Edi return nil, err } snapshot := *e - snapshot.OnWriteProgress = func(stage string, completed, total int) error { - if err := ctx.Err(); err != nil { - return err - } - return editorProgress(e.OnWriteProgress).report(stage, completed, total) - } + snapshot.OnWriteProgress = editorProgress(e.OnWriteProgress).withContext(ctx) snapshot.output = &outputOptimization{ctx: ctx, options: options.Compression} if options.Compression.Mode == CompressionUnchanged { return &snapshot, nil diff --git a/ofdgo_compression_parallel.go b/ofdgo_compression_parallel.go index facd617..4a1fef2 100644 --- a/ofdgo_compression_parallel.go +++ b/ofdgo_compression_parallel.go @@ -84,13 +84,16 @@ func (e *Editor) prepareOutputImages(keys []string, load func(string) ([]byte, e if err := e.output.ctx.Err(); err != nil { return err } - if err := editorProgress(e.OnWriteProgress).report("compress", e.output.completed, 0); err != nil { + if err := editorProgress(e.OnWriteProgress).withContext(e.output.ctx).report("compress", e.output.completed, 0); err != nil { return err } data, err := load(key) if err != nil { return err } + if err := e.output.ctx.Err(); err != nil { + return err + } if len(data) == 0 { continue } diff --git a/ofdgo_convert.go b/ofdgo_convert.go index b6baaaf..e221a7e 100644 --- a/ofdgo_convert.go +++ b/ofdgo_convert.go @@ -353,17 +353,21 @@ func Convert(ctx context.Context, source io.ReaderAt, size int64, output io.Writ return report, err } -// ReadAt 读取指定位置的数据并响应取消 +// ReadAt 读取前后响应取消,保留读取字节数和源错误 // 入参: data 目标缓冲, offset 源偏移 // 返回: int 读取字节数, error 读取错误 func (r convertReader) ReadAt(data []byte, offset int64) (int, error) { if err := r.context.Err(); err != nil { return 0, err } - return r.source.ReadAt(data, offset) + n, err := r.source.ReadAt(data, offset) + if err == nil { + err = r.context.Err() + } + return n, err } -// Write 写出数据并响应取消及短写 +// Write 写入前后响应取消,保留写入及短写错误 // 入参: data 待写数据 // 返回: int 写入字节数, error 写入错误 func (w convertWriter) Write(data []byte) (int, error) { @@ -375,5 +379,26 @@ func (w convertWriter) Write(data []byte) (int, error) { if err == nil && n != len(data) { err = io.ErrShortWrite } + if err == nil { + err = w.context.Err() + } + return n, err +} + +// WriteString 写入字符串并保留底层原生写入能力,前后响应取消 +// 入参: data 待写文本 +// 返回: int 写入字节数, error 写入、短写或取消错误 +func (w convertWriter) WriteString(data string) (int, error) { + if err := w.context.Err(); err != nil { + return 0, err + } + n, err := io.WriteString(w.writer, data) + *w.count += int64(n) + if err == nil && n != len(data) { + err = io.ErrShortWrite + } + if err == nil { + err = w.context.Err() + } return n, err } diff --git a/ofdgo_pdf.go b/ofdgo_pdf.go index 65aad6d..4c6995f 100644 --- a/ofdgo_pdf.go +++ b/ofdgo_pdf.go @@ -151,10 +151,9 @@ func ImportPDF(ctx context.Context, source io.ReaderAt, size int64, options PDFI if !finite(renderer.DPI) || renderer.DPI <= 0 { return nil, PDFImportReport{}, fmt.Errorf("invalid PDF raster DPI") } - if options.OnProgress != nil { - if err := options.OnProgress("open", 0, 0); err != nil { - return nil, PDFImportReport{}, err - } + progress := editorProgress(options.OnProgress).withContext(ctx) + if err := progress.report("open", 0, 0); err != nil { + return nil, PDFImportReport{}, err } editor := NewEditor() editor.SetRenderBackends(renderer.Backends()) @@ -168,7 +167,7 @@ func ImportPDF(ctx context.Context, source io.ReaderAt, size int64, options PDFI readWarning = func(diagnostic pdfgo.Diagnostic) { diagnostics = append(diagnostics, diagnostic) } readOptions.Warning = func(diagnostic pdfgo.Diagnostic) { readWarning(diagnostic) } } - reader, err := pdfgo.NewReaderWithOptions(source, size, readOptions) + reader, err := pdfgo.NewReaderWithOptions(convertReader{context: ctx, source: source}, size, readOptions) if err != nil { return nil, PDFImportReport{}, err } @@ -206,10 +205,8 @@ func ImportPDF(ctx context.Context, source io.ReaderAt, size int64, options PDFI } readWarning = importer.warning } - if options.OnProgress != nil { - if err := options.OnProgress("pages", 0, 0); err != nil { - return nil, PDFImportReport{}, err - } + if err := progress.report("pages", 0, 0); err != nil { + return nil, PDFImportReport{}, err } err = reader.WalkPages(ctx, func(index int, page *pdfgo.Page) error { _, width, height := codecpdf.PageMatrix(page) @@ -220,10 +217,7 @@ func ImportPDF(ctx context.Context, source io.ReaderAt, size int64, options PDFI importer.pageOrder = append(importer.pageOrder, page) importer.pageIDs[page.Reference] = editor.pages[index].ID importer.pageIndexes[page.Reference] = index - if options.OnProgress != nil { - return options.OnProgress("pages", index+1, 0) - } - return nil + return progress.report("pages", index+1, 0) }) if err != nil { return nil, PDFImportReport{}, err @@ -241,10 +235,8 @@ func ImportPDF(ctx context.Context, source io.ReaderAt, size int64, options PDFI } importer.page = 0 } - if options.OnProgress != nil { - if err := options.OnProgress("convert", 0, len(editor.pages)); err != nil { - return nil, PDFImportReport{}, err - } + if err := progress.report("convert", 0, len(editor.pages)); err != nil { + return nil, PDFImportReport{}, err } err = reader.WalkPages(ctx, func(index int, page *pdfgo.Page) error { matrix, width, height := codecpdf.PageMatrix(page) @@ -284,13 +276,13 @@ func ImportPDF(ctx context.Context, source io.ReaderAt, size int64, options PDFI return fmt.Errorf("import PDF page %d: %w", index+1, err) } importer.report.Pages++ - if options.OnProgress != nil { - if err := options.OnProgress("convert", index+1, len(editor.pages)); err != nil { - return err - } + if err := progress.report("convert", index+1, len(editor.pages)); err != nil { + return err } if options.Progress != nil { - return options.Progress(index + 1) + if err := options.Progress(index + 1); err != nil { + return err + } } return ctx.Err() }) diff --git a/ofdgo_pdf_composite_draw.go b/ofdgo_pdf_composite_draw.go index 47973b3..fc1c767 100644 --- a/ofdgo_pdf_composite_draw.go +++ b/ofdgo_pdf_composite_draw.go @@ -231,6 +231,8 @@ func (c *pdfCompositor) drawPaint(node pdfCompositeNode, outline bool, paint pdf } var uniformValues [4]float64 var uniformPoint pdfgo.Point + uniformChannels := pdfTransferChannels + selectChannels := (c.channels != nil || transfer != nil && c.transferModel == "DeviceCMYK") && (compatible || marked != nil || patternChannels != nil) uniformReady := false var sampler *pdfCompositeColorSampler if g := paint.Axial; (node.image == nil || node.image.Image.ImageMask) && shading == nil && g != nil && g.Bounds == nil && g.Background == nil { @@ -259,8 +261,14 @@ func (c *pdfCompositor) drawPaint(node pdfCompositeNode, outline bool, paint pdf if err := c.importer.ctx.Err(); err != nil { return err } + row := pdfCoverageRow(coverage, y, region.Min.X, region.Max.X) for x := region.Min.X; x < region.Max.X; x++ { - a := baseimage.AlphaAt(coverage, x, y) + var a uint32 + if row != nil { + a = uint32(row[4*(x-region.Min.X)+3]) * 257 + } else { + a = baseimage.AlphaAt(coverage, x, y) + } if a == 0 { continue } @@ -325,6 +333,14 @@ func (c *pdfCompositor) drawPaint(node pdfCompositeNode, outline bool, paint pdf } else { uniformValues, _, err = codecpdf.CompositeColor(paint, pdfgo.Point{}, space, style.RenderingIntent, style.ColorConversion) } + if selectChannels { + uniformChannels = 0 + for j, selected := range codecpdf.PaintChannels(uniformValues, compatible, marked) { + if selected { + uniformChannels |= 1 << j + } + } + } uniformReady = true } values = uniformValues @@ -391,11 +407,15 @@ func (c *pdfCompositor) drawPaint(node pdfCompositeNode, outline bool, paint pdf } pixels[index].Shape += shape * (1 - pixels[index].Shape) bits := pdfTransferChannels - if (c.channels != nil || transfer != nil && c.transferModel == "DeviceCMYK") && (compatible || marked != nil || patternChannels != nil) { - bits = 0 - for j, selected := range codecpdf.PaintChannels(values, compatible, marked) { - if selected { - bits |= 1 << j + if selectChannels { + if uniformReady { + bits = uniformChannels + } else { + bits = 0 + for j, selected := range codecpdf.PaintChannels(values, compatible, marked) { + if selected { + bits |= 1 << j + } } } if patternChannels != nil { diff --git a/ofdgo_pdf_composite_scratch.go b/ofdgo_pdf_composite_scratch.go index 755a5b3..73f2336 100644 --- a/ofdgo_pdf_composite_scratch.go +++ b/ofdgo_pdf_composite_scratch.go @@ -30,6 +30,18 @@ type pdfCompositeScratch struct { rasters [8]*image.RGBA } +// pdfCoverageRow 读取完整范围内的原生覆盖行,其他类型沿用逐点采样 +// 入参: coverage 覆盖图像, y 行坐标, left 起始列, right 结束列 +// 返回: []byte RGBA像素或空值 +func pdfCoverageRow(coverage image.Image, y, left, right int) []byte { + buffer, ok := coverage.(*image.RGBA) + if !ok || y < buffer.Rect.Min.Y || y >= buffer.Rect.Max.Y || left < buffer.Rect.Min.X || right > buffer.Rect.Max.X || left >= right { + return nil + } + start := buffer.PixOffset(left, y) + return buffer.Pix[start : start+4*(right-left)] +} + // acquireCoverage 借用当前块的独占画布,保留活动画布供嵌套合成读取 // 返回: *image.RGBA 未清空画布,不支持缓冲绘制的后端返回nil, error 无效绘制区域 func (c *pdfCompositor) acquireCoverage() (*image.RGBA, error) { diff --git a/ofdgo_pdf_shading.go b/ofdgo_pdf_shading.go index c4d5104..bc9dacc 100644 --- a/ofdgo_pdf_shading.go +++ b/ofdgo_pdf_shading.go @@ -21,6 +21,7 @@ import ( "strconv" basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom" + baseimage "github.com/xiaoqidun/ofdgo/internal/base/image" codecpdf "github.com/xiaoqidun/ofdgo/internal/codec/pdf" "github.com/xiaoqidun/pdfgo" ) @@ -612,7 +613,7 @@ func (c *pdfCompositor) gradient(paint pdfgo.Paint, space *pdfgo.ColorSpace, nod for sx := range samples { weight := 1.0 if coverage != nil { - _, _, _, a := coverage.At(x*samples+sx, y*samples+sy).RGBA() + a := baseimage.AlphaAt(coverage, x*samples+sx, y*samples+sy) weight = float64(a) / 65535 if weight == 0 { continue diff --git a/ofdgo_raster.go b/ofdgo_raster.go index 9c22b67..67b0f24 100644 --- a/ofdgo_raster.go +++ b/ofdgo_raster.go @@ -130,15 +130,14 @@ func (m RasterMatrix) Apply(p RasterPoint) RasterPoint { return RasterPoint{m[0]*p.X + m[2]*p.Y + m[4], m[1]*p.X + m[3]*p.Y + m[5]} } -// Inverse 求逆矩阵,退化或行列式非有限时返回错误 +// Inverse 求有限逆矩阵,复用几何变换的溢出、下溢及相消处理 // 返回: RasterMatrix 逆矩阵, error 无效矩阵 func (m RasterMatrix) Inverse() (RasterMatrix, error) { - det := m[0]*m[3] - m[1]*m[2] - if det == 0 || math.IsNaN(det) || math.IsInf(det, 0) { + inverse, ok := MatrixFromValues([6]float64(m)).Invert() + if !ok { return RasterMatrix{}, fmt.Errorf("invalid raster transform") } - return RasterMatrix{m[3] / det, -m[1] / det, -m[2] / det, m[0] / det, - (m[2]*m[5] - m[3]*m[4]) / det, (m[1]*m[4] - m[0]*m[5]) / det}, nil + return RasterMatrix(inverse.Values()), nil } // At 采样局部坐标下的渐变,使用预乘颜色插值 diff --git a/ofdgo_raster_clip.go b/ofdgo_raster_clip.go index 19987b0..965a932 100644 --- a/ofdgo_raster_clip.go +++ b/ofdgo_raster_clip.go @@ -21,6 +21,8 @@ import ( "image" "image/color" "math" + + baseimage "github.com/xiaoqidun/ofdgo/internal/base/image" ) // rasterClipKey 区分页面尺寸、分辨率和裁剪路径 @@ -102,9 +104,10 @@ func rasterClipMask(backend RasterBackend, page *RasterPage, path []RasterSegmen return nil, err } for y := bounds.Min.Y; y < bounds.Max.Y; y++ { - for x := bounds.Min.X; x < bounds.Max.X; x++ { - _, _, _, alpha := pixels.At(x-bounds.Min.X, y-bounds.Min.Y).RGBA() - mask.SetAlpha(x, y, color.Alpha{uint8(alpha >> 8)}) + start := mask.PixOffset(bounds.Min.X, y) + row := mask.Pix[start : start+bounds.Dx()] + for x := range row { + row[x] = uint8(baseimage.AlphaAt(pixels, x, y-bounds.Min.Y) >> 8) } } } diff --git a/ofdgo_render_output.go b/ofdgo_render_output.go index d845e25..0aff578 100644 --- a/ofdgo_render_output.go +++ b/ofdgo_render_output.go @@ -172,6 +172,8 @@ func (r *Renderer) RenderToText(page *PageContent, writer io.Writer) error { if err != nil { return err } + var written int64 + writer = convertWriter{context: ctx, writer: writer, count: &written} _, err = text.WriteTo(writer) if err != nil { return err @@ -201,6 +203,8 @@ func (r *Renderer) RenderToMultiPageText(writer io.Writer, indices ...int) error if err := r.exportProgress(0, len(indices)); err != nil { return err } + var count int64 + writer = convertWriter{context: ctx, writer: writer, count: &count} written := false for i, index := range indices { text, err := r.PageTextByIndexContext(ctx, index) @@ -235,11 +239,13 @@ func (r *Renderer) RenderToImage(page *PageContent) (image.Image, error) { // RenderToPNG 渲染为PNG,像素尺寸由DPI决定 // 入参: page 页面内容, writer 输出流 -// 返回: error 错误信息 +// 返回: error 渲染、写入或取消错误 func (r *Renderer) RenderToPNG(page *PageContent, writer io.Writer) error { if err := r.Compression.Validate(); err != nil { return err } + var written int64 + writer = convertWriter{context: r.outputContext(), writer: writer, count: &written} img, err := r.RenderToImage(page) if err != nil { return err @@ -262,11 +268,13 @@ func (r *Renderer) RenderToPNG(page *PageContent, writer io.Writer) error { // RenderToJPEG 渲染为白底JPEG,像素尺寸由DPI决定 // 入参: page 页面内容, writer 输出流, options 编码选项,nil使用标准库默认值 -// 返回: error 错误信息 +// 返回: error 渲染、写入或取消错误 func (r *Renderer) RenderToJPEG(page *PageContent, writer io.Writer, options *jpeg.Options) error { if err := r.Compression.Validate(); err != nil { return err } + var written int64 + writer = convertWriter{context: r.outputContext(), writer: writer, count: &written} img, err := r.RenderToImage(page) if err != nil { return err @@ -320,7 +328,7 @@ func (r *Renderer) RenderToPDF(page *PageContent, writer io.Writer) error { // RenderToEPS 渲染为EPS // 入参: page 页面内容, writer 输出流 -// 返回: error 错误信息 +// 返回: error 渲染、写入或取消错误 func (r *Renderer) RenderToEPS(page *PageContent, writer io.Writer) error { if err := r.Compression.Validate(); err != nil { return err @@ -328,6 +336,8 @@ func (r *Renderer) RenderToEPS(page *PageContent, writer io.Writer) error { if r.backends.EPS == nil { return fmt.Errorf("eps: %w", ErrBackendUnavailable) } + var written int64 + writer = convertWriter{context: r.outputContext(), writer: writer, count: &written} return r.backends.EPS.RenderEPS(r, page, writer) } diff --git a/ofdgo_render_svg.go b/ofdgo_render_svg.go index f8ace6a..faf4908 100644 --- a/ofdgo_render_svg.go +++ b/ofdgo_render_svg.go @@ -64,7 +64,7 @@ func (r *Renderer) RenderToSVGWithObjects(page *PageContent, writer io.Writer) ( // renderSVG 将SVG输出及对象分组交给指定后端 // 入参: page 页面内容, writer 输出流, mode 资源封装方式 -// 返回: SVGResources 字体和图片资源, error 输出错误 +// 返回: SVGResources 字体和图片资源, error 渲染、写入或取消错误 func (r *Renderer) renderSVG(page *PageContent, writer io.Writer, mode SVGMode) (SVGResources, error) { if err := r.Compression.Validate(); err != nil { return SVGResources{}, err @@ -72,5 +72,7 @@ func (r *Renderer) renderSVG(page *PageContent, writer io.Writer, mode SVGMode) if r.backends.SVG == nil { return SVGResources{}, fmt.Errorf("svg: %w", ErrBackendUnavailable) } + var written int64 + writer = convertWriter{context: r.outputContext(), writer: writer, count: &written} return r.backends.SVG.RenderSVG(r, page, writer, mode) } diff --git a/ofdgo_renderer_canvas.go b/ofdgo_renderer_canvas.go index 3df72c8..1ce8ba0 100644 --- a/ofdgo_renderer_canvas.go +++ b/ofdgo_renderer_canvas.go @@ -99,6 +99,7 @@ func (b CanvasBackend) renderImage(page *RasterPage, img *image.RGBA) (image.Ima } r := rasterizer.FromImage(img, canvas.DPMM(page.DPI/25.4), space) masks := renderCache[[32]byte, *image.Alpha]{limit: 16 << 20} + var pathData []float64 for i, cmd := range page.Commands { if cmd.Image != nil { source, err := baseimage.PixelData(cmd.Image) @@ -119,23 +120,11 @@ func (b CanvasBackend) renderImage(page *RasterPage, img *image.RGBA) (image.Ima } continue } - p := &canvas.Path{} - for _, s := range cmd.Path { - switch s.Verb { - case RasterMove: - p.MoveTo(s.End.X, s.End.Y) - case RasterLine: - p.LineTo(s.End.X, s.End.Y) - case RasterQuad: - p.QuadTo(s.Control1.X, s.Control1.Y, s.End.X, s.End.Y) - case RasterCubic: - p.CubeTo(s.Control1.X, s.Control1.Y, s.Control2.X, s.Control2.Y, s.End.X, s.End.Y) - case RasterClose: - p.Close() - default: - return nil, fmt.Errorf("canvas command %d: unsupported path verb %d", i, s.Verb) - } + p, err := rasterCanvasPath(cmd.Path, pathData) + if err != nil { + return nil, fmt.Errorf("canvas command %d: %w", i, err) } + pathData = p.Data()[:0] paint := canvas.Paint{Color: cmd.Paint.Color} var sampled rasterGradientCanvas if g := cmd.Paint.Gradient; g != nil { @@ -220,6 +209,44 @@ func (b CanvasBackend) renderImage(page *RasterPage, img *image.RGBA) (image.Ima return img, nil } +// rasterCanvasPath 预留路径容量,保留光栅指令的构建及闭合规则 +// 入参: path 只读光栅路径, buffer 可复用的独占路径缓冲 +// 返回: *canvas.Path 使用独占缓冲的绘制路径, error 不支持的指令 +func rasterCanvasPath(path []RasterSegment, buffer []float64) (*canvas.Path, error) { + capacity := 0 + for _, segment := range path { + switch segment.Verb { + case RasterQuad: + capacity += 6 + case RasterCubic: + capacity += 8 + default: + capacity += 4 + } + } + if cap(buffer) < capacity { + buffer = make([]float64, 0, capacity) + } + p := canvas.NewPathFromData(buffer[:0]) + for _, s := range path { + switch s.Verb { + case RasterMove: + p.MoveTo(s.End.X, s.End.Y) + case RasterLine: + p.LineTo(s.End.X, s.End.Y) + case RasterQuad: + p.QuadTo(s.Control1.X, s.Control1.Y, s.End.X, s.End.Y) + case RasterCubic: + p.CubeTo(s.Control1.X, s.Control1.Y, s.Control2.X, s.Control2.Y, s.End.X, s.End.Y) + case RasterClose: + p.Close() + default: + return nil, fmt.Errorf("unsupported path verb %d", s.Verb) + } + } + return p, nil +} + // At 返回原局部坐标的描边渐变颜色 // 入参: x 页面横坐标, y 页面纵坐标 // 返回: color.RGBA 预乘颜色 diff --git a/ofdgo_renderer_eps_compression.go b/ofdgo_renderer_eps_compression.go index 7ab6ee6..131b93d 100644 --- a/ofdgo_renderer_eps_compression.go +++ b/ofdgo_renderer_eps_compression.go @@ -63,21 +63,39 @@ func (r *epsImageRenderer) RenderImage(img image.Image, matrix canvas.Matrix) { var encoded bytes.Buffer compressor, _ := zlib.NewWriterLevel(&encoded, zlib.BestCompression) opaque := true + native, _ := img.(image.RGBA64Image) for y := 0; y < height; y++ { if r.err = r.ctx.Err(); r.err != nil { return } clear(pixels) for x := 0; x < width; x++ { - red, green, blue, alpha := img.At(bounds.Min.X+x, bounds.Min.Y+y).RGBA() + if x&4095 == 0 { + if r.err = r.ctx.Err(); r.err != nil { + return + } + } + var red, green, blue, alpha uint32 + if native != nil { + pixel := native.RGBA64At(bounds.Min.X+x, bounds.Min.Y+y) + red, green, blue, alpha = uint32(pixel.R), uint32(pixel.G), uint32(pixel.B), uint32(pixel.A) + } else { + red, green, blue, alpha = img.At(bounds.Min.X+x, bounds.Min.Y+y).RGBA() + } if alpha != 65535 { opaque = false } - if alpha != 0 { + if alpha == 65535 { + offset := x * 3 + pixels[offset], pixels[offset+1], pixels[offset+2] = byte(red>>8), byte(green>>8), byte(blue>>8) + } else if alpha != 0 { offset := x * 3 pixels[offset], pixels[offset+1], pixels[offset+2] = byte(red*65535/alpha>>8), byte(green*65535/alpha>>8), byte(blue*65535/alpha>>8) } } + if r.err = r.ctx.Err(); r.err != nil { + return + } if _, r.err = compressor.Write(pixels); r.err != nil { return } @@ -101,6 +119,9 @@ func (r *epsImageRenderer) RenderImage(img image.Image, matrix canvas.Matrix) { data, filter = optimized, "DCTDecode" } } + if r.err = r.ctx.Err(); r.err != nil { + return + } m := matrix.Scale(float64(width), float64(height)) _, r.err = fmt.Fprintf(r.writer, " gsave /DeviceRGB setcolorspace [%s %s %s %s %s %s] concat <>image\n", ofdNumber(m[0][0]), ofdNumber(m[1][0]), ofdNumber(m[0][1]), ofdNumber(m[1][1]), ofdNumber(m[0][2]), ofdNumber(m[1][2]), width, height, width, height, height, filter) if r.err != nil { diff --git a/ofdgo_renderer_image_canvas.go b/ofdgo_renderer_image_canvas.go index d188c5c..0743620 100644 --- a/ofdgo_renderer_image_canvas.go +++ b/ofdgo_renderer_image_canvas.go @@ -267,6 +267,7 @@ func (r *Renderer) imageWithClip(img image.Image, clipPath *canvas.Path, m canva } if baseimage.HighPrecision(img) || baseimage.HighPrecision(result) { out := image.NewNRGBA64(bounds) + source64, _ := img.(*image.NRGBA64) maskBounds := result.Bounds() opaque := true for y := bounds.Min.Y; y < bounds.Max.Y; y++ { @@ -274,6 +275,11 @@ func (r *Renderer) imageWithClip(img image.Image, clipPath *canvas.Path, m canva r.renderError = err return img } + row := out.Pix[(y-bounds.Min.Y)*out.Stride:][:w*8] + if source64 != nil { + start := source64.PixOffset(bounds.Min.X, y) + copy(row, source64.Pix[start:start+len(row)]) + } for x := bounds.Min.X; x < bounds.Max.X; x++ { if (x-bounds.Min.X)&255 == 0 { if err := work.Err(); err != nil { @@ -281,9 +287,15 @@ func (r *Renderer) imageWithClip(img image.Image, clipPath *canvas.Path, m canva return img } } - pixel := baseimage.NRGBA64At(img, x, y) alpha := baseimage.AlphaAt(result, maskBounds.Min.X+x-bounds.Min.X, maskBounds.Min.Y+y-bounds.Min.Y) opaque = opaque && alpha == 65535 + if source64 != nil { + i := (x-bounds.Min.X)*8 + 6 + a := (uint32(row[i])<<8 | uint32(row[i+1])) * alpha / 65535 + row[i], row[i+1] = byte(a>>8), byte(a) + continue + } + pixel := baseimage.NRGBA64At(img, x, y) pixel.A = uint16(uint32(pixel.A) * alpha / 65535) out.SetNRGBA64(x, y, pixel) }