设计实践

把 Photopea 嵌进自己的网页:用户在页面上选一张图,图片发进 iframe 里的 Photopea 编辑

把 Photopea 嵌进自己的网页:用户在页面上选一张图,图片发进 iframe 里的 Photopea 编辑;点“保存为 PNG”后,结果回到页面上,可以直接下载。全部通过官方的 Live Messaging(postMessage)实现,不需要服务器。

BINBHO · 图文教程

把 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 加三种消息类型,但足以把一个完整的图像编辑器变成你页面里的一个组件。用户的图片始终在浏览器里处理,你的服务器什么都不用存。