# 分享卡(share card) 两处入口都在 `route/question/share.go`:`ShareCard`(箱子卡,960×540)、`QuestionShareCard`(单题卡)。 单题卡出图条件:**已回答 + 非私密 + 无站方处置**(`db.Restrictions.ActiveLevel` 非空即拒绝,fail-closed)。 ## 文字排版 - 居中/限宽**必须**用 `measureTextWidth`(逐字 `HMetric().AdvanceWidth` + `Kern`,`scale = fontSize*64`), 它与 `DrawString` 返回笔位逐位相等。绝不要用「中文 1em / 其余 em 系数」估算:URL 里的 `: / . _ -` 只有 0.25~0.35em,整句会虚高 55%,居中直接偏 40px。 - 链接展示走 `displayURL`(去 `https://`)→ `layoutLink`(单行 15→13px,放不下按 host/path 两行 13→11px, host 超宽才中间省略,字号下限 `LinkMinSize`)→ `ellipsizeMiddle`。限宽恒定 `QRCardW-40`。 ## 二维码(`generateQR`) - **图像只输出符号**:`qr.DisableBorder = true`,静默区由承载它的白色卡片提供。 把 4 模块静默区画进图里 + 再居中留白,会让 165px 目标只剩 111px(3px/模块)——这是曾经的回归。 - `perModule = size / modules` 向下取整,符号 = `modules*perModule`;**调用方必须用 `qr.Bounds().Dx()` 排版与居中**,不能假设等于传入的 size。 - 单题卡 `QQRSize = 190`(37 模块 → 185px = 5px,卡片 236×300 居中于 679,120); 箱子卡 `QRCodeSize = 190`(37 模块 → 185px = 5px,卡片 270×335)。 - 单题卡的二维码卡片绘制在 `drawQuestionQRCard`(handler 只是调用),测试可直接出图。 - 卡内垂直间距在 4px 栅格上:`24 / 17 / 20 / 1 / 20 / 16`,总高按链接行数算,整块垂直居中。 ## 模块像素:唯一的硬指标 | 模块 | 960 | 720 | 480 | 320 | 240 | |---|---|---|---|---|---| | 4px(148px,37 模块) | o | o | **x** | x | x | | 5px(185px,37 模块) | o | o | o | o | x | (go-qrcode 纠错 Q、真实 55 字符链接、含内嵌头像;用 gozxing 解码,它比手机严格,可当保守下限。) → **目标 5px/模块**;`TestGenerateQRModuleSizeAtLeast5` 锁住三种常见链接长度,改小 QRCodeSize 立刻 FAIL。 改大模块的路线排序(成本从低到高): 1. 同画布放大二维码(已做,0 CPU)——`generateQR` 只 0.8ms,与尺寸几乎无关。 2. 降纠错 Q→M:模块 37→33 只省 11%,容错 25%→15%,不划算,别单独用。 3. 换 PNG:只保住原图,缩到 480 一样解不出,体积还是 JPEG 的 5 倍。 4. 2x 渲染:+60~100ms(本地基准:JPEG 编码 12→48ms、背景 Lanczos 25→41ms、画布 3→13ms)。 只在「原图本身也要更精细(背景/文字抗锯齿)」时值得。 ## 缓存命中率才是 2x 之外更值得看的地方 进程内 LRU 只有 16 条 + TTL 1h,站点箱子多时冷渲染频繁;把生成结果落盘 / 上 R2 能让「贵」只付一次。这是比 2x 更实在的优化方向。 ## 验证手法 - 守门测试 `route/question/share_layout_test.go`:测量=笔位、链接限宽、按着墨边界判居中 ±3px、 二维码 `width == modules×perModule` 且吃掉目标尺寸 ≥80%、头像不在边缘裁切。改完都做变异验证。 - `TEST_PREVIEW=1 go test ./route/question/ -run TestShareCardLinkPreview` 出 270×315 的卡区域 PNG。 - 图片实测:PIL 数着墨 bbox(居中偏差 / 二维码宽度 / 各段间距);解码用 `github.com/makiuchi-d/gozxing` 起个临时 module 跑(`_ "image/jpeg"` 和 `_ "image/png"` 都要 import)。 - 单题卡要真实出图:`.workbuddy/local/qcard_probe.py`(发提问 → 登录 → 回答 → 拉图 → 删除,不入库)。 ## 缓存 进程内 TTL 1 小时 + 响应头 `Cache-Control: public, max-age=300`。改完要复验:重启(清进程内缓存)+ URL 加随机参数。