From c250ca914efcacc15e7a2676cf6aab15c7d3c2c9 Mon Sep 17 00:00:00 2001 From: xiaoqidun Date: Wed, 21 Oct 2026 15:18:52 +0800 Subject: [PATCH] =?UTF-8?q?fix(=E8=BD=AC=E6=8D=A2=E8=83=BD=E5=8A=9B):=20?= =?UTF-8?q?=E4=BF=AE=E5=A4=8DPDF=E5=AD=97=E4=BD=93=E5=8F=8A=E7=BB=86?= =?UTF-8?q?=E7=BA=BF=E6=B8=B2=E6=9F=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go.mod | 2 +- go.sum | 4 +- internal/base/font/cff.go | 33 +++++-- internal/base/font/cff_subset.go | 3 + internal/base/font/glyf.go | 2 +- internal/base/font/hint.go | 9 +- internal/base/image/scale.go | 149 ++++++++++++++++++++++++++++++- 7 files changed, 185 insertions(+), 17 deletions(-) diff --git a/go.mod b/go.mod index 01a8165..e8636c2 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/xiaoqidun/guigo v0.0.0-20261020075840-622fd8853d72 github.com/xiaoqidun/j2kgo v1.0.2 github.com/xiaoqidun/jbig2 v1.1.8 - github.com/xiaoqidun/pdfgo v0.0.0-20261021045903-4d52abc4d1c4 + github.com/xiaoqidun/pdfgo v0.0.0-20261021071541-3fdb0cab9d95 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 115f950..9b39c6d 100644 --- a/go.sum +++ b/go.sum @@ -66,8 +66,8 @@ 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-20261021045903-4d52abc4d1c4 h1:RXZ4mH75yoSxdNuXdgvRsoz6DHJGYz71+r1A14qLoj8= -github.com/xiaoqidun/pdfgo v0.0.0-20261021045903-4d52abc4d1c4/go.mod h1:AaabzsObZ8Z76AcaWbo6swRROrJsblyIGB7HbieeJA4= +github.com/xiaoqidun/pdfgo v0.0.0-20261021071541-3fdb0cab9d95 h1:Fcr2BVlFYc+BZ62OhrN55QIdkcH47ryxKWnFqCRcoE4= +github.com/xiaoqidun/pdfgo v0.0.0-20261021071541-3fdb0cab9d95/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 f95c129..0ccafcd 100644 --- a/internal/base/font/cff.go +++ b/internal/base/font/cff.go @@ -260,6 +260,21 @@ func SanitizeCFF(data []byte) ([]byte, error) { if kind == 1 { return normalizeCFFType1(data, topDict) } + private, err := readType2Private(data, topDict[18], &type2Font{}) + if err != nil { + return nil, err + } + count := len(private) + if err := normalizeCFFPrivate(private); err != nil { + return nil, err + } + if len(private) != count { + font, err := readType2Font(data) + if err != nil { + return nil, err + } + return rebuildNamedCFF(data, font, font.chars, nil) + } return data, nil } fdArrOff, err := cffDictOffset(data, topDict, 1236) @@ -342,17 +357,21 @@ func SanitizeCFF(data []byte) ([]byte, error) { return encodeSanitizedCFF(data[:hdrSize], nameIndexData, topDict, stringIndexData, globalSubrIndexData, charStringsData, finalPrivData, localSubrData), nil } -// normalizeCFFPrivate 移除旧版CFF中不生效的缺省字段,拒绝需要额外解释的取值 +// normalizeCFFPrivate 移除废弃的加粗阈值及未加密标记,拒绝加密字形程序 // 入参: dict 私有字典 // 返回: error 不支持的旧版行为 func normalizeCFFPrivate(dict cffDict) error { - for op, value := range map[int]float64{1215: 0, 1216: -1} { - if values, ok := dict[op]; ok { - if len(values) != 1 || values[0] != value { - return fmt.Errorf("unsupported legacy CFF private operator %d", op) - } - delete(dict, op) + if values, ok := dict[1215]; ok { + if len(values) != 1 || !finite(values[0]) { + return fmt.Errorf("invalid legacy CFF bold threshold") } + delete(dict, 1215) + } + if values, ok := dict[1216]; ok { + if len(values) != 1 || values[0] != -1 { + return fmt.Errorf("unsupported legacy CFF private operator 1216") + } + delete(dict, 1216) } return nil } diff --git a/internal/base/font/cff_subset.go b/internal/base/font/cff_subset.go index c9b96e3..f6ea27a 100644 --- a/internal/base/font/cff_subset.go +++ b/internal/base/font/cff_subset.go @@ -231,6 +231,9 @@ func rebuildNamedCFF(data []byte, font *type2Font, chars [][]byte, ids []uint16) if err != nil { return nil, err } + if err := normalizeCFFPrivate(private); err != nil { + return nil, err + } privateData, err := encodeCFFSubsetPrivate(private) if err != nil { return nil, err diff --git a/internal/base/font/glyf.go b/internal/base/font/glyf.go index 675b4a7..25516b2 100644 --- a/internal/base/font/glyf.go +++ b/internal/base/font/glyf.go @@ -235,7 +235,7 @@ func (f *Outliner) phantomPoints(id uint16, data []byte) [4]ttPoint { return [4]ttPoint{{x: left * 64, ox: left * 64}, {x: (left + int32(f.Font.GlyphAdvance(id))) * 64, ox: (left + int32(f.Font.GlyphAdvance(id))) * 64}, {y: top * 64, oy: top * 64}, {y: (top - int32(f.Font.GlyphVerticalAdvance(id))) * 64, oy: (top - int32(f.Font.GlyphVerticalAdvance(id))) * 64}} } -// ttRecoverableHint 判断栈参数、指令引用或控制流错误是否允许保留执行前轮廓 +// ttRecoverableHint 判断栈参数、区点轮廓及函数引用或控制流错误是否允许保留执行前轮廓 // 入参: err 指令错误 // 返回: bool 是否可恢复 func ttRecoverableHint(err error) bool { diff --git a/internal/base/font/hint.go b/internal/base/font/hint.go index c5b301c..5e57456 100644 --- a/internal/base/font/hint.go +++ b/internal/base/font/hint.go @@ -25,7 +25,7 @@ import ( // errTTStackUnderflow 表示字形程序缺少必需的栈参数 var errTTStackUnderflow = errors.New("TrueType instruction stack underflow") -// errTTInvalidReference 表示字形指令引用不存在的点或函数 +// errTTInvalidReference 表示字形指令引用不存在的区、点、轮廓或函数 var errTTInvalidReference = errors.New("invalid TrueType instruction reference") // errTTInvalidControlFlow 表示条件分支未闭合或跳转超出字节码范围 @@ -380,7 +380,8 @@ func (v *ttInterpreter) execute(code []byte, depth int) error { case op >= 0x13 && op <= 0x16: z := v.pop() if z < 0 || z > 1 { - return fmt.Errorf("invalid TrueType zone") + v.err = fmt.Errorf("%w: zone %d", errTTInvalidReference, z) + break } if op == 0x16 { g.zone = [3]int32{z, z, z} @@ -854,7 +855,7 @@ func (v *ttInterpreter) shift(op byte) { if op <= 0x35 { contour := v.pop() if contour < 0 || int64(contour) >= int64(len(v.ends)) { - v.err = fmt.Errorf("invalid TrueType contour reference") + v.err = fmt.Errorf("%w: contour %d", errTTInvalidReference, contour) return } if contour > 0 { @@ -864,7 +865,7 @@ func (v *ttInterpreter) shift(op byte) { } else { target = v.pop() if target < 0 || target > 1 { - v.err = fmt.Errorf("invalid TrueType zone") + v.err = fmt.Errorf("%w: zone %d", errTTInvalidReference, target) return } to = len(v.zones[target]) diff --git a/internal/base/image/scale.go b/internal/base/image/scale.go index 2fac6a4..c47b9c6 100644 --- a/internal/base/image/scale.go +++ b/internal/base/image/scale.go @@ -56,7 +56,7 @@ func (s rasterImageSource) RGBA64At(x, y int) color.RGBA64 { return color.RGBA64{R: uint16(r), G: uint16(g), B: uint16(b), A: uint16(a)} } -// drawRasterAxisImage 按像素中心进行轴向缩放,逐行复用横向采样并保留镜像与小数坐标 +// drawRasterAxisImage 轴向缩放复用逐行采样,细轴按面积覆盖并保留镜像与小数坐标 // 入参: dst 目标图像, src 源图像, m 像素变换, op 合成操作 // 返回: bool 是否使用轴向采样 func drawRasterAxisImage(dst draw.Image, src image.Image, m [6]float64, op draw.Op) bool { @@ -78,8 +78,14 @@ func drawRasterAxisImage(dst draw.Image, src image.Image, m [6]float64, op draw. return true } if float64(r.Dx())*math.Min(float64(r.Dy()), math.Ceil(4*math.Max(1, sy))+2) > rasterScaleBufferLimit/32 { + if sx*float64(b.Dx()) < 1 || sy*float64(b.Dy()) < 1 { + return drawRasterFootprintImage(dst, src, m, op, r) + } return false } + if !rasterFootprintWeightsFit(b, r, m) { + return drawRasterFootprintImage(dst, src, m, op, r) + } 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 { @@ -139,10 +145,13 @@ func drawRasterAxisImage(dst draw.Image, src image.Image, m [6]float64, op draw. return true } -// rasterImageAxis 按连续源坐标生成CatmullRom权重,不对图像几何边界取整 +// rasterImageAxis 按连续源坐标生成CatmullRom权重,亚像素细轴使用面积覆盖权重 // 入参: first 目标起点, end 目标终点, source 源起点, limit 源终点, scale 缩放比例, offset 平移距离 // 返回: []rasterAxisSample 轴向采样表 func rasterImageAxis(first, end, source, limit int, scale, offset float64) []rasterAxisSample { + if math.Abs(scale)*float64(limit-source) < 1 { + return rasterImageFootprintAxis(first, end, source, limit, scale, offset) + } result := make([]rasterAxisSample, 0, end-first) argument := math.Min(1, math.Abs(scale)) support := draw.CatmullRom.Support / argument @@ -183,6 +192,142 @@ func rasterImageAxis(first, end, source, limit int, scale, offset float64) []ras return result } +// rasterFootprintWeightsFit 检查细轴权重表的合计预算,乘法前排除溢出 +// 入参: source 源边界, target 目标范围, m 像素变换 +// 返回: bool 权重表是否符合预算 +func rasterFootprintWeightsFit(source, target image.Rectangle, m [6]float64) bool { + budget := rasterScaleBufferLimit / 8 + for _, axis := range [...]struct { + source, target int + scale float64 + }{{source.Dx(), target.Dx(), m[0]}, {source.Dy(), target.Dy(), m[3]}} { + if math.Abs(axis.scale)*float64(axis.source) < 1 && axis.target > 0 { + n, samples := axis.source, min(2, axis.target) + if n > budget/samples { + return false + } + budget -= n * samples + } + } + return true +} + +// rasterImageWeightSpan 提供单个目标像素的源范围和权重,不分配整轴权重表 +// 入参: pixel 目标像素, source 源起点, limit 源终点, scale 缩放比例, offset 平移距离 +// 返回: int 源起点, int 源终点, func(int)float64 源权重 +func rasterImageWeightSpan(pixel, source, limit int, scale, offset float64) (int, int, func(int) float64) { + if math.Abs(scale)*float64(limit-source) < 1 { + left, right := (float64(pixel)-offset)/scale, (float64(pixel+1)-offset)/scale + if right < left { + left, right = right, left + } + if right <= float64(source) || left >= float64(limit) { + return 0, 0, nil + } + return source, limit, func(x int) float64 { + return math.Max(0, math.Min(float64(x)+1, right)-math.Max(float64(x), left)) * math.Abs(scale) + } + } + center := (float64(pixel) + .5 - offset) / scale + if center < float64(source) || center >= float64(limit) { + return 0, 0, nil + } + center -= .5 + argument := math.Min(1, math.Abs(scale)) + support := draw.CatmullRom.Support / argument + start := int(max(float64(source), math.Floor(center-support))) + stop := int(min(float64(limit), math.Ceil(center+support))) + weight := func(x int) float64 { + distance := math.Abs((float64(x) - center) * argument) + if distance < draw.CatmullRom.Support { + return draw.CatmullRom.At(distance) + } + return 0 + } + var total float64 + for x := start; x < stop; x++ { + total += weight(x) + } + return start, stop, func(x int) float64 { return weight(x) / total } +} + +// drawRasterFootprintImage 在细轴权重超预算时按原累加顺序逐点绘制 +// 入参: dst 目标图像, src 源图像, m 像素变换, op 合成操作, target 目标范围 +// 返回: bool 是否完成绘制 +func drawRasterFootprintImage(dst draw.Image, src image.Image, m [6]float64, op draw.Op, target image.Rectangle) bool { + pixels, ok := src.(image.RGBA64Image) + if !ok { + pixels = rasterImageSource{src} + } + b := src.Bounds() + output := image.NewRGBA64(image.Rect(0, 0, 1, 1)) + var value [1]rasterImagePixel + for y := target.Min.Y; y < target.Max.Y; y++ { + firstY, endY, wy := rasterImageWeightSpan(y, b.Min.Y, b.Max.Y, m[3], m[5]) + if firstY == endY { + continue + } + for x := target.Min.X; x < target.Max.X; x++ { + firstX, endX, wx := rasterImageWeightSpan(x, b.Min.X, b.Max.X, m[0], m[4]) + if firstX == endX { + continue + } + value[0] = rasterImagePixel{} + for sy := firstY; sy < endY; sy++ { + var row rasterImagePixel + for sx := firstX; sx < endX; sx++ { + p, weight := pixels.RGBA64At(sx, sy), wx(sx) + if weight != 0 { + row.r += float64(p.R) * weight + row.g += float64(p.G) * weight + row.b += float64(p.B) * weight + row.a += float64(p.A) * weight + } + } + if weight := wy(sy); weight != 0 { + value[0].r += row.r * weight + value[0].g += row.g * weight + value[0].b += row.b * weight + value[0].a += row.a * weight + } + } + rasterImageWriteRow(dst, output, value[:], x, y, op) + } + } + return true +} + +// rasterImageFootprintAxis 对不足一个目标像素的整轴保留真实交叠面积及预乘透明度 +// 入参: first 目标起点, end 目标终点, source 源起点, limit 源终点, scale 缩放比例, offset 平移距离 +// 返回: []rasterAxisSample 轴向覆盖权重 +func rasterImageFootprintAxis(first, end, source, limit int, scale, offset float64) []rasterAxisSample { + lo, hi := float64(source)*scale+offset, float64(limit)*scale+offset + if hi < lo { + lo, hi = hi, lo + } + result := make([]rasterAxisSample, 0, min(2, end-first)) + for pixel := first; pixel < end; pixel++ { + if math.Min(float64(pixel+1), hi) > math.Max(float64(pixel), lo) { + result = append(result, rasterAxisSample{pixel: pixel, first: source}) + } + } + buffer := make([]float64, len(result)*(limit-source)) + for index := range result { + sample := &result[index] + left, right := (float64(sample.pixel)-offset)/scale, (float64(sample.pixel+1)-offset)/scale + if right < left { + left, right = right, left + } + n := limit - source + sample.weights, buffer = buffer[:n:n], buffer[n:] + for i := range sample.weights { + x := float64(source + i) + sample.weights[i] = math.Max(0, math.Min(x+1, right)-math.Max(x, left)) * math.Abs(scale) + } + } + return result +} + // rasterImageHorizontal 按源列复用像素,保留各目标像素的权重累加顺序与中间精度 // 入参: row 横向缓冲, src 源像素, samples 采样表, y 源行坐标 func rasterImageHorizontal(row []rasterImagePixel, src image.RGBA64Image, samples []rasterAxisSample, y int) {