perf(share): 用「同画布放大二维码」替代 2x 渲染,模块 4px → 5px

960×540 的卡被聊天软件缩到 480 宽时,二维码每模块只剩 2px,解码器读不出。
2x 渲染能解决,但实测成本不低(见下),先用零代价的办法把模块做大。

实测矩阵(go-qrcode 纠错 Q / 真实 55 字符链接 / 37 模块,含内嵌头像):
  尺寸 148px = 4.0px/模块 -> 960 o 720 o 480 x 320 x
  尺寸 190px = 5.0px/模块 -> 960 o 720 o 480 o 320 o 240 x
纠错降到 M(33 模块)只省 11% 模块数,容错却从 25% 掉到 15%,不划算;
PNG 只能保住原图,缩到 480 一样解不出,体积还是 JPEG 的 5 倍。

- QRCodeSize 165 → 190(37 模块取整为 5px/模块;生产短域名 33 模块为 165px)
- QRCardH 315 → 335 容纳变高的二维码,卡片整体仍垂直居中
- 新增 TestGenerateQRModuleSizeAtLeast5:三种常见链接长度下模块必须 ≥5px,
  把 QRCodeSize 改回 165 立刻 FAIL

渲染成本基准(1x vs 2x):JPEG 编码 12 → 48ms、背景 Lanczos 缩放 25 → 41ms、
画布分配 3 → 13ms,二维码生成本身 0.8ms —— 2x 的代价主要在编码与缩放,
与二维码尺寸无关,所以放大二维码不增加任何 CPU。
This commit is contained in:
tamakyi
2026-10-22 23:36:02 +08:00
parent 9fb003cc39
commit 72f08cd7c9
2 changed files with 29 additions and 3 deletions
+4 -3
View File
@@ -68,14 +68,15 @@ const (
// 右侧二维码卡片尺寸 // 右侧二维码卡片尺寸
const ( const (
QRCardW = 270 QRCardW = 270
QRCardH = 315 QRCardH = 335
QRCardX = ContentX + ContentW - QRCardW - 15 QRCardX = ContentX + ContentW - QRCardW - 15
QRCardY = ContentY + (ContentH-QRCardH)/2 QRCardY = ContentY + (ContentH-QRCardH)/2
) )
// 二维码尺寸 // 二维码尺寸。190 在 37 模块(High 纠错、55 字符链接)下取整为 5px/模块:
// 实测 5px/模块的卡缩到 480、320 宽仍可解码,4px 时 480 就解不出。
const ( const (
QRCodeSize = 165 QRCodeSize = 190
) )
// 社交信息 // 社交信息
+25
View File
@@ -228,6 +228,31 @@ func TestGenerateQRIsModuleAligned(t *testing.T) {
} }
} }
// 模块像素是扫码能力的唯一硬指标:实测 5px/模块的卡缩到 320 宽仍可解码,
// 4px 时缩到 480 就解不出。这里锁住常见链接长度下的模块尺寸。
func TestGenerateQRModuleSizeAtLeast5(t *testing.T) {
for _, url := range []string{
"https://box.shiroko.one/_/tamakyi",
"https://box-dev-api.337845818.best/_/tbunread1022181904",
"https://box.shiroko.one/_/a-rather-long-handle-name",
} {
img, err := generateQR(url, nil, QRCodeSize)
if err != nil {
t.Fatal(err)
}
probe, err := qrcode.New(url, qrcode.High)
if err != nil {
t.Fatal(err)
}
probe.DisableBorder = true
mods := len(probe.Bitmap())
if per := img.Bounds().Dx() / mods; per < 5 {
t.Errorf("%s: %d modules in %dpx -> %dpx/module, want >=5",
url, mods, img.Bounds().Dx(), per)
}
}
}
// 视觉自检:TEST_PREVIEW=1 时把链接区渲成 PNG,人工确认断行与对齐。 // 视觉自检:TEST_PREVIEW=1 时把链接区渲成 PNG,人工确认断行与对齐。
func TestShareCardLinkPreview(t *testing.T) { func TestShareCardLinkPreview(t *testing.T) {
if os.Getenv("TEST_PREVIEW") == "" { if os.Getenv("TEST_PREVIEW") == "" {