
做PS插件最常听到的需求就是把图片里的文字“抠”出来。之前我用过一个在线OCR网站得把PS里的图导出来再传到网页上识别完再复制回来来回折腾。所以后来我干脆自己写了一个Photoshop CEP插件把OCR文字识别功能直接嵌进PS里菜单一按选个区域文字就直接出来了还能一键生成文字图层。这篇就把我的实现思路、踩过的坑、关键代码都梳理一遍给想做PS插件但还没入门的朋友一个完整参考。1. 整体设计与技术选型为什么用CEP而不是UXP先说结论要做PS内的OCR插件CEPCommon Extensibility Platform通用扩展平台是目前最成熟、资料最多、坑也最好查的方案。Adobe这两年一直在推新一代插件架构UXP想取代老旧的CEP但PS版本迭代到现在CEP依然是生态最全的尤其是在Windows环境下的稳定性和对Node.js的支持程度都比UXP要实在。1.1 核心需求拆解OCR插件到底要做什么一个能用的PS OCR插件至少要满足三个核心场景场景一识别截稿图、长图里的文字。很多设计师拿到甲方发来的截图、样机图里面的文字要提取成可编辑文本手动敲一遍效率极低。场景二识别扫描件、PDF转出来的图片。这类图往往有倾斜、噪点、低对比度对OCR引擎的预处理能力要求高。场景三识别UI设计稿里的文案。切图时经常会发现图层没整理好、文字变形状了这时候直接识别比肉眼抄快得多。围绕这三个场景插件需要具备的能力清单如下功能模块具体需求优先级图像采集截取当前文档选区或获取整个画布像素数据必须图像预处理灰度化、二值化、降噪、倾斜矫正高OCR识别引擎支持中英文混合识别、数字、标点必须结果输出识别文本显示在面板中可一键复制、一键生成文字图层高批量处理多文件/多选区批量识别导出文本清单可选1.2 CEP插件架构与UXP对比为什么老技术更香Adobe CEP基于一个大家非常熟悉的底层组合Chromium老一点的版本是CEF新版本是Chromium Embedded Framework Node.js HTML/CSS/JavaScript。也就是说写CEP插件基本等于写一个桌面端的Electron应用UI用HTML/CSS画业务逻辑用JS写底层的文件读写、系统调用可以走Node.js能力。UXP则是Adobe推的新架构底层用的是自研的JavaScript引擎支持和PS UI风格更一致的原生控件运行性能也更好。但目前UXP有个硬伤它不允许插件直接运行Node.js环境很多依赖文件系统、外部命令行的功能实现起来很绕。OCR识别恰恰需要大量的本地图像处理和外部引擎调用CEP的Node环境能省掉很多麻烦。我做这个插件时选型的对比结论是如果只做简单的脚本扩展ScriptUI或ExtendScript不适合复杂业务逻辑也不适合现代化UI如果做小众功能且要求UI与PS完全统一UXP可以试但遇到信号机制不完善、模块加载限制时很头大做OCR这类重逻辑、重本地资源调用的插件CEP是最合适的资料多、社区活跃、Node生态可以直接复用。提示如果你的PS版本是2021以上CEP依旧兼容只是安装目录变成统一的CEPExtensions文件夹。只要manifest.xml配置对了新版PS都能加载。2. 核心细节解析与实操要点CEP插件的最小骨架下面我直接给出一个最简但能运行的CEP插件项目结构你在本机照着建PS里就能刷出菜单。2.1 项目目录结构与manifest.xml配置CEP插件由两部分组成CSXS扩展配置文件和Client客户端HTML页面。最简单的目录结构如下com.example.ocrplugin/ ├── CSXS/ │ └── manifest.xml ├── client/ │ ├── index.html │ ├── css/ │ │ └── style.css │ ├── js/ │ │ ├── CSInterface.js │ │ └── index.js │ └── libs/ │ └── (这里放Node端脚本CEP里可以配置为Node运行) └── node/ ├── ocrWorker.js └── (第三方OCR引擎相关文件)manifest.xml是插件的身份证它声明了插件名称、支持的最低PS版本、面板UI的入口文件、以及是否启用Node.js支持。核心配置如下?xml version1.0 encodingUTF-8? ExtensionManifest xmlnshttp://www.adobe.com/extension/manifest/1.0 ExtensionList Extension Idcom.example.ocrplugin Version1.0.0/ /ExtensionList ExecutionEnvironment HostList Host NamePHSP Version[16.0,99.9]/ Host NamePHXS Version[16.0,99.9]/ /HostList LocaleList Locale CodeAll/ /LocaleList /ExecutionEnvironment DispatchInfoList Extension Idcom.example.ocrplugin DispatchInfo Resources MainPath./client/index.html/MainPath ScriptPath./client/js/index.js/ScriptPath /Resources Lifecycle AutoVisibletrue/AutoVisible /Lifecycle UI TypePanel/Type MenuOCR文字识别/Menu Geometry Size Height500/Height Width340/Width /Size /Geometry /UI /DispatchInfo /Extension /DispatchInfoList /ExtensionManifest需要特别注意的配置项Host NamePHSP表示Photoshop主程序有些版本也能兼容到Photoshop StarterPHXS。要兼容中文版、英文版PS建议两个都写上。Version[16.0,99.9]中的16.0对应PS CC 2014。虽然你本机装的是新版本但版本下限尽量放宽方便分享给同事用旧版。AutoVisibletrue表示安装后自动显示面板通常建议改成false让用户从窗口菜单自己打开。2.2 CSInterface.js与ExtendScript桥接PS与页面的通信管道CEP里UIHTML页面与PS的通信主要依赖于CSInterface这个JS库。它封装了与宿主应用的通信协议evalScript()方法可以在页面中执行PS的ExtendScript代码。// index.js const csInterface new CSInterface(); // 执行一段PS脚本 csInterface.evalScript(app.activeDocument.name, (result) { console.log(当前文档名称:, result); });这个是CEP插件最基础的用法。但我们要做OCR必须要把图像像素数据从PS里导出来。最直接的思路通过ExtendScript调用文档的存储副本功能把当前选区或整个画布保存成PNG/JPG临时文件然后Node端读取这个图片交给OCR引擎识别。2.3 Node端配置与第三方库支持CEP的隐藏能力CEP的manifest.xml里可以通过CEFCommandLine配置Node.js的启动参数也可以把ScriptPath指向一个Node环境下的JS文件。但更常用的方式是在页面HTML里直接引入Node模块因为CEP的渲染进程本身就有Node能力前提是manifest里开启Node.js。在manifest.xml中你需要给DispatchInfo的Resources增加一行Resources MainPath./client/index.html/MainPath ScriptPath./client/js/index.js/ScriptPath CEFCommandLine Parameter--enable-nodejs/Parameter /CEFCommandLine /Resources开启后在client/js/index.js中就能使用require(fs)、require(path)等Node原生模块了再配合npm包就能在CEP内部实现复杂逻辑。3. 工具选型OCR引擎怎么选才省心做OCR插件最关键也最容易翻车的是选错识别引擎。我先后试过Tesseract、PaddleOCR、以及几款云端API。如果你也是做本地插件的我建议是优先本地化识别尽量避免把用户图片传到外部API。这不是技术洁癖而是很多设计项目涉及未公开稿、客户隐私你不能默认用户愿意把图片传给别人。3.1 Tesseract与PaddleOCR对比不只是识别率的问题引擎识别率中文打包体积内存占用配置难度许可证Tesseract 5中等中文字库单独下载较小几十MB低低Apache 2.0PaddleOCR高中文印刷体和部分手写体不错较大几百MB以上高高Apache 2.0云API高无无低依赖外部服务Tesseract的优点C底层PHP/Node/Python都有绑定模型文件可控在笔记本上也能跑得动。缺点中文长文识别率不如PaddleOCR尤其在带背景干扰的UI截图、艺术字上错字率会明显上升。PaddleOCR的优点百度开源中文场景优化得很到位版面分析、表格识别都有现成模块。缺点Python生态CEP里要集成需要起一个本地Python服务HTTP或者用PaddleOCR的C预测库自己封装整体复杂度直接上一个台阶。我的选择是Tesseract通过tesseract.js在Node端调用原因很现实开发成本可控、跨平台省心、社区案例多。如果你对识别率有更高要求且愿意多花时间做本地服务PaddleOCR的可定制空间更大。3.2 tesseract.js在CEP里的落地方式纯JS方案tesseract.js是Tesseract的JavaScript封装可以在浏览器环境和Node环境运行底层用WebAssembly加载训练模型。CEP的Chromium内核支持WebAssembly所以直接用tesseract.js就绕开了外部命令行依赖也不需要在系统里单独装Tesseract对插件分发非常友好。# 在client目录下安装 npm init -y npm install tesseract.js安装后在index.js里引入const Tesseract require(tesseract.js);注意tesseract.js第一次识别会下载语言包比如中英文的chi_simeng网速不好时可能卡很久。建议把语言包文件放在插件本地目录通过langPath参数指定本地路径。3.3 语言包与路径配置离线环境的关键一步tesseract.js默认从CDN下载训练数据这在插件里不可控。我踩过一次用户现场没外网识别一直白屏。后来的做法是把chi_sim.traineddata.gz和eng.traineddata.gz下载后放在插件的client/lang-data目录下用如下方式加载const Tesseract require(tesseract.js); const path require(path); const worker await Tesseract.createWorker({ langPath: path.join(__dirname, lang-data), logger: m console.log(m) }); await worker.loadLanguage(chi_simeng); await worker.initialize(chi_simeng);这样插件本地化后完全不依赖外网识别速度也更快。4. 获取图像数据ExtendScript端的关键脚本OCR的第一步是把PS里的图像“喂”给识别引擎。这里我提供两个方案选区导出PNG和整图导出临时文件。4.1 选区导出与整图导出的ExtendScript实现选“文件 存储副本”这种操作在PS界面里点很简单但用ExtendScript自动执行时需要先把文档复制一份再用saveAs保存为PNG。关键脚本如下// 导出当前选区或整图为临时PNG function exportCurrentDocumentToPNG(filePath) { var doc app.activeDocument; // 如果存在选区先复制到新文档 if (doc.selection doc.selection.bounds) { doc.selection.copy(); var newDoc app.documents.add(); newDoc.paste(); // 把新文档保存后关闭 newDoc.saveAs(new File(filePath), new PNGSaveOptions(), true, Extension.LOWERCASE); newDoc.close(SaveOptions.DONOTSAVECHANGES); } else { // 无选区就保存原文档副本 var tempDoc doc.duplicate(); tempDoc.saveAs(new File(filePath), new PNGSaveOptions(), true, Extension.LOWERCASE); tempDoc.close(SaveOptions.DONOTSAVECHANGES); } return filePath; }这个函数有几个细节值得注意复制文档再保存能避免用户原文档被改动。如果在原文档上直接saveAs会把原始文件覆盖或弹窗提示体验很差。设置PNG保存选项时PNGSaveOptions可以设置compression但不需要设置透明通道因为OCR识别时白底背景更稳。临时文件路径建议放在系统临时目录。比如app.getTempPath()或者通过Node的os.tmpdir()获取识别完成后立即删除。4.2 通过CSInterface传参让Node端拿到图片路径ExtendScript负责保存图片Node端负责读取并识别。中间通过CSInterface.evalScript()把路径传回来。// 在页面JS中调用PS脚本 function captureSelectionAndOCR() { const script ${exportCurrentDocumentToPNG.toString()} var result exportCurrentDocumentToPNG(${tempPath}); result; ; csInterface.evalScript(script, async (path) { console.log(临时图片已保存到:, path); const text await recognizeImage(path); renderResult(text); }); }这里我用的字符串拼接方式把函数体带到PS中执行。特别注意ExtendScript的字符串里不要包含模板字符串因为CEF里的JS和ExtendScript的语法版本不同许多ES6语法在ExtendScript里不支持函数最稳妥的写法还是原生ES5。5. 实操过程与核心环节实现从截取到识别的完整流程现在进入整个插件最核心的流程实现。这一节你跟着敲能看到一个能跑的OCR识别功能从0到1。5.1 搭建插件UI识别面板的交互设计CEP的UI就是普通HTML页面但有几个PS风格设计要点宽度控制在300~360px太高会挤压PS主界面。按钮状态要即时反馈识别中的loading动画必须有因为大图识别经常要等好几秒。结果展示区域支持滚动识别出的长文可能超出面板高度。我的面板布局如下!DOCTYPE html html head meta charsetutf-8 titleOCR文字识别/title link relstylesheet hrefcss/style.css /head body div classcontainer h3OCR文字识别/h3 div classactions button idbtnCapture识别当前选区/button button idbtnCaptureAll识别整张画布/button /div div classoptions label识别语言/label select idlangSelect option valuechi_simeng中文英文/option option valueeng英文/option option valuechi_sim中文/option /select labelinput typecheckbox idpreprocessCheck checked 启用图像预处理/label /div div idstatus classstatus就绪/div textarea idresult rows15 readonly placeholder识别出的文字会显示在这里.../textarea div classactions button idbtnCopy复制文本/button button idbtnCreateLayer生成文字图层/button /div /div script srcjs/CSInterface.js/script script srcjs/index.js/script /body /html样式文件我就不全贴了核心就一点保持和PS面板一致的深色或浅色主题。用-webkit-app-region: drag设置拖动区域的时候要小心按钮区域不要设置成可拖拽。5.2 图像预处理提高识别率的关键环节Tesseract对干净图像识别率很高但实际截图往往有阴影、彩色背景、杂点。我加了一个预处理开关用到的技术不复杂但效果显著转为灰度图去掉颜色信息减少干扰。二值化把图片变成纯黑白的让文字更突出。放大图像如果图像分辨率太低先用canvas放大到200%识别率会有肉眼可见的提升。在页面JS里加载图片后用Canvas做预处理function preprocessImage(imagePath) { return new Promise((resolve) { const img new Image(); img.onload () { const canvas document.createElement(canvas); // 如果图片比较小放大两倍再识别 if (img.width 800) { canvas.width img.width * 2; canvas.height img.height * 2; } else { canvas.width img.width; canvas.height img.height; } const ctx canvas.getContext(2d); ctx.drawImage(img, 0, 0, canvas.width, canvas.height); const imageData ctx.getImageData(0, 0, canvas.width, canvas.height); const data imageData.data; // 灰度化二值化 for (let i 0; i data.length; i 4) { const gray data[i] * 0.299 data[i 1] * 0.587 data[i 2] * 0.114; const binary gray 128 ? 255 : 0; data[i] binary; data[i 1] binary; data[i 2] binary; } ctx.putImageData(imageData, 0, 0); canvas.toBlob((blob) { const url URL.createObjectURL(blob); resolve(url); }, image/png); }; img.onerror () resolve(imagePath); img.src imagePath; }); }这段代码的核心逻辑是先用亮度公式0.299R 0.587G 0.114B计算灰度值再用128作为二值化阈值。这个阈值不是对每张图都最优但对绝大多数截图、扫描件都适用。如果想要更智能可以写自适应阈值算法但在插件场景里固定的128能让效果稳定可预期避免用户参数调来调去反而调不明白。5.3 调用Tesseract识别完整代码演示预处理拿到的Canvas图片可以直接转成Blob或File传给Tesseract。async function recognizeImage(imageSource) { const worker await Tesseract.createWorker({ langPath: path.join(__dirname, lang-data), logger: progress { const pct Math.floor(progress.progress * 100); document.getElementById(status).textContent 识别中... ${pct}%; } }); const langSelect document.getElementById(langSelect); await worker.loadLanguage(langSelect.value); await worker.initialize(langSelect.value); const { data } await worker.recognize(imageSource); await worker.terminate(); return data.text; }Tesseract返回的data对象里除了text还有words、lines、paragraphs等结构化结果。如果你要生成带位置的文字图层这些结构就很有用——比如我可以把每个识别出来的词块坐标信息拿回来在PS里按原位置生成文本框。5.4 一键生成文字图层把OCR结果写回PS识别完成后很多用户不只是想复制文本而是想直接在PS里把这个文字重新变成一个可编辑的图层。这需要用到ExtendScript的TextFrameAPI。function createTextLayer(text, position) { var doc app.activeDocument; var artLayer doc.artLayers.add(); artLayer.kind LayerKind.TEXT; var textItem artLayer.textItem; textItem.contents text; textItem.position position; // 数组 [x, y] textItem.size 24; // 字号 textItem.font ArialMT; }这里面有个容易踩的坑识别出来的文字里如果含有换行符直接设置textItem.contents可能会被Tesseract的段落信息搞乱排版。我的做法是识别结果的每一行对应一个独立的文字图层这样后续还可以单独调整位置。function createTextLayersFromLines(lines) { var doc app.activeDocument; var y 100; for (var i 0; i lines.length; i) { var artLayer doc.artLayers.add(); artLayer.kind LayerKind.TEXT; var textItem artLayer.textItem; textItem.contents lines[i]; textItem.position [50, y]; textItem.size 20; y 36; } }注意ExtendScript的对象构造方式和JS略有差异不要用new TextFrame()这种写法所有对象都是从文档里的artLayers.add()创建出来的。6. 常见问题与排查技巧实录这里集中记录我实际开发过程中遇到的一堆问题很多都非常典型直接给排查思路。6.1 CEP面板打不开或菜单不出现这是新手最容易遇到的。排查顺序如下确认manifest.xml的Id唯一且不重复。有些PS插件之间Id冲突会导致新插件加载失败。确认PS以管理员权限运行win10/11的UAC权限会导致插件目录写入失败。检查CSXS目录位置。Windows下是C:\Users\你的用户名\AppData\Roaming\Adobe\CEP\extensions\com.example.ocrplugin\Mac下是/Library/Application Support/Adobe/CEP/extensions/。路径不对绝对不显示。开调试日志。在Windows注册表HKEY_CURRENT_USER\Software\Adobe\CSXS.9下新建LogLevel1重启PS查看日志输出。6.2 tesseract.js识别时页面崩溃或Worker加载失败CEP的Chromium版本比Chrome落后不少新版tesseract.js可能需要更新的WebAssembly特性。解决方案锁版本用3.x。比如npm install tesseract.js3.0.4另外CEP页面里如果开了Node环境加载Worker时可能会碰到路径解析问题。建议把workerPath和corePath显式设置成本地路径const worker await Tesseract.createWorker({ workerPath: path.join(__dirname, libs, worker-script.js), corePath: path.join(__dirname, libs, tesseract-core-simd.wasm.js), langPath: path.join(__dirname, lang-data) });6.3 识别率低中文名字、艺术字、背景复杂图片Tesseract对中文的识别率确实一般尤其遇到艺术字时几乎全军覆没。优化经验预处理里增加对比度增强先用ctx.filter contrast(1.5)拉高对比度再灰度化。文字区域切割如果识别整张大图效果差先让用户框选一个局部区域识别效果远好于全图。白名单限制如果你知道图片里只有数字或字母可以设置worker.setParameters({ tessedit_char_whitelist: 0123456789 })减少候选字符提高正确率。6.4 PS无选区时“复制”命令报错用doc.selection.copy()前必须先判断是否是一个有效选区。doc.selection可能是空对象判断它的bounds是否为null或长度是否为0。更稳妥的做法是直接用doc.selection.contract(0)再判断错误码或者是try-catch包住try { doc.selection.copy(); } catch (e) { // 无选区或选区无效 alert(请先用矩形选框工具选中要识别的区域。); return; }6.5 临时文件堆积导致磁盘占用每次识别都会生成一个PNG文件放到temp目录识别完一定要删除const fs require(fs); // 识别完成后 fs.unlink(tempPath, (err) { if (err) console.error(临时文件删除失败:, err); });我还做了兜底插件每次启动时扫描temp目录下所有带特定前缀的文件比如ocr_tmp_超过3天的强制删除。6.6 不同PS版本的命令兼容性问题在PS 2021之后某些老的ActionManager代码可能会有兼容性调整。我的原则是能用DOM API的尽量用DOM API比如artLayers.add()、PNGSaveOptions这些稳定性远高于ActionManager手写记录。如果确实需要执行命令历史记录要用app.activeDocument.selection.copy()这类层面上的、被PS官方文档定义过的方法。7. 打包与分发把插件分享给同事插件开发完下一步就是打成安装包分发。CEP插件其实不太需要“安装”就是一个文件夹放进extensions目录就行。但为了体验好可以做个一键安装脚本。7.1 Windows下的一键安装批处理写一个install.bat自动创建目录并复制文件echo off set EXT_DIR%APPDATA%\Adobe\CEP\extensions if not exist %EXT_DIR% mkdir %EXT_DIR% set PLUGIN_DIR%EXT_DIR%\com.example.ocrplugin if exist %PLUGIN_DIR% rmdir /s /q %PLUGIN_DIR% mkdir %PLUGIN_DIR% xcopy /s /e /i %~dp0dist\* %PLUGIN_DIR%\ echo 插件安装完成请重启Photoshop。 pause7.2 签名与安全提示CEP插件分发到别人电脑上时如果PS开启了“允许扩展访问网络”相关的安全选项可能会拦截。2022年后的PS版本对未经签名的插件有额外限制但一般来说放在extensions目录下的本地插件不会被拦截除非用户手动开启了“受保护模式”。如果你的插件要通过插件商店分发那就要走Adobe的签名流程个人开发者成本较高目前多数插件还是以GitHub分发为主。8. 后续扩展思路这个插件目前已经把“选区截图→预处理→OCR识别→输出文本或生成文字图层”跑通了作为工具已经能实打实提升效率。但站在长期维护的角度我认为还有几个方向可以继续做批量识别遍历当前打开的多个文档自动识别每张图的文字汇总输出为TXT/CSV。对处理大量截稿图、素材图非常有用。保存识别配置把语言、预处理开关、字号、字体等参数持久化到本地配置文件不同项目可以保存不同预设。集成在线翻译识别结果直接调翻译API把英文截图转成中文文本相当于一个“看图翻译”工具。输出带坐标的JSON在生成文字图层前先输出结构化的文本块坐标信息这样可以在PS里精确重建原版排版。我在实际使用中最满意的一点是整个识别过程完全在本地完成没有网络请求不会把客户的设计稿传到第三方服务器。对设计师来说这是个很让人安心的特性。后续我也想把PaddleOCR的离线模型集成进来把中文长文的识别准确率再往上提一档到时候再把前后对比发出来给大家参考。