把 Photopea 嵌进自己的网页:用户在页面上选一张图,图片发进 iframe 里的 Photopea 编辑;点“保存为 PNG”后,结果回到页面上,可以直接下载。全部通过官方的 Live Messaging(
postMessage)实现,不需要服务器。
先看清成品
本机用 Playwright 驱动无头 Chrome 打开这个页面(真实 Photopea iframe):Photopea 回
done后页面显示“就绪”;发送一张生成的 PNG,加模板图层,运行 A 篇脚本,再点保存,页面依次收到 3 张价签和 1 张保存结果。注意:在本机无头 Chrome 里,iframe 一开始显示的是 Photopea 首页,要点一下“Start using Photopea”才进入编辑器,测试脚本里自动点了这一步。普通浏览器里是否也会这样没有验证。没有在真人操作的浏览器里手动试过。
注意:Photopea 网页版和 API 可以免费使用;去广告的 Premium、白标嵌入用的 Distributor 账号的价格和条款以官方页面为准,本文不列数字。只编辑你有权使用的图片和字体,不要用它去除他人水印,也不要共享 Premium 账号。
做完之后你会得到什么?
| 成果 | 说明 |
|---|---|
index.html |
文件选择、保存按钮、下载链接、日志 |
| 就绪检测 | 等 Photopea 发出 done 后才允许发送 |
| 来源校验 | 只处理来自 Photopea iframe 的消息 |
e2e.py |
一键端到端测试 |
需要准备哪些环境?
| 项目 | 要求 | 说明 |
|---|---|---|
| 任意静态服务器 | 例如 python3 -m http.server |
本地打开 index.html |
| Python + Playwright | 只用于端到端测试 | pip install playwright,本机用系统 Chrome |
文件结构
27-photopea-B-src/
├── index.html
└── e2e.py # 端到端测试,会读取 A 篇的 priceTags.js
第 1 步:消息协议
| 方向 | 内容 | 含义 |
|---|---|---|
| 页面 → Photopea | 字符串 | 当作脚本执行 |
| 页面 → Photopea | ArrayBuffer | 当作文件打开 |
| Photopea → 页面 | "done" |
加载完成,或一条命令执行完 |
| Photopea → 页面 | 其他字符串 | 脚本里 app.echoToOE() 发出的 |
| Photopea → 页面 | ArrayBuffer | 脚本里 saveToOE() 导出的文件 |
第 2 步:页面
index.html:
<!doctype html>
<html lang="zh-CN">
<head><meta charset="utf-8"><title>Photopea 嵌入编辑器</title>
<style>body{font-family:sans-serif;margin:16px}#pp{width:100%;height:640px;border:1px solid #ccc}#log{font:12px monospace;white-space:pre}</style>
</head>
<body>
<input type="file" id="file" accept="image/*,.psd">
<button id="save" disabled>保存为 PNG</button>
<a id="dl" hidden download="edited.png">下载结果</a>
<span id="status">等待 Photopea 就绪…</span>
<iframe id="pp" src="https://www.photopea.com/"></iframe>
<div id="log"></div>
<script>
const pp = document.getElementById("pp");
const log = (s) => (document.getElementById("log").textContent += s + "\n");
let ready = false, waitingFile = false;
window.addEventListener("message", (e) => {
if (e.source !== pp.contentWindow) return; // 只认 Photopea iframe
if (e.data === "done") {
if (!ready) { ready = true; document.getElementById("status").textContent = "就绪"; }
document.getElementById("save").disabled = false;
return;
}
if (e.data instanceof ArrayBuffer) {
const url = URL.createObjectURL(new Blob([e.data], { type: "image/png" }));
const a = document.getElementById("dl");
a.href = url; a.hidden = false;
log("收到文件 " + e.data.byteLength + " 字节");
return;
}
log("Photopea: " + e.data);
});
document.getElementById("file").addEventListener("change", async (ev) => {
const f = ev.target.files[0];
if (!f) return;
if (!ready) { log("Photopea 还没就绪,稍后再选"); return; }
const buf = await f.arrayBuffer();
pp.contentWindow.postMessage(buf, "*"); // 发送二进制 = 在 Photopea 中打开
log("已发送 " + f.name);
});
document.getElementById("save").addEventListener("click", () => {
pp.contentWindow.postMessage('app.activeDocument.saveToOE("png");', "*");
});
</script>
</body>
</html>
要点:
e.source !== pp.contentWindow时直接忽略,防止别的窗口冒充 Photopea。- 第一次收到
done才算就绪,之后的done只表示命令执行完。 saveToOE("png")可以换成"jpg:0.8"(JPG,质量 0.8)或"psd:true",写法以官方脚本文档为准。
第 3 步:本地打开
cd 27-photopea-B-src
python3 -m http.server 8000
# 浏览器打开 http://localhost:8000/
检查结果:几秒后状态变成“就绪”。选一张图片,Photopea 里打开了它;点“保存为 PNG”,出现“下载结果”链接。
第 4 步:端到端测试
e2e.py:
"""端到端:本地页面 + 真实 Photopea iframe。打开 PNG,加 A 篇的模板图层,跑批量脚本,导出。"""
import asyncio, base64, sys, struct, zlib
from pathlib import Path
from playwright.async_api import async_playwright
def png(w=80, h=40):
raw = b"".join(b"\x00" + b"\xff\x80\x00" * w for _ in range(h))
ch = lambda t, d: struct.pack(">I", len(d)) + t + d + struct.pack(">I", zlib.crc32(t + d))
return b"\x89PNG\r\n\x1a\n" + ch(b"IHDR", struct.pack(">IIBBBBB", w, h, 8, 2, 0, 0, 0)) + ch(b"IDAT", zlib.compress(raw)) + ch(b"IEND", b"")
HTML = Path(__file__).with_name("index.html").read_text()
SCRIPT = (Path(__file__).parent.parent / "27-photopea-A-src" / "priceTags.js")
SCRIPT = SCRIPT if SCRIPT.exists() else Path(sys.argv[1])
TEMPLATE = """var d = app.activeDocument;
var names = ['名称','价格'];
for (var i = 0; i < names.length; i++) { var l = d.artLayers.add(); l.kind = LayerKind.TEXT; l.name = names[i]; l.textItem.contents = names[i]; }
app.echoToOE('layers:' + d.layers.length);"""
async def main():
async with async_playwright() as p:
b = await p.chromium.launch(channel="chrome", headless=True)
pg = await b.new_page()
await pg.route("http://local.test/", lambda r: r.fulfill(body=HTML, content_type="text/html"))
await pg.goto("http://local.test/")
await asyncio.sleep(8)
btn = pg.frame_locator("#pp").get_by_role("button", name="Start using Photopea").first
if await btn.count(): await btn.click() # 本机无头浏览器里 iframe 先显示了首页
await pg.wait_for_function("document.getElementById('status').textContent==='就绪'", timeout=120000)
await pg.set_input_files("#file", files=[{"name": "bg.png", "mimeType": "image/png", "buffer": png()}])
await pg.wait_for_timeout(3000)
post = lambda s: pg.evaluate("s => document.getElementById('pp').contentWindow.postMessage(s,'*')", s)
await post(TEMPLATE); await pg.wait_for_timeout(2000)
await post(SCRIPT.read_text()); await pg.wait_for_timeout(8000)
await pg.click("#save"); await pg.wait_for_timeout(3000)
print(await pg.inner_text("#log"))
await b.close()
asyncio.run(main())
pip install playwright
python e2e.py ../27-photopea-A-src/priceTags.js
本机输出:
已发送 bg.png
Photopea: tag 1: 手冲咖啡豆 250g
收到文件 469 字节
Photopea: tag 2: 滤杯 V60
收到文件 557 字节
Photopea: tag 3: 电子秤
收到文件 455 字节
收到文件 455 字节
(建模板那一步的 layers:3 回显,在页面把它记下来之前就被测试脚本跳过了,所以只在调试时看到过。)
调试示例
等了 2 分钟都没有收到 done
最早的探测脚本把 iframe 放好后等了 60 秒、又等了 120 秒,消息列表一直是 []。截图发现 iframe 里显示的是 Photopea 的营销首页(“Free Online Photo Editor”),编辑器根本没加载。加一步点击 iframe 里的 “Start using Photopea” 按钮后,1 秒内就收到了 ['done']。页面上有好几个同名按钮,Playwright 严格模式会报错,要用 get_by_role("button", name=...).first。
常见问题
| 现象 | 原因 | 处理 |
|---|---|---|
| 状态一直是“等待” | iframe 显示的是首页,或网络打不开 | 看 iframe 里的内容,必要时点一下“开始”按钮 |
| 发送文件没反应 | 还没就绪就发了 | 页面已经拦住,重新选择文件 |
| 下载的文件打不开 | Blob 的类型与实际格式不一致 | 改了 saveToOE 格式也要改 type |
| 消息被忽略 | 来源校验没通过 | 确认 pp 指向的就是 Photopea iframe |
模板规则
- 先等到
done再发送任何东西。 - 只信任来自 iframe 本身的消息。
- 文件用 ArrayBuffer 发送,命令用字符串发送。
- 需要去掉 Photopea 品牌做白标时,去看官方 Distributor 账号的条款,不要自己改 iframe 内容。
项目资源
| 用途 | 地址 |
|---|---|
| Photopea API 总览(配置 JSON、server 保存) | https://www.photopea.com/api/ |
| Live Messaging | https://www.photopea.com/api/live |
| 脚本 API | https://www.photopea.com/api/scripts |
| 账号与 Distributor | https://www.photopea.com/api/accounts |
| Playwright Python | https://playwright.dev/python/docs/intro |
Live Messaging 只是 postMessage 加三种消息类型,但足以把一个完整的图像编辑器变成你页面里的一个组件。用户的图片始终在浏览器里处理,你的服务器什么都不用存。