@爱特,我让ai帮我修复了 你看看
# 签章白底盖住落款和日期
现象:用 OFDGo 打开带电子签章的 OFD 时,章不是只压住红印底下的字,而是一整块矩形把落款和时间都挡住。红印压在日期上是公文盖章的正常效果;整块挡住,是印章里的白底被当成不透明内容画到了正文上面。
本文说明原因、修复方式和相关代码,供上游合入时对照。
## 原因
电子签章(GB/T 38540 SES)的外观通常是一份小 OFD,或一张 PNG/GIF/JPEG。外层用 `StampAnnot` 的 `Boundary` 把它放到页面上,这个框正好压在单位落款和日期上。很多数科等阅读器生成的印章里,除了红印,还有一块铺满整个框的白底:白路径,或者没有透明通道的白底图片。`Clip` 经常不写。
OFDGo 先画完正文,再在 `WalkPage` 末尾调用 `DrawStamp`,印章永远叠在文字上面。这一层叠放是对的。问题出在白底没有被当成透明。
已有的 `Renderer.TransparentBackground` 只关掉印章页自己的页面底色。`renderCanvasPageToContext` 的 `drawBackground=false`,以及编译路径里 `TransparentBackground=true`,都只是不再额外铺一层页面白。印章内容里的白矩形、白描边,以及 RGB 接近白色且 alpha 为不透明的像素,仍然按普通源覆盖画进 `Boundary`。
两条绘制路径都会出现这个问题:
1. 屏幕 SVG。`RenderPreview` 在非光栅模式下走 Canvas,入口是 `Renderer.renderStamp`。
2. PNG、打印和编译输出。`RenderToPNG` / `CompilePage` 走 `semanticCompiler.drawStampContent`。
`s.Type == "ofd"` 且 `Clip == nil` 时,Canvas 路径把整页印章缩放到 `Stamp.Box`。图片章则把位图拉满这个框。没有 `Clip` 时,白底的范围就是整个签章框。
## 修复
只在绘制印章外观时去掉不透明近白底,不改 OFD 文件,也不改正文里的白色对象。
判定:8 位通道 R、G、B 都大于等于 250,且 alpha 不透明(16 位 alpha 大于等于 `0x8000`)。`color.Color.RGBA()` 的 16 位下限是 `250 * 257 = 64250`。渐变和图案不按这个规则丢弃。
做法:
- 给 `Renderer` 增加内部标志 `stampPlate`。只有印章子渲染器打开它,`childRenderer` 会把标志传给印章内部的嵌套内容。正文渲染器保持关闭。
- OFD 印章:子渲染器设置 `stampPlate = true`,并继续设置 `TransparentBackground = true`。
- 矢量纯色:Canvas 跳过近白填充和近白描边。编译器在 `command` 里直接不生成这条指令。
- 图像:`imageWithoutStampPlate` 把近白不透明像素的 alpha 改成 0。没有这类像素时返回原图,避免无谓重编码。直接放置的 PNG/GIF/JPEG 章,以及 OFD 印章内部的图片对象,都走这里。
红印像素保留。红印压到的字仍然会被盖住。
近白阈值留了缝:低于 250 的灰底、以及渐变或图案形式的底,不会被去掉。印章字符如果本来就是用白色像素画在红印上,这些像素会变成镂空,底下的纸面或文字能透出来,这和实体印章的阴文效果一致。
## 相关代码
### 近白判断和图像抠底
`ofdgo_stamp_plate.go`
```go
const stampPlateMin = 250 * 257
func stampPlateColor(c color.Color) bool {
if c == nil {
return false
}
r, g, b, a := c.RGBA()
return a >= 0x8000 && r >= stampPlateMin && g >= stampPlateMin && b >= stampPlateMin
}
func imageWithoutStampPlate(ctx context.Context, img image.Image) (image.Image, error) {
// 解码后扫描 NRGBA。R、G、B 均 >= 250 且 alpha > 0 的像素改为 alpha 0。
// 没有任何改动时返回传入的原图。
}
标志只作用于印章子渲染器
ofdgo_renderer.go
TransparentBackground bool
stampPlate bool
childRenderer 复制该标志:
renderer.TransparentBackground = r.TransparentBackground
renderer.stampPlate = r.stampPlate
Canvas:OFD 印章打开抠底后再画
ofdgo_renderer_stamp_canvas.go 的 renderStamp。无 Clip 时仍按矢量缩放进 Stamp.Box,但子渲染器会丢掉白底:
renderer := r.childRenderer(reader)
renderer.stampPlate = true
renderer.TransparentBackground = true
ctx.Scale(s.Box.W/sealBox.W, s.Box.H/sealBox.H)
err = renderer.renderCanvasPageToContext(ctx, content, false)
有 Clip 时仍走 renderOFDStampImage。那里同样打开 stampPlate,再光栅化。图片章和光栅化结果进入 renderStampImage,调用 imageWithoutStampPlate。
Canvas:跳过近白填充和描边
ofdgo_renderer_path_canvas.go 的 renderPath:
if shouldFill && r.stampPlate && style.fillPattern == nil && stampPlatePaint(style.fillPaint) {
shouldFill = false
}
if shouldStroke && r.stampPlate && style.strokePattern == nil && stampPlatePaint(style.strokePaint) {
shouldStroke = false
}
stampPlatePaint 只接受 color.Color。渐变对象断言失败,不会被跳过。
Canvas:OFD 印章内部的图片
ofdgo_renderer_image_canvas.go 的 renderImage,在效果处理之后:
if r.stampPlate {
img, err = imageWithoutStampPlate(r.pageContext, img)
}
编译路径:OFD 印章同样打开标志
ofdgo_render_compile_image.go 的 drawStampContent:
renderer := c.renderer.childRenderer(reader)
renderer.stampPlate = true
renderer.TransparentBackground = true
page, err := renderer.CompilePage(content)
直接的图片章在 stampImage 里调用 imageWithoutStampPlate。印章内部图片在 addImage 里、仅当 stampPlate 为真时抠底。
编译路径:不生成近白纯色指令
ofdgo_render_compile.go 的 command:
if c.renderer.stampPlate && paint.Gradient == nil && paint.Mesh == nil && stampPlateColor(paint.Color) {
return nil
}
填充和普通描边都会进入 command,所以同色的白填充和白描边都不会进入页面指令。
测试
ofdgo_stamp_plate_test.go 的 TestStampPlateDoesNotCoverPage。
页面是 20mm 黑色矩形。印章框是上方 20mm×10mm,左半是不透明白,右半是红。分别用 PNG 章和内嵌白路径加红路径的 OFD 章,走 RenderToPNG 和 Canvas 光栅化。红印必须还在;红印左侧、同一行上的像素必须是页面黑色,不能是白或浅灰。
go test -count=1 -run TestStampPlateDoesNotCoverPage .