一、为什么不用现成的 PSD 转 HTML 工具?
社区里其实已经有不少 PSD 转代码方案,但落到运营活动、H5、长图详情页这类需要“像素级还原”的需求上,普遍有三个让人头疼的问题。
① 文字字号不准
PSD 里的 FontSize=17.5,浏览器一渲染,又小又模糊。问题往往出在忽略了图层里的 transform.scale 参数。
② 全是 position: absolute
几百个图层全部用绝对定位,CSS 体积爆炸不说,后期维护起来简直是一场灾难。
③ 组级效果丢失
圆角矩形加个 8px 外描边,或者文字描边加投影,到了浏览器里,要么被裁切,要么直接糊掉。
我们拿几个实际项目的 PSD 做了对比测试,包括“南瓜大作战 H5”、“总决赛-折叠 H5”、“兑奖 H5”。结果是:
传统切图工作流:从设计稿到可运行 HTML,平均需要 4~6 小时。
psd2code 自动转换:从设计稿到可运行 HTML,平均只需 20 秒。
这背后对应着 psd2code 的四大核心方向,也正好是本文接下来要展开的四个章节:
- PSD 解析:借助 psd-tools,但对它的缺陷做了深度修补。
- 资源提取与优化:像素去重、智能命名、合成背景图。
- 布局优化:用聚类算法识别行列和网格,智能重写成 Flex。
- 多 Target 可插拔:同一份中间表示,一键产出 HTML、React、Vue 三种工程。
二、整体架构:编译器式分层
psd2code 借鉴了编译器的经典三段式设计:“前端解析 + IR + 后端代码生成”。
核心抽象其实就几个概念:
- IR (Intermediate Representation):用 pydantic BaseModel 严格定义的中间表示,自带校验。它是 core 与 targets 之间的契约——任何 target 都从 IR 出发,不直接读取 PSD。
- PipelineContext:贯穿所有 Stage 的全局上下文,承载 PSD、IR、配置、产物路径、target 中间产物等。
- Stage:单一职责的处理步骤,输入和输出都是 PipelineContext。
- Target:一个产物对应一个 Target 子类,通过 @register("html") 注册到全局注册表。
这个分层的直接好处是:HTML target 每次能力升级,React 和 Vue target 会自动受益——因为它们只是在 HTML 产物之上做二次加工。
三、Skill 使用方式
psd2code 同时也是一个 CodeBuddy Skill。在对话框里直接说“帮我把这个 PSD 转成 HTML”,就会自动触发。当然,也可以脱离 CodeBuddy 单独跑命令行。
3.1 在 CodeBuddy 中调用(推荐)
只要项目里有相应的 skill 目录,触发词就能让 CodeBuddy 自动加载。它会自动选择合适的 target、定位 PSD 文件、执行转换,然后把产物路径反馈给你。
3.2 命令行直接运行
默认 target 是 html,同时产出 absolute 原版和 Flex 优化版。也可以显式指定 target:
# 默认 target = html
python3 .codebuddy/skills/psd2code/psd_to_code.py /path/to/file.psd
# 显式指定 target
python3 .codebuddy/skills/psd2code/psd_to_code.py /path/to/file.psd --target html
python3 .codebuddy/skills/psd2code/psd_to_code.py /path/to/file.psd --target react
python3 .codebuddy/skills/psd2code/psd_to_code.py /path/to/file.psd --target vue
3.3 常用参数
| 参数 | 默认 | 说明 |
|---|---|---|
| --target {html,react,vue} | html | 选择产物形态 |
| --css-style {compact,expanded} | compact | 优化版 CSS 输出风格:compact 接近手写,expanded 全展开并附上 PSD 坐标溯源注释 |
| --no-css-pretty | 关闭 | 关闭 CSS 美化,回到字母序机械渲染,常用于 CI 基线对比 |
举个例子,排查某个元素位置不对时,可以开 expanded 模式看坐标溯源注释。跑 React 产物时,可以直接进入目录启动开发服务器。
3.4 产物目录速查
每个 target 的产物都会组织在 output/ 目录下,结构清晰。以 html 为例,包含 index.html(原始 absolute 版)、index_optimized.html(Flex 优化版)、style.css、main.js、metadata.json、layer_map.json、_naming_report.md 以及 images/ 目录。
3.5 排查与定位三件套
跑完后如果发现某处不对,优先看这三个文件:
- _naming_report.md:CSS 类名为什么是这个?是哪个层级给出的?
- layer_map.json:某个 CSS 类对应 PSD 里的哪个图层?
- 对比 index.html 和 index_optimized.html:absolute 原版是“地面真相”,优化版有偏移基本就是布局优化器哪一步过激了。
3.6 系统依赖
运行环境需要 Python 3.10+,以及 psd-tools、Pillow、numpy、beautifulsoup4、pydantic、pypinyin 这些库。
四、PSD 解析:踩过 psd-tools 的那些坑
psd-tools 是 Python 生态里最成熟的 PSD 解析库,但它只实现了最常用的渲染器,对一些常见场景要么出错、要么丢失。我们的做法是:能修的源码级修,不能修的就绕开走手动栅格化。
3.1 文字 transform.scale 修正
PSD 文本图层的 engine_dict 里 FontSize 是原始字号,但 Photoshop 实际渲染时会用 layer.transform 矩阵做缩放。如果不处理 transform.scale,浏览器渲染出来的文字会缩得像花生米一样小。另一个容易踩的坑是 ParagraphSheet 的路径问题,老代码写错路径会导致所有文字永远左对齐。这两处我们在核心模块里做了重新解析。
3.2 浏览器字宽差异:纵向 + 横向双兜底
设计师常用思源黑体或造字工房,浏览器渲染时却用 PingFang 或 Arial。相同字号下,浏览器中文会比 PSD 更宽,导致单行文本被挤成两行,或者按钮内的文字撑出边界。
我们设计了双兜底策略:纵向兜底确保字号不超过 bbox 高度的 0.85,横向兜底则针对纯中文短标题,按字数和宽度倒推上限。效果验证下来,“主赛区”和“预测四强”这类文本都能正确单行显示。
3.3 Shape + 图层样式描边:psd-tools 的致命 bug
一个看似普通的 PSD shape 图层,用 psd-tools 直接渲染会得到整片纯色——填充色完全丢失。排查后发现是两个问题叠加导致的:一是 layer.topil() 对 shape 返回 None,降级到 composite() 后把整块区域错误涂成描边色;二是描边检测模块在 alpha 紧贴画布无 padding 时检测不到边缘,描边色铺满整张图。
绕开方案是跳过 composite(),直接读取填充色和几何信息用 PIL 合成基础图,然后在调用描边检测前给 alpha 加 padding,渲染完再裁回。实测兑奖活动卡片的浅黄背景加绿色描边能完美还原,无需人工补图。
3.4 图层样式的两层 enabled 开关
Photoshop 的图层样式面板有两个开关:整体开关和单项开关。整体开关关闭时,所有效果都不渲染,哪怕单项 enabled=True。psd-tools 不会替你 AND 这两个标志,早期代码多处只看单项开关,导致“PS 中没效果的文本”被当成“有外发光”处理,错误地栅格化成图片。修复后用统一 helper 做判断,全工程 8 处调用点全部切换,解决了带 fx 残留的昵称文本被错误降级的问题。
3.5 组级效果溢出:手动渲染 + composite 混合
Photoshop 的图层效果在组级别有个隐蔽特性:效果会沿组的整体边界裁切。psd-tools 的 composite() 能在组内正确复现这一行为,但只在组的 bbox 内有效。当圆角矩形有外描边时,描边会溢出组的 bbox 被裁掉。我们试过各种“绕过”方法——父级 composite、根级 composite、给超大 viewport——都是徒劳。
最终的解法是手动栅格化加 composite 混合:先用扩展画布手动逐层渲染,拿到完整溢出像素;再用 group.composite 拿到组 bbox 内的原生高质量内容;最后把 composite 结果覆盖到手动渲染结果的内部区域。实测 Alpha 差异几乎为零。
3.6 总结:PSD 解析的修复清单
| 问题 | 表现 | 做法 |
|---|---|---|
| transform.scale 未应用 | 文本被缩成花生米 | 读取 transform,FontSize 乘以它 |
| ParagraphSheet 路径错误 | 所有文字左对齐 | 走正确路径重新解析 |
| shape + 图层样式整片涂描边色 | 兑奖卡片全变绿 | 手动栅格化填充,给 alpha 加 padding 再描边 |
| 两层 enabled 未 AND | 无效果文本被错误降级 | 统一 helper 判断 |
| 组级效果溢出被裁 | 外描边断掉 | 混合渲染:手动扩展 + composite 覆盖 |
五、资源提取与优化
psd2code 会对每个图层做一次决策:是切图,还是保留文字,还是用 CSS 还原 shape,或者吸收为父容器的背景,甚至合并成单张图片。
5.1 智能去重:基于内容哈希而非图层名
活动页里的星星、糖果、装饰点这类小图经常被设计师复用几十次。每次都单独切图是极大的浪费。我们采用基于内容 MD5 的去重策略:相同内容的图片只导出一份。南瓜大作战 H5 实测,239 个 image 图层最终只导出了 87 张 PNG,去重率 63.6%。
5.2 语义化文件名:标签 + 内容哈希
旧的拼音加自增序号方案有两个问题:HTML 里不可读,每次运行序号都在跳,git diff 噪声极大。新方案采用语义标签加内容哈希前 6 位的方式,例如 rounded-a3f012.png、btn-receive-279914.png、bg-main-4e8c1d.png。语义标签由专门的子包从图层名推断得出,支持三层置信度。PSD 没改,文件名就不变,git diff 和 CDN 缓存两全其美。
5.3 形状层保留矢量:不切图就是最好的优化
圆角矩形、椭圆、纯色矩形这类简单 shape,不切图而是直接翻译成 CSS 几何属性。比如 Rectangle 转 background 加宽高,RoundedRectangle 转 border-radius,Ellipse 转 border-radius: 50%。效果是 CSS 体积下降的同时,文件还能支持视网膜屏无损缩放。
5.4 多张全屏背景的合成
活动页常见模式是组里有 2~3 张全屏背景叠加。如果每张都单独切图,HTML 里会写多个 url 背景,多张 PNG 多次请求,浏览器还要多次合成。psd2code 的做法是在布局优化阶段检测这种多 url 模式,用 PIL 的 alpha_composite 合成为单张 PNG 写回磁盘。南瓜大作战 H5 实测 47 组合并,节省了 45.6 KB。
有个与 CSS 规范相反的坑需要特别注意:background-image 的第一个 url 在视觉最上层,而 PIL 的 alpha_composite 期望底层在前。调用方必须 reverse 列表,否则所有合成图的颜色会上下层叠错。
5.5 图层扁平化:子图合并 + 父容器吸收
更激进的优化是:当一个容器里只有纯 image 子图层时,把容器自身的 background 和所有 image 子按 z 序合成为单张 PNG,删掉所有子 div 及其 CSS 规则,只留容器自己的 background-image。这个操作采用后序遍历加多轮扫描,护栏非常严格,比如子元素必须全部是 image 类型,容器本身不能有 border-radius、box-shadow 等不能“烧进 PNG”的装饰字段。南瓜大作战 H5 实测,这一步搞定了 47 个容器的视觉简化,DOM 节点从约 500 降到了约 280。
六、布局优化(本工具最核心的功能)
直接用 absolute 还原 PSD 没问题,但 200 多个图层全部 position: absolute 是工程灾难。psd2code 的 LayoutOptimizer 能把 absolute 智能重写成 Flex,同时保证视觉零偏移。
6.1 七步流水线全景
整个优化过程是一条七步流水线:从原始 absolute HTML 开始,经过 DOM 重构、图层扁平化、同质兄弟分组,再到 Flex 推断、单子 wrapper 折叠,然后进行 CSS 去冗余和重复元素抽取,最后完成 CSS 美化,输出优化后的 HTML 和 CSS。
6.2 聚类算法:怎么“看懂”一堆 absolute 框
这是整个 LayoutOptimizer 的灵魂。对任意一个容器,我们要把 N 个子图层的 bbox 自动组织成行、列、叠图组的树状结构。
第一步:切行。 从左到右、从上到下遍历子元素,维护一个“当前行”的包络。新元素与包络的纵向重叠率达到一定阈值即为同行,否则新开一行。
第二步:行内切列。 对每一行内部,把切行逻辑换成纵轴和横轴就是切列。递归后得到一棵“行包列 / 列包行”的嵌套树。
第三步:背景层剥离。 一个组里常有“全屏卡片底框加多个浮层”的设计模式。直接聚类会把底框当成占 100% 空间的大元素,严重干扰行列判断。我们在聚类前先剥离满足三种规则之一的背景层,包括完全包含型、主轴覆盖型、双轴主导覆盖型。剥离后的背景层被吸收进父容器的 background-image 列表。
第四步:伪多行装饰堆叠回退。 切出多行后,若所有行都只有一个元素,且相邻行横向覆盖率较高,回退为堆叠——这是“图标加标题上下贴边”这类装饰场景。
第五步:二维网格识别。 当多行多列的元素满足列对齐和跨行对齐时,改成显式的网格结构。南瓜大作战 H5 的“用户信息区”9 个子图层,被正确识别为 2 行 4 列和 3 列组成的网格。
6.3 三道安全闸门:什么时候不该用 Flex
不是所有看起来整齐的容器都该用 Flex。踩过太多坑后,我们总结出三道闸门:互相重叠的装饰簇保持 absolute;存在支配背景层的大底图加浮层卡片保持 absolute;先在子节点中分类出内容部分再做趋势检测,避免装饰打乱排版被误判。
6.4 Flex 应用:非趋势子元素保留 absolute
识别为纵向或横向布局后,趋势元素写成 flex 子项,用 margin 表达间距;非趋势元素(如角标、装饰)保留其 position: absolute 坐标。这里有个极易反复重犯的 bug:容器重构后,子元素的 top/left 是相对父容器的坐标,不需要再减父 top。另外,flex_applier 默认会删除子元素的 position,但如果子本身是 v-stack wrapper 且包含 absolute 子节点,删除 position 会导致内部元素飘到屏幕角落。修复后遇到这种情况会改成 position: relative。
6.5 同质兄弟簇检测:识别“同类卡片”
PSD 设计师经常把 N 个商品卡或道具卡平铺在画布直接子节点,没有用父组包起来。传统聚类只在已有 group 内部做,这种列表会全部走 absolute 路径,开发拿到 HTML 完全看不出它是一个数据列表。
检测规则有五条,包括至少 3 个连续兄弟、class 词根相同、bbox 尺寸近似、满足网格规则、父容器本身不是 flex。识别成功后包成虚拟容器,CSS 用 display: flex 加 flex-wrap: wrap,下游开发可直接写 v-for。
6.6 CSS 去冗余:z-index 精简 + 等价规则合并
原始代码给每个图层无脑塞 z-index,这是合理的像素还原默认值,但优化版完全不需要。精简逻辑是:只有一项时删除,严格递增时全删(DOM 顺序等于 z 序),出现倒挂时才全部保留。等价规则合并则将属性完全相同的多个选择器合并为单条规则。南瓜大作战 H5 实测合并了 209 条。
更进一步,当有三个或以上不同 hash 类但 CSS 完全等价时,合并为单一 base 类,HTML 同步改写。最终你看到的就是干净的语义化结构。
6.7 实战效果(南瓜大作战 H5)
| 指标 | V2 优化器 | 当前版本 |
|---|---|---|
| 元素位置偏移 PSD 原位置 | 94 个元素偏离 5~13px | 0 个元素偏离 |
| CSS 行数 | 4805 | 1499 |
| CSS 块数 | 457 | 约 270 |
| z-index 字段 | 432 | 97 |
| 6×4 任务网格识别 | 每个 cell 独立 absolute | 自动识别行列嵌套 |
6.8 算法的天花板与人工边界
再好的算法也有上限。以下场景 psd2code 会“尽力而为,但结果不一定最优”。
① Flex 布局化不充分:设计师图层组织混乱。 典型问题是按钮、图标、装饰全部散乱摆在同一个 PSD 根组,没有任何分组。解决方案是按视觉版块整理 PSD 图层结构,每个版块内部再细分组。分好组之后,95% 的场景都能自动重构为干净的 Flex。
② CSS 不够语义化:图层名用了默认命名。 像“矢量智能对象”“图层 12 拷贝 3”这样的命名,psd2code 只能给出内容哈希名。解决方案是重要的结构性图层给中文或 kebab-case 命名,psd2code 的语义子包能识别按钮、背景、卡片容器等语义,命名整洁后 HTML 就会是一眼能看懂的语义类。
③ 特殊场景需人工干预。 由于只实现了常用渲染器,部分图层导出效果不好时,需要手动栅格化或导出图片。
七、实战演练:把“南瓜大作战 H5”PSD 跑一遍
以 750×6778 的长图活动页为例,一行命令 20 秒就能拿到完整可运行的 HTML。
运行日志里有几个有意思的点:组级效果溢出自动触发了 3 次,全部走混合渲染策略;47 个容器被图层扁平化,原本 105 张 image 合并成 47 张 PNG,节省 45.6 KB;3 组同质兄弟列表被识别并包成了 v-list,可直接写 v-for;叠图组则被正确识别为装饰堆叠,保持 absolute。
打开浏览器,第一屏与 PSD 设计稿完全像素对齐,包括标语上的描边发光、用户信息区的圆角、糖果图标的渐变叠加。absolute 原版与 Flex 优化版的对比也很明显:HTML 从 71 KB 降到 52 KB,CSS 从 113 KB 降到 38 KB,可维护性大幅提升。
八、多 Target 可插拔架构
8.1 Target Registry:装饰器注册
注册表的设计非常简洁,通过 @register 装饰器即可将 Target 子类注册到全局注册表。每个 target 是 Target 子类,实现 build_pipeline 方法,返回一个 Pipeline。新增 target(比如小程序)只需继承 Target、实现构建方法,用装饰器注册,无需改动核心代码。
8.2 为什么 React / Vue 都在 HTML 基础上二次加工
业界也有直接从 IR 生成 JSX 的设计,但 psd2code 选择先走一遍 HTML target,再转框架。这样做的好处是:HTML target 的优化免费继承给 React/Vue,任何一次布局优化器升级自动惠及三端;开发者 review 时可以直观对比视觉一致性;React/Vue 的转换就是 DOM 遍历加模板语法替换,逻辑简单,可测试性强。
九、其他你可能在意的细节
- 图片去重按内容 MD5,同一张装饰图只导出一份。
- 语义化类名支持从图层名推断,中文关键词可通过词典映射为英文。
- 所有文本节点自动带 data-i18n-key,可通过 JS 动态替换,为国际化预留。
- 旋转或倾斜的文本自动降级为图片,保证视觉一致。
- 剪贴蒙版按标志识别,合并成父图基底加描边或发光效果。
十、踩过的坑(写给后来者)
如果你打算自己实现 PSD 到代码的工具,以下几个坑可以省你几天时间。transform.scale 不能忘,shape 加图层样式描边必须手动处理,两层 enabled 开关必须 AND 操作,composite 的 viewport 限制导致组级溢出效果必须手动扩展画布,子组必须用 composite 渲染,背景剥离后合并 background-image 要注意顺序,多 url 背景合成时列表需要 reverse,CSS parser 不能用贪婪正则,flex 容器的 envelope 可能越界要加 max(0),v-stack wrapper 的 position 必须保留,background-repeat 不是默认值不能误删。把这些坑都填了,工具才能稳定可靠。
十一、写在最后
psd2code 不是一个“AI 读图猜布局”的玩具——它是一个严格基于 PSD 结构信息的编译器。每一步决策都可解释、可调参、可单测,算法失败点都有明确的 fallback 路径。
再强调一次:算法做的再多效果也是有限的。想要 psd2code 产出高质量代码,有两件事你得做:一是整理 PSD 图层结构,按视觉版块分组;二是整理 PSD 图层命名,关键图层给语义名。做到这两点,运营活动页从设计稿到可上线代码的时间可以从 4~6 小时降到 20 秒。
未来计划方面,HTML、React、Vue 已经上线,小程序 target 的架构预留了扩展点,Tailwind CSS 输出和 Figma 文件支持也在规划中。
