折腾笔记

MD2WX 双封面拼接与坐标直推实践

采用 3350x1000 左右双图拼接与归一化裁剪坐标直推草稿箱,消除多端显示比例冲突。

MD2WX 双封面拼接与坐标直推实践

作者:野生宝藏箱
项目:MD2WX(微信公众号 Markdown 排版转换器与草稿箱直推工具)
主题:tech-blue 现代科技蓝 / terminal-geek 极客终端
核心改动:双比例封面左右拼接、归一化裁剪坐标自动上报、Web 画板联动导出


00. 背景:单一封面素材与多端显示比例的冲突

经常给微信公众号投稿或运营排版的人,基本都会遇到同一个排版细节问题:

微信公众号的草稿箱 API(draft/add)中,每篇图文消息仅能关联一个封面素材 ID(即 thumb_media_id)。这意味着平台不支持分别为文章上传独立的“横版封面”和“方版封面”。

然而在微信客户端内,文章封面实际会在两种差异巨大的视口下呈现:

  1. 订阅号信息流大图:采用 2.35:1 的横屏比例(微信官方推荐 900x383 或 2350x1000),横向展开,适合呈现大标题和主视觉;
  2. 消息转发与次条列表:发送给好友、群聊、转发到朋友圈或多图文次条时,微信会自动裁切为 1:1 的正方形缩略图。

这种展示逻辑导致了常规设计上的权衡两难:

  • 如果按 2.35:1 的长图进行排版,把核心文字放在右侧,分享给好友时系统默认抓取中心区域,标题很容易被直接截断;
  • 如果为了兼顾转发,把所有视觉元素都收缩在中间 1:1 的安全区内,在信息流大图里展开时,左右两侧又会出现大面积留白,画面比例失衡。

在此之前,一种常见的变通做法是人工在图像软件里把两张图拼在一块,然后在公众号后台用鼠标手动拉伸两个裁剪框。但靠鼠标拖拽很难做到像素级对齐,比例稍有偏差就可能引起接口校验失败或显示细微白边;而且每次发文都需要重复这一步手工操作。

微信草稿箱接口本身其实留出了程序化裁剪的能力:只要在提交草稿时附带 pic_crop_235_1pic_crop_1_1 参数,就能直接指定裁切区域。结合这一特性,MD2WX 在本次更新中实现了双图水平拼接生成与归一化裁剪坐标自动上报的完整链路。


01. 拼接布局与坐标归一化计算

1. 为什么采用 3350x1000 左右拼接

拼接两张不同比例的图片通常有两种排布方式:上下纵向排布,或左右横向排布。

对比维度上下纵向拼接左右横向拼接(当前方案)
画布尺寸宽 2350,高 1000 + 1000 = 2000宽 2350 + 1000 = 3350,高 1000
对齐逻辑上方宽 2350,下方方图宽仅 1000,两侧需要补 675px 空白两部分高度完全一致(均为 1000px),无需任何填充
总像素量$2350 \times 2000 = 4,700,000$ 像素(约 4.7 MP)$3350 \times 1000 = 3,350,000$ 像素(约 3.35 MP)
资源开销存在无效空白区域,文件体积相对偏大像素利用率 100%,渲染和网络传输体积均更小

左右并排将高度统一为 1000px,既省去了空白区域的计算,又保持了紧凑的画布布局:

+-------------------------------------------------------+-----------------------------+
|                                                       |                             |
|               2.35 : 1 头条横版大图                    |       1 : 1 次条/分享方图    |
|                  (2350 x 1000)                        |        (1000 x 1000)        |
|                                                       |                             |
+-------------------------------------------------------+-----------------------------+
<--------------------- 2350 px ------------------------><---------- 1000 px --------->
<--------------------------------- 总宽 3350 px -------------------------------------->

2. 裁剪坐标归一化计算

微信接口要求的裁剪格式为 X1_Y1_X2_Y2,数值是相对于整图宽高的归一化浮点数,原点 (0, 0) 位于图片左上角,(1, 1) 位于右下角,通常保留 6 位小数。

在总宽 $W = 3350\text{px}$、总高 $H = 1000\text{px}$ 的画布中:

