本文更新于 2026 年 11 月。
一个 App 上架要准备的截图,是「尺寸 × 语言 × 张数」:iPhone 和 iPad 各一套,Google Play 一套,每种语言一套,每套 5–10 张。手工在设计软件里一张张导出,改一句文案就要全部重来。
一个常见的自动化做法是:把截图页面写成 HTML 模板,用无头浏览器渲染成图片。CSS 排版、文字自动换行、多语言字体回退都是浏览器现成的能力,同一个模板换个尺寸、换一份文案就能批量出图。本文用 Python 版的 Playwright 讲一遍这条流水线,以及几个容易踩的坑。
一、先定尺寸
下面是常用的几档(2026 年 11 月对照官方页面,以 App Store Connect 和 Play 管理中心的实时提示为准):
| 平台 | 尺寸(竖屏,像素) | 说明 |
|---|---|---|
| iPhone 6.9 英寸 | 1320×2868、1290×2796、1260×2736 | 和 6.5 英寸二选一 |
| iPhone 6.5 英寸 | 1284×2778、1242×2688 | 没提供的尺寸,App Store 用大一档的自动缩放 |
| iPad 13 英寸 | 2064×2752、2048×2732 | 支持 iPad 的 App 必须提供 |
| Google Play 手机 | 常用 1080×1920(9:16) | 边长 320–3840 px,长边不超过短边的 2 倍;想进推荐位至少 4 张、≥ 1080 px |
两家共同的硬性要求:
- 尺寸要精确:App Store 只接受表里的尺寸,差一个像素也会被拒
- 不能有透明通道:Apple 要求截图不含 alpha,Google Play 要求 JPEG 或 24 位 PNG(无 alpha)。哪怕画面完全不透明,PNG 文件带着 alpha 通道也可能被拒
出处:Apple「Screenshot specifications」、Google Play 管理中心帮助「添加预览资源」。
二、模板:CSS 像素就是最终像素
模板里画布的 CSS 尺寸直接写成目标像素,后面截图时再处理清晰度(第三节)。截图素材建议转成 data URL 填进模板,或者本地起一个静态服务:set_content 写进去的页面没有基准地址,相对路径的图片加载不到。
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<style>
html, body {
margin: 0; }
.canvas {
width: 1290px; height: 2796px; overflow: hidden;
display: flex; flex-direction: column; align-items: center;
background: linear-gradient(160deg, #4f46e5, #0ea5e9);
}
.title {
margin: 180px 96px 0; color: #fff; text-align: center;
font: 700 96px/1.2 "Noto Sans CJK SC", sans-serif;
overflow-wrap: anywhere;
}
.phone {
margin-top: 120px; width: 78%; }
.phone img {
display: block; width: 100%; }
</style>
</head>
<body>
<div class="canvas">
<div class="title">{
{title}}</div>
<div class="phone"><img src="{
{screenshot}}"></div>
</div>
</body>
</html>
多语言时有两件事要注意:
- 每种语言写对
lang,字体也分开指定。同一个汉字在中文和日文里字形不同,中文用Noto Sans CJK SC,日文用Noto Sans CJK JP,不要靠系统回退碰运气 - 文案长度差很多。德语、法语的标题经常比中文长一倍,可以在截图前跑一段脚本,超过两行就逐步缩小字号:
const el = document.querySelector('.title');
const maxLines = 2;
// 用元素自己的高度算行数;scrollHeight 会把超出行框的字形也算进去,偏大
const lines = () =>
Math.round(el.getBoundingClientRect().height / parseFloat(getComputedStyle(el).lineHeight));
let size = parseFloat(getComputedStyle(el).fontSize);
while (lines() > maxLines && size > 48) {
size -= 4;
el.style.fontSize = `${
size}px`;
}
缩到下限(这里是 48 px)还超过两行的,说明文案本身太长,应该改文案,而不是继续缩小。
三、渲染:等一切就绪再截图
最常见的问题是截图截早了:字体还是回退字体,图片还没解码,偶尔出一张缺图的。wait_until="load" 不够,要显式等三件事:
WAIT_READY = """async () => {
await document.fonts.ready; // 网页字体加载完
await Promise.all([...document.images].map(
img => img.decode().catch(() => {}))); // 图片解码完
await new Promise(r => requestAnimationFrame(
() => requestAnimationFrame(r))); // 等两帧,确保已经画上屏
}"""
async def render(context, html: str, w: int, h: int) -> bytes:
page = await context.new_page()
try:
await page.set_viewport_size({
"width": w, "height": h})
await page.set_content(html, wait_until="load")
await page.evaluate(WAIT_READY)
return await page.screenshot(type="png")
finally:
await page.close()
注意 document.images 不包含 CSS 背景图。背景图也要等的话,改成用 <img> 放,或者先用 new Image() 预加载再 decode()。
清晰度:用 2 倍像素比渲染,再缩回目标尺寸。创建上下文时设 device_scale_factor=2,截出来是 2 倍大的图,再用高质量算法缩回目标尺寸。相当于超采样,设备外壳的斜边、细线和小字边缘会更干净:
# browser = await playwright.chromium.launch()
context = await browser.new_context(device_scale_factor=2)
from io import BytesIO
from PIL import Image
def finalize(png: bytes, w: int, h: int, out_path: str) -> None:
img = Image.open(BytesIO(png))
# 比例不一致说明模板尺寸写错了:直接报错,不要悄悄裁掉一块
if img.width * h != img.height * w:
raise ValueError(f"比例不一致:{img.size} vs {(w, h)}")
img = img.resize((w, h), Image.Resampling.LANCZOS)
# 去掉透明通道:铺白底,转成 RGB
img = img.convert("RGBA")
flat = Image.new("RGB", img.size, (255, 255, 255))
flat.paste(img, mask=img.getchannel("A"))
flat.save(out_path, "PNG") # 或 flat.save(out_path, "JPEG", quality=92)
比例不一致时宁可报错也不要按比例裁切:裁切会让导出结果和模板里看到的不一样,而且往往要等到上传商店时才发现。
四、批量:浏览器复用、定期重建、任务队列
浏览器复用:每个 worker 进程启动一个浏览器,每个任务开一个新页面,用完关掉。启动浏览器要几百毫秒到一秒,不要每张图启动一次。
定期重建:长时间运行后,浏览器进程的内存会慢慢涨。比较稳的做法是每个上下文处理几十个任务后关掉重建,浏览器崩溃时整个重启。具体多少个任务重建一次,按内存监控来定。
任务队列:截图任务适合放进队列,由多个 worker 消费。用 Redis Streams 的消费者组,worker 崩溃时没确认的任务不会丢(XAUTOCLAIM 需要 Redis 6.2 及以上):
import redis.asyncio as redis
from redis.exceptions import ResponseError
STREAM, GROUP = "render:tasks", "renderers"
async def worker(r: redis.Redis, name: str) -> None:
try:
await r.xgroup_create(STREAM, GROUP, id="0", mkstream=True)
except ResponseError:
pass # 消费者组已经存在
while True:
# 先认领别的 worker 拿走后超过 60 秒没确认的任务(比如那个 worker 崩了)
res = await r.xautoclaim(STREAM, GROUP, name, min_idle_time=60_000, count=10)
msgs = [m for m in res[1] if m[1]]
if not msgs:
resp = await r.xreadgroup(GROUP, name, {
STREAM: ">"}, count=1, block=5_000)
msgs = [m for _, entries in (resp or []) for m in entries]
for msg_id, fields in msgs:
await handle(fields) # 渲染、保存;结果按任务 ID 覆盖写,重跑没有副作用
await r.xack(STREAM, GROUP, msg_id)
几个要点:
- 处理成功才
XACK。失败了不确认,消息留在待处理列表里,过一会儿会被别的 worker 认领重试 - 结果幂等:输出文件按「任务 ID + 语言 + 尺寸」命名、覆盖写,同一个任务跑两次也不会出错
- 限制重试次数:用
XPENDING看投递次数,超过几次就记成失败、移出队列,避免一张坏模板无限重试
五、容器里的字体
服务端渲染最常见的「本地正常、线上变方块」,基本都是字体问题。Playwright 的 install --with-deps 会装上一些基础字体,但设计用的字体要自己装,并在 CSS 里写明:
RUN apt-get update \
&& apt-get install -y --no-install-recommends fonts-noto-cjk fonts-noto-color-emoji \
&& rm -rf /var/lib/apt/lists/*
RUN pip install playwright && playwright install --with-deps chromium
商用字体注意授权范围,服务器上渲染出图一般属于「嵌入 / 服务端使用」,和桌面使用的授权不一定一样。
六、常见问题对照
| 现象 | 原因 | 办法 |
|---|---|---|
| 偶尔缺图、字体没生效 | 截图早于加载 | 等 fonts.ready、img.decode()、两帧 requestAnimationFrame |
| CSS 背景图偶尔没出来 | document.images 不含背景图 |
改用 <img>,或预加载后再截 |
| 中文、日文变方块 | 容器里没有对应字体 | 装字体,CSS 里写明字体名 |
| 导出图边上少了一块 | 比例不一致时被裁切 | 比例不一致直接报错 |
| 上传商店被拒:含 alpha | PNG 带透明通道 | 铺白底转 RGB,或导出 JPG |
| 跑久了内存越来越大 | 页面、上下文累积 | 每个任务新页面,定期重建上下文 |
| worker 崩了任务卡住 | 消息停在待处理列表 | XAUTOCLAIM 认领,结果幂等 |
现成的工具
不想自己搭这一套的话,也有现成的在线工具。Pixsolo 是一个在浏览器里使用的在线设计平台,主线是 App Store 和 Google Play 上架截图设计(App截图套装),另外提供智能抠图、智能消除、文档去底纹、AI 生图,以及免费的设备样机、3D 设备样机、视频转GIF、图片编辑和图片工具箱,一共 10 个工具。

App截图套装上传截图后会按分辨率自动套上真实设备外壳(150 多款,苹果和安卓都有,含平板和手表),模板按两家商店的尺寸做好,可以多语言、多尺寸批量导出。导出预设包括 iPhone 6.5 英寸(1242×2688、1284×2778)、iPad 13 英寸(2064×2752、2048×2732)和 Android 常用尺寸,其他尺寸(比如 6.9 英寸)可以自定义画布。可以免费上手设计,导出需要付费;建议导出 JPG(没有透明通道)。
常见问题
有什么在线工具可以做 App Store 上架截图?
有开发能力的团队可以用本文的办法,或者用 fastlane 的 snapshot + frameit 自动截图、套外壳。不想写代码的话,可以用在线工具,比如 Pixsolo 的 App截图套装:截图自动匹配设备外壳,模板按商店尺寸做好,批量导出。
App Store 和 Google Play 的截图尺寸要求是什么?有工具能一键导出吗?
iPhone 准备一套 6.9 英寸或 6.5 英寸,支持 iPad 再准备一套 13 英寸;Google Play 常用 1080×1920,长边不超过短边的 2 倍。两家都不接受带透明通道的截图。尺寸和多语言多的话,用模板批量导出最省事,自己写脚本或用在线工具都可以。
Playwright 截图里的中文变成方块怎么办?
容器里缺中文字体。装 fonts-noto-cjk 这类字体,并在 CSS 里写明字体名。
Playwright 截图偶尔缺图片、字体没生效?
截图截早了。截图前等 document.fonts.ready、所有图片 decode() 完成,再等两帧 requestAnimationFrame。