fix(转换能力): 修复PDF字体及细线渲染
build.yaml / build (push) Successful in 2m4s

This commit is contained in:
xiaoqidun committed 2026-10-21 15:18:52 +08:00
1 parent 201082daa0
commit c250ca914e
7 files changed
+185 -17

No files matched your search

+1 -1
View File
@@ -11,7 +11,7 @@ require (
github.com/xiaoqidun/guigo v0.0.0-20261020075840-622fd8853d72 github.com/xiaoqidun/guigo v0.0.0-20261020075840-622fd8853d72
github.com/xiaoqidun/j2kgo v1.0.2 github.com/xiaoqidun/j2kgo v1.0.2
github.com/xiaoqidun/jbig2 v1.1.8 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/crypto v0.58.0
golang.org/x/image v0.47.0 golang.org/x/image v0.47.0
golang.org/x/text v0.43.0 golang.org/x/text v0.43.0
+2 -2
View File
@@ -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/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 h1:dERPW7F8gs18a/DOD9gXHR3fP1Vl6KhCNsLnq5l3BIo=
github.com/xiaoqidun/jbig2 v1.1.8/go.mod h1:9jromqVwMqlptUDEt6Yip8vqYA5I1YjhG7FgMl/Z2dA= 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-20261021071541-3fdb0cab9d95 h1:Fcr2BVlFYc+BZ62OhrN55QIdkcH47ryxKWnFqCRcoE4=
github.com/xiaoqidun/pdfgo v0.0.0-20261021045903-4d52abc4d1c4/go.mod h1:AaabzsObZ8Z76AcaWbo6swRROrJsblyIGB7HbieeJA4= 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 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=
+26 -7
View File
@@ -260,6 +260,21 @@ func SanitizeCFF(data []byte) ([]byte, error) {
if kind == 1 { if kind == 1 {
return normalizeCFFType1(data, topDict) 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 return data, nil
} }
fdArrOff, err := cffDictOffset(data, topDict, 1236) 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 return encodeSanitizedCFF(data[:hdrSize], nameIndexData, topDict, stringIndexData, globalSubrIndexData, charStringsData, finalPrivData, localSubrData), nil
} }
// normalizeCFFPrivate 移除旧版CFF中不生效的缺省字段,拒绝需要额外解释的取值 // normalizeCFFPrivate 移除废弃的加粗阈值及未加密标记,拒绝加密字形程序
// 入参: dict 私有字典 // 入参: dict 私有字典
// 返回: error 不支持的旧版行为 // 返回: error 不支持的旧版行为
func normalizeCFFPrivate(dict cffDict) error { func normalizeCFFPrivate(dict cffDict) error {
for op, value := range map[int]float64{1215: 0, 1216: -1} { if values, ok := dict[1215]; ok {
if values, ok := dict[op]; ok { if len(values) != 1 || !finite(values[0]) {
if len(values) != 1 || values[0] != value { return fmt.Errorf("invalid legacy CFF bold threshold")
return fmt.Errorf("unsupported legacy CFF private operator %d", op)
}
delete(dict, op)
} }
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 return nil
} }
+3
View File
@@ -231,6 +231,9 @@ func rebuildNamedCFF(data []byte, font *type2Font, chars [][]byte, ids []uint16)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if err := normalizeCFFPrivate(private); err != nil {
return nil, err
}
privateData, err := encodeCFFSubsetPrivate(private) privateData, err := encodeCFFSubsetPrivate(private)
if err != nil { if err != nil {
return nil, err return nil, err
+1 -1
View File
@@ -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}} 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 指令错误 // 入参: err 指令错误
// 返回: bool 是否可恢复 // 返回: bool 是否可恢复
func ttRecoverableHint(err error) bool { func ttRecoverableHint(err error) bool {
+5 -4
View File
@@ -25,7 +25,7 @@ import (
// errTTStackUnderflow 表示字形程序缺少必需的栈参数 // errTTStackUnderflow 表示字形程序缺少必需的栈参数
var errTTStackUnderflow = errors.New("TrueType instruction stack underflow") var errTTStackUnderflow = errors.New("TrueType instruction stack underflow")
// errTTInvalidReference 表示字形指令引用不存在的点或函数 // errTTInvalidReference 表示字形指令引用不存在的区、点、轮廓或函数
var errTTInvalidReference = errors.New("invalid TrueType instruction reference") var errTTInvalidReference = errors.New("invalid TrueType instruction reference")
// errTTInvalidControlFlow 表示条件分支未闭合或跳转超出字节码范围 // errTTInvalidControlFlow 表示条件分支未闭合或跳转超出字节码范围
@@ -380,7 +380,8 @@ func (v *ttInterpreter) execute(code []byte, depth int) error {
case op >= 0x13 && op <= 0x16: case op >= 0x13 && op <= 0x16:
z := v.pop() z := v.pop()
if z < 0 || z > 1 { if z < 0 || z > 1 {
return fmt.Errorf("invalid TrueType zone") v.err = fmt.Errorf("%w: zone %d", errTTInvalidReference, z)
break
} }
if op == 0x16 { if op == 0x16 {
g.zone = [3]int32{z, z, z} g.zone = [3]int32{z, z, z}
@@ -854,7 +855,7 @@ func (v *ttInterpreter) shift(op byte) {
if op <= 0x35 { if op <= 0x35 {
contour := v.pop() contour := v.pop()
if contour < 0 || int64(contour) >= int64(len(v.ends)) { 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 return
} }
if contour > 0 { if contour > 0 {
@@ -864,7 +865,7 @@ func (v *ttInterpreter) shift(op byte) {
} else { } else {
target = v.pop() target = v.pop()
if target < 0 || target > 1 { if target < 0 || target > 1 {
v.err = fmt.Errorf("invalid TrueType zone") v.err = fmt.Errorf("%w: zone %d", errTTInvalidReference, target)
return return
} }
to = len(v.zones[target]) to = len(v.zones[target])
+147 -2
View File
@@ -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)} return color.RGBA64{R: uint16(r), G: uint16(g), B: uint16(b), A: uint16(a)}
} }
// drawRasterAxisImage 按像素中心进行轴向缩放,逐行复用横向采样并保留镜像与小数坐标 // drawRasterAxisImage 轴向缩放复用逐行采样,细轴按面积覆盖并保留镜像与小数坐标
// 入参: dst 目标图像, src 源图像, m 像素变换, op 合成操作 // 入参: dst 目标图像, src 源图像, m 像素变换, op 合成操作
// 返回: bool 是否使用轴向采样 // 返回: bool 是否使用轴向采样
func drawRasterAxisImage(dst draw.Image, src image.Image, m [6]float64, op draw.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 return true
} }
if float64(r.Dx())*math.Min(float64(r.Dy()), math.Ceil(4*math.Max(1, sy))+2) > rasterScaleBufferLimit/32 { 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 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]) 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]) 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 { 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 return true
} }
// rasterImageAxis 按连续源坐标生成CatmullRom权重,不对图像几何边界取整 // rasterImageAxis 按连续源坐标生成CatmullRom权重,亚像素细轴使用面积覆盖权重
// 入参: first 目标起点, end 目标终点, source 源起点, limit 源终点, scale 缩放比例, offset 平移距离 // 入参: first 目标起点, end 目标终点, source 源起点, limit 源终点, scale 缩放比例, offset 平移距离
// 返回: []rasterAxisSample 轴向采样表 // 返回: []rasterAxisSample 轴向采样表
func rasterImageAxis(first, end, source, limit int, scale, offset float64) []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) result := make([]rasterAxisSample, 0, end-first)
argument := math.Min(1, math.Abs(scale)) argument := math.Min(1, math.Abs(scale))
support := draw.CatmullRom.Support / argument support := draw.CatmullRom.Support / argument
@@ -183,6 +192,142 @@ func rasterImageAxis(first, end, source, limit int, scale, offset float64) []ras
return result 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 按源列复用像素,保留各目标像素的权重累加顺序与中间精度 // rasterImageHorizontal 按源列复用像素,保留各目标像素的权重累加顺序与中间精度
// 入参: row 横向缓冲, src 源像素, samples 采样表, y 源行坐标 // 入参: row 横向缓冲, src 源像素, samples 采样表, y 源行坐标
func rasterImageHorizontal(row []rasterImagePixel, src image.RGBA64Image, samples []rasterAxisSample, y int) { func rasterImageHorizontal(row []rasterImagePixel, src image.RGBA64Image, samples []rasterAxisSample, y int) {