设计实践

用 Photopea 脚本批量出价签图:做一张带“名称”“价格”两个文字图层的模板

用 Photopea 脚本批量出价签图:做一张带“名称”“价格”两个文字图层的模板,脚本依次换成每个商品的名字和价格,每换一次导出一张 PNG。逻辑在 Node 里用模拟的 app 对象测过,也在真实 Photopea 里跑通了。

BINBHO · 图文教程

用 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 步:准备模板

  1. 打开 Photopea,File → Open 打开背景图(或 File → New 新建)。
  2. 用文字工具(T)写两段字,在图层面板里把它们分别重命名为 名称 和 价格。
  3. 调好字体、字号和位置。脚本只换文字,样式保留。

检查结果:图层面板里能看到 名称、价格 两个文字图层(图标是 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 里运行

  1. 把 ITEMS 改成你的商品。
  2. File → Script,粘贴整个 priceTags.js,点 Run。
  3. 如果是通过 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 参数的函数,同一份代码既能在本地测,也能在编辑器里跑。批量出图时,图层命名就是脚本和设计师之间的约定。