用 Photopea 脚本批量出价签图:做一张带“名称”“价格”两个文字图层的模板,脚本依次换成每个商品的名字和价格,每换一次导出一张 PNG。逻辑在 Node 里用模拟的
app对象测过,也在真实 Photopea 里跑通了。
先看清成品
本机做了两种验证:①
node --test用模拟app跑了 3 个测试,全部通过;② 用无头 Chrome 打开真实 Photopea,通过 Live Messaging 发送同一份脚本,收到 3 条tag N回显和 3 个 PNG 文件(469 / 557 / 455 字节)。第一版脚本在 Photopea 里什么都没做,原因是 Photopea 里也有module变量(见调试示例),已修复。没有手动在 File → Script 菜单里运行过,这个菜单入口来自官方文档。
注意:Photopea 网页版和 API 可以免费使用;去广告的 Premium、白标嵌入用的 Distributor 账号的价格和条款以官方页面为准,本文不列数字。只编辑你有权使用的图片和字体,不要用它去除他人水印,也不要共享 Premium 账号。
做完之后你会得到什么?
| 成果 | 说明 |
|---|---|
priceTags.js |
一份脚本,既能在 Photopea 里跑,也能被 Node 引入做测试 |
| 模板约定 | 文字图层名必须是“名称”和“价格” |
| 价格校验 | 非数字或负数直接报错,统一成 ¥59.9 这种格式 |
| 测试 | 正常导出、缺图层、价格格式 |
需要准备哪些环境?
| 项目 | 要求 | 说明 |
|---|---|---|
| 浏览器 | 近期版本的 Chrome、Edge 或 Firefox | 打开 www.photopea.com,不需要登录 |
| Node.js | 18 以上(本机 v22) | 只用于本地测试 |
| 模板 | PSD 或任意图片 | 在 Photopea 里加两个文字图层 |
文件结构
27-photopea-A-src/
├── priceTags.js # 粘贴进 Photopea 运行
└── priceTags.test.cjs # Node 测试,用模拟的 app
第 1 步:准备模板
- 打开 Photopea,File → Open 打开背景图(或 File → New 新建)。
- 用文字工具(T)写两段字,在图层面板里把它们分别重命名为
名称和价格。 - 调好字体、字号和位置。脚本只换文字,样式保留。
检查结果:图层面板里能看到 名称、价格 两个文字图层(图标是 T)。
第 2 步:脚本
priceTags.js:
// Photopea 脚本:批量替换模板里的“名称”“价格”文字图层,每个商品导出一张 PNG。
// 用法:File → Script 粘贴运行;或通过 Live Messaging 发给 iframe。
var ITEMS = [
{ name: "手冲咖啡豆 250g", price: "59.9" },
{ name: "滤杯 V60", price: "89" },
{ name: "电子秤", price: "129" }
];
function findLayer(doc, layerName) {
for (var i = 0; i < doc.layers.length; i++) {
if (doc.layers[i].name === layerName) return doc.layers[i];
}
return null;
}
function formatPrice(p) {
var n = Number(p);
if (!isFinite(n) || n < 0) throw new Error("价格不合法: " + p);
return "¥" + (Math.round(n * 100) / 100);
}
function runPriceTags(app, items) {
var doc = app.activeDocument;
var nameLayer = findLayer(doc, "名称");
var priceLayer = findLayer(doc, "价格");
if (!nameLayer || !priceLayer) {
app.echoToOE("ERROR 缺少名为“名称”或“价格”的文字图层");
return 0;
}
for (var i = 0; i < items.length; i++) {
nameLayer.textItem.contents = items[i].name;
priceLayer.textItem.contents = formatPrice(items[i].price);
app.echoToOE("tag " + (i + 1) + ": " + items[i].name);
doc.saveToOE("png");
}
return items.length;
}
// 注意:Photopea 里 typeof module 也是 "object",所以要用 app 判断运行环境
if (typeof app !== "undefined" && app.activeDocument !== undefined) {
runPriceTags(app, ITEMS);
} else if (typeof module !== "undefined") {
module.exports = { runPriceTags: runPriceTags, formatPrice: formatPrice };
}
要点:
doc.saveToOE("png")是脚本 API 的导出方法,会把当前画面以 PNG 发给“外部环境”(嵌入它的网页,见 B 篇)。直接在 photopea.com 运行时没有外部页面接收文件。手动使用时,把saveToOE换成你习惯的导出方式,或者用 B 篇的嵌入页接收。- 每次
saveToOE前先改文字,所以导出的图和商品一一对应。 - 只用
for循环和 ES5 语法,原因见调试示例 2。
第 3 步:本地测试
priceTags.test.cjs:
const test = require("node:test");
const assert = require("node:assert");
const { runPriceTags, formatPrice } = require("./priceTags.js");
function mockApp(layerNames) {
const log = [], saved = [];
const layers = layerNames.map((n) => ({ name: n, textItem: { contents: "" } }));
const doc = { layers, saveToOE: (f) => saved.push(f + "|" + layers.map((l) => l.textItem.contents).join("/")) };
return { app: { activeDocument: doc, echoToOE: (s) => log.push(s) }, log, saved };
}
test("每个商品导出一次", () => {
const m = mockApp(["名称", "价格", "背景"]);
const n = runPriceTags(m.app, [{ name: "A", price: "59.9" }, { name: "B", price: 89 }]);
assert.strictEqual(n, 2);
assert.deepStrictEqual(m.saved, ["png|A/¥59.9/", "png|B/¥89/"]);
});
test("缺少图层时报错且不导出", () => {
const m = mockApp(["标题"]);
assert.strictEqual(runPriceTags(m.app, [{ name: "A", price: 1 }]), 0);
assert.match(m.log[0], /^ERROR/);
assert.strictEqual(m.saved.length, 0);
});
test("价格校验", () => {
assert.strictEqual(formatPrice("0.105"), "¥0.11");
assert.throws(() => formatPrice("abc"));
assert.throws(() => formatPrice(-1));
});
node --test priceTags.test.cjs
检查结果:# pass 3、# fail 0。
第 4 步:在 Photopea 里运行
- 把
ITEMS改成你的商品。 - File → Script,粘贴整个
priceTags.js,点 Run。 - 如果是通过 B 篇的嵌入页运行,页面日志会依次出现
tag 1: …、tag 2: …,每条后面跟一个“收到文件”。
检查结果:回显条数和导出的文件数都等于商品数。
调试示例
1. 脚本在 Photopea 里没有任何反应
第一版脚本的结尾是:
if (typeof module !== "undefined") module.exports = {...};
else runPriceTags(app, ITEMS);
在 Node 里没问题,但在 Photopea 里没有回显,也没有导出,加了 try/catch 也没报错。逐行回显排查后发现:
Photopea: t0 object ← app.echoToOE("t0 " + typeof module)
Photopea 的脚本环境里 module 也存在,所以脚本走进了“导出”分支,runPriceTags 根本没执行。改成先判断 app 是否存在之后就正常了。
2. forEach 写法静默失败
在端到端测试里,第一版建模板的脚本用了 ['名称','价格'].forEach(...),结果没有回显,也没有建出图层。改成普通 for 循环后输出 layers:3。原因没有查到官方说明,保险起见,Photopea 脚本里一律用 ES5 的 for 循环。
常见问题
| 现象 | 原因 | 处理 |
|---|---|---|
日志出现 ERROR 缺少名为… |
图层名不对,或文字被栅格化了 | 检查图层名,必须是文字图层 |
| 只导出 1 张 | 商品列表写错,或 saveToOE 被你删掉了 |
看回显条数 |
| 字体变了 | 换字后字体缺字(如中文字体不含 ¥) | 换一个含该字符的字体 |
| 直接在官网运行看不到文件 | saveToOE 只发给外部嵌入页 |
用 B 篇的页面接收 |
模板规则
- 文字图层命名固定,脚本按名字找图层,不按顺序。
- 数据和逻辑分开:
ITEMS在最上面,函数不读全局变量。 - 先在 Node 里用模拟对象测逻辑,再去真实环境跑。
- Photopea 脚本只用 ES5 语法。
项目资源
| 用途 | 地址 |
|---|---|
| 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 |
| 脚本入门(Learn) | https://www.photopea.com/learn/scripts |
脚本对 Photopea 来说就是一段 JavaScript。把逻辑写成接收 app 参数的函数,同一份代码既能在本地测,也能在编辑器里跑。批量出图时,图层命名就是脚本和设计师之间的约定。