2.35:1 头条大图裁剪区

  • 覆盖区域:$X \in [0, 2350]$,$Y \in [0, 1000]$;
  • $X_1 = 0,\quad Y_1 = 0$;
  • $X_2 = \frac{2350}{3350} \approx 0.701492537…$,保留 6 位小数为 $0.701493$,$Y_2 = 1.0$;
  • 参数值pic_crop_235_1 = "0_0_0.701493_1"
  • 比例校验: $$\text{Ratio}_{2.35} = \frac{(0.701493 - 0) \times 3350}{(1.0 - 0) \times 1000} = \frac{2350.00155}{1000} \approx 2.350002 : 1$$ 与 2.35 标准比例的相对误差在百万分之一以内,能够稳定通过微信服务端校验。

1:1 分享方图裁剪区

  • 覆盖区域:$X \in [2350, 3350]$,$Y \in [0, 1000]$;
  • $X_1 = 0.701493,\quad Y_1 = 0$;
  • $X_2 = 1.0,\quad Y_2 = 1.0$;
  • 参数值pic_crop_1_1 = "0.701493_0_1_1"
  • 比例校验: $$\text{Ratio}_{1.0} = \frac{(1.0 - 0.701493) \times 3350}{(1.0 - 0) \times 1000} = \frac{999.99845}{1000} \approx 0.999998 : 1$$ 与 1.0 标准比例的相对误差同样在百万分之一以内。

02. CLI 引擎实现:单 HTML 双画板截图与素材兼容

MD2WX 的 CLI 工具定位为轻量级独立工具,核心能力依靠 Python 标准库实现,不强制引入 Pillow 或 OpenCV 等第三方图像处理库。为了在无外部重型依赖的前提下生成这块 3350x1000 的高分辨率封面,主要通过以下两套机制落地:

1. 无头浏览器单页面双画板排布

复用 CLI 内置的无头 Chromium 渲染能力(md2wx/cover.py),通过组装单份 HTML 页面完成并排渲染:

  • 页面视口逻辑尺寸设为 $1675\text{px} \times 500\text{px}$;
  • 页面左侧放置宽度 1175px 的横版 Banner HTML 结构;
  • 页面右侧放置宽度 500px 的方形 Square HTML 结构;
  • 启动本地 Chrome/Edge 进程时传入 --force-device-scale-factor=2 进行双倍采样,单次截屏直接输出 $3350 \times 1000$ 的 PNG 图像。
# md2wx/cover.py
DUAL_COVER_WIDTH = 1675   # 1175 + 500
DUAL_COVER_HEIGHT = 500

WECHAT_CROP_235_1 = "0_0_0.701493_1"
WECHAT_CROP_1_1 = "0.701493_0_1_1"

def build_dual_cover_html(theme_id: str, meta: Dict[str, str]) -> str:
    banner_html = _build_banner_inner_html(theme_id, meta)
    square_html = _build_square_inner_html(theme_id, meta)
    return f"""<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<style>{_COVER_CSS}</style>
</head>
<body>
<div class="cover-canvas-dual">
  {banner_html}
  {square_html}
</div>
</body>
</html>"""

生成的封面文件会根据标题、作者等元数据摘要缓存在 ~/.config/md2wx/covers/ 目录下,二次推稿时可直接读取缓存文件。

2. 自定义本地双素材拼合降级

若用户希望使用自己设计的素材,可通过命令行参数分别指定横图与方图:

  • --cover assets/banner.png
  • --cover-square assets/square.png

stitch_cover_images 方法中,代码会按以下优先级处理:

  1. 检查 Python 运行环境中是否安装了 Pillow。若有,则通过 ImageOps.fit 完成比例适配与合并;
  2. 若未安装 Pillow,则自动退回至生成本地临时 HTML,调用无头浏览器进行排版截屏;
  3. 输出结果均统一为 $3350 \times 1000$,并自动绑定上述两个裁剪参数。

03. 微信草稿箱发布(Publisher)流程

在草稿箱提交流程(md2wx/publisher.py)中,构造文章载荷时将裁剪坐标直接注入字段:

# md2wx/publisher.py
article_obj = {
    "title": title,
    "author": author,
    "digest": digest,
    "content": content_html,
    "content_source_url": content_source_url,
    "thumb_media_id": thumb_media_id,
    "need_open_comment": 1,
    "only_fans_can_comment": 0
}
if pic_crop_235_1:
    article_obj["pic_crop_235_1"] = pic_crop_235_1
if pic_crop_1_1:
    article_obj["pic_crop_1_1"] = pic_crop_1_1

payload = {"articles": [article_obj]}

使用 CLI 发布命令:

# 自动生成主题双封面并直推草稿箱
md2wx article.md --publish

终端执行日志展示了坐标的计算与生效过程:

>>> 3. 准备提交草稿至微信公众号...
    已动态生成 '现代科技蓝' 主题专属双比例拼接封面 (3350x1000):
      - 2.35:1 头条裁剪坐标: 0_0_0.701493_1
      - 1:1 次条/方图裁剪坐标: 0.701493_0_1_1
    使用封面图片: ~/.config/md2wx/covers/cover-dual-tech-blue-xxx.png
[+] 文章已成功推送到微信公众号草稿箱
    草稿标题: 深度架构思考与实践
    草稿 ID:   xxxx_draft_media_id
    双图裁剪坐标已同步生效 (头条: 0_0_0.701493_1, 方图: 0.701493_0_1_1)

发布完成后,在微信公众号后台打开草稿,即可看到头条展示为 2.35:1 的宽幅横图,而转发卡片与次条列表则准确对齐 1:1 方形构图。


04. Web 工作台同步升级

对于习惯在浏览器中使用 Web Studio(web/)的用户,前端画板也同步做了对应调整:

  1. 新增【双图合拼 (3350x1000)】选项:在原有的 2.35:1 与 1:1 单图切换旁,增加了双图合拼视图。仿真视口通过等比缩放矩阵动态适配双画板宽度,直观展示左右并排效果;
  2. 裁剪坐标复制功能:底部操作栏新增【复制裁剪坐标】按钮,点击即可将标准坐标复制到系统剪贴板,方便在第三方管理平台中直接引用:
    pic_crop_235_1: "0_0_0.701493_1"
    pic_crop_1_1: "0.701493_0_1_1"
  3. Canvas 2D / SVG 双引擎导出canvas_exporter.js 导出链路支持生成 $3350 \times 1000$ 分辨率的图片,可直接下载并在后台手工上传;
  4. 纯矢量 SVG 图标:前端界面严格遵循项目规范,全部采用 Lucide 风格的矢量 SVG 图标,不使用 Emoji。

05. 近期核心架构演进梳理 (v1.1.1 ~ v1.1.5)

除本次双比例封面方案外,近期几个小版本在正文渲染和图床集成上也完成了一些关键改进:

1. 代码块微信真机渲染兼容规范(v1.1.1 - v1.1.4)

微信公众号的富文本净化过滤器会对外部 CSS 和部分 HTML 标签进行重构,针对这套机制梳理了四项核心策略:

  • 换行与空格自包含:微信容易剥离 <pre>white-space 属性并将裸换行塌缩为单行。通过在解析层将文本节点内的换行符转为 <br>、连续空格转为 &nbsp;,确保排版结构不依赖外部样式;
  • 外层容器承载横向滚动:微信会清除 <pre> 本身的 overflow-x,因此统一在外层嵌套 div 作为滚动容器;
  • 滚动容器承担背景色:在移动端横向滑动长代码时,若背景绘制在内层元素上容易出现背景截断。将背景色和内边距直接写在外层滚动容器上,避免滑动时文字脱离底色;
  • 等宽字体栈兜底:优化字体声明顺序,优先选用 'SF Mono', Menlo, Consolas, monospace,避免因移动端缺少特定字体而导致目录树制表符(├ └ │ ─)错位。

2. Cloudflare R2 图床客户端与自动换链(v1.1.5)

  • 提供基于 Cloudflare Worker + R2 的轻量级图床部署模板,内建基于客户端 IP 的限流逻辑(默认 60 请求/分钟);
  • 发布流程自动识别 Markdown 内的本地相对路径图片,上传至图床并自动替换为在线链接,减少手动上传步骤。

06. 总结

从早期的纯标准库 Markdown 解析器、iPhone 仿真视口,到多主题设计,再到本次通过 3350x1000 水平拼接与归一化裁剪坐标直推草稿箱,MD2WX 的改动主要集中在减少写作之后的样式调试和人工修补步骤。

后续将继续围绕移动端排版体验与发布自动化完善功能,欢迎体验并反馈使用中的问题。


开源主页https://github.com/zaneven/MD2WX
在线工作台https://md2wx.zaneven.com