Poster to PSD — 海报转分层 PSD 生成器
把「用户一句话描述的海报」变成「带可编辑图层的 Photoshop PSD」。
分两步,中间留一个关键提示词审阅 checkpoint(用户可控、可改词)。
触发关键词(什么情况下用我)
用户说出下列任意一类话,就应调用本 skill(命中一条即可):
| 类别 | 触发示例 |
|---|---|
| 直接要海报 | 「做一张海报」「生成海报」「设计个海报」「出张促销海报」「帮我做个活动海报」「来个科技感海报」 |
| 要分层 / 可编辑 | 「海报 PSD」「分层 PSD」「可编辑海报」「能在 PS 里改字的海报」「图层分开的海报」「把插画拆成图层」 |
| 参考图驱动 | 「把这张图里的蛋糕换成奶茶」「做类似这样的海报」「参考这张图生成海报」「照这个排版来」「按我发的图排版」 |
| 按题材 | 「中秋/春节/国庆海报」「国风海报」「电商促销海报」「产品宣传图」「品牌视觉」「课程封面」 |
| 转格式 | 「图片转 PSD」「海报转 PSD」 |
判定边界:凡涉及「要一张能在 Photoshop 里逐层移动/修改的海报或宣传图」,都归本 skill。纯对话问答、与海报/PSD/图层无关的需求不触发。
两步工作流
第 1 步:描述 → 完整精准关键提示词(LLM 产出,不调模型)
用户给自然语言,尽量包含:主题/用途、尺寸(如 1920x800)、风格、要写的文字、主要元素。
你产出:
1. 海报设计纲要:画布尺寸、配色、版式分区、图层规划表(每层叫什么、什么效果)。
2. ★ 完整精准的关键提示词清单(用户 2026-09-17 硬性要求,缺一不可,且必须精准):
- ① 全局风格提示词(STYLE_SUFFIX):一条,定义画质(8k/photorealistic)、光线(soft diffused studio lighting)、镜头(shallow DOF / sharp focus)、色彩倾向、构图约束、以及去背约束(isolated on PURE WHITE BACKGROUND, no shadow, no reflection, no text)。所有元素提示词都拼这个后缀,保证风格统一。
- ② 每个素材元素一条专属提示词:主视觉、每个装饰件、背景底图、每个图标,各写一条。每条必须写清:主体是什么、材质、颜色、角度/朝向、在画面中的占比或构图位置、四周留白要求(避免主体贴边导致泛洪去背失败)。
- ③ 文字设计感说明:每层文字用什么字体、什么特效(描边/阴影/渐变/底衬),不能只写"放个标题"。
3. 文案占位:标题/副标题/印章文字。
4. 落盘:写成 关键提示词.md 随交付物一起给出,元素与提示词一一对应,方便逐条改词。
> 这一步是给用户审阅、修改的「关键提示词」。用户改完回「就按这个生成 / go」进入第 2 步。
> ★ 绝不跳过:没有先给出提示词清单就直接生图 = 违反规则。
第 2 步:确认 → 生成 PSD
- 用
scripts/gen_asset.py调 Agnes 生成:主视觉插画(1920x800 等目标尺寸,提示词里写 no text/no words 以免和文字层冲突)+ 各独立元素贴纸(isolated on white background)。 - 贴纸类素材去白底 → 透明 PNG:优先用
scripts/cut_sticker.py(从画布四边泛洪,只去外部白底、保留元素内部的白色图案——桶/瓶/盒类正面常有白色图标,全局阈值的remove_bg.py会把它们掏空;且只模糊 alpha、RGB 用原图,边缘不留白圈)。只有素材内部确实没有白色图案时,才退回scripts/remove_bg.py。务必先量一下去背后的实际像素尺寸(python -c "from PIL import Image;print(Image.open('x.png').size)"),布局时用真尺寸算坐标,别凭感觉。 - 写一份 JSON 规格(见下),用
scripts/assemble_psd.py拼装 PSD:背景渐变层 + 每个元素独立层 + 文字层(默认光栅化;需要 PS 双击改字时加editable:true,见「真文字层」节) + 调色层(blend 模式)。 - 用
scripts/render_preview.py出忠实预览(能正确处理真文字层,mock_preview.py不能)。 - 交付:
xxx.psd+预览.png+ 图层清单说明。
图层完全独立的要求(用户高频诉求,必须满足)
用户常明确要求「文字是文字、每个元素各自独立图层、背景是单独的背景色/渐变、祥云等不能和背景合并、人物单独出来」。落地做法:
- 背景用 gradient 或 color 整层铺满,绝不把任何元素烤进背景。
- 每个元素单独一次 Agnes 生成 → 单独去背 → 单独一层。
- 文字默认光栅化(独立像素层:PS 里能移动/改色/加效果,只是不能双击改字)。用户明确要「在 PS 里双击改字」时才用 editable: true。TySh 真文字层现已按真实 Photoshop 文件逐字节校正(详见文末「真文字层」节);但本机无 PS 可实测,仍建议交付真文字层 + 光栅化兜底两份。
- 交付前用 kind= 逐层打印自检,确认没有任何层是被合并的。
规格 JSON 格式(assemble_psd.py 输入)
{
"width": 1920, "height": 800,
"output": "poster.psd",
"layers": [
{"type":"gradient","name":"背景渐变","angle":"diagonal",
"stops":[["#8f1318",0.0],["#5c0a0f",0.55],["#2a0406",1.0]],"opacity":100},
{"type":"image","src":"moon_cut.png","name":"明月","left":1390,"top":70,"scale":0.30},
{"type":"image","src":"cloud_cut.png","name":"金缎带(文字底衬)","left":0,"top":130,"scale":0.80},
{"type":"image","src":"chang_e_cut.png","name":"嫦娥","left":1360,"top":100,"scale":0.46},
{"type":"color","name":"印章底色","x":70,"y":690,"w":104,"h":104,"radius":18,
"color":"#C0392B","opacity":95},
{"type":"text","editable":true,"text":"中秋","name":"印章文字","font":"simhei.ttf",
"font_ps":"SimHei","font_size":46,"color":"#FFFFFF","anchor":"center","x":122,"y":742},
{"type":"text","editable":true,"text":"中秋节快乐","name":"主标题","font":"STXINGKA.TTF",
"font_ps":"STXingkai","font_size":168,"color":"#7a0c12","anchor":"center","x":610,"y":388},
{"type":"color","name":"调色-暖色叠加","color":"#FFC46B","opacity":12,"blend":"OVERLAY"},
{"type":"color","name":"调色-光晕","color":"#FFF3D4","opacity":8,"blend":"SOFT_LIGHT"}
]
}
图层类型
| type | 用途 | 关键字段 |
|---|---|---|
gradient |
独立背景层:多段渐变,numpy 渲染 | stops:[[hex,pos]...]、angle: vertical/horizontal/diagonal |
image |
图片元素 | cover:true 铺满画布;否则 left/top/scale 精确定位(图层像素就是元素本身) |
text + editable:true |
真 Photoshop 文字层(TySh),PS 里可双击改字;已按真实 PS 文件逐字节校正,但本机无 PS 实测,建议配一份光栅化兜底 | font(PIL 测量用) + font_ps(PS 字体 PostScript 名) + font_size + color + anchor + x/y(锚点) |
text(默认,不写 editable) |
光栅化文字层(推荐,兼容性最好):不能改字,但能移动/改色/加效果 | 同上,可加 stroke/stroke_width/shadow |
color |
整层调色(配 blend);或给 x/y/w/h/radius 当圆角色块(印章底) |
color、opacity、blend |
seal |
红底白字印章(光栅化) | text、x、y、size、color |
anchor: center / left / right(决定 x/y 指哪一点)。
运行命令(跨平台 · 零手动装依赖 · 无硬编码路径)
> ✅ 依赖通过 scripts/run.py 自动安装,功能与原版一致。本 skill 自带引导器:
> 首次运行会自动在 skill 目录内建隔离环境 .venv 并装好 Pillow/psd-tools/requests
> (若存在 wheels/ 优先离线安装;本 SkillHub 发布包为控制体积不含离线大包,自动退回 PyPI 安装同一套 requirements.txt 锁定依赖),之后每次直接跑。
> 你只要设置一个变量 S(脚本目录),其余全自动。素材与 PSD 放在你自己的任意工作目录。
# —— 1) 设置脚本目录(按本 skill 实际安装位置改这一行)——
# S = <skill 根目录>/scripts
S="$HOME/.config/mimocode/skills/poster-to-psd/scripts" # MiMo 全局 / 类 Unix
# S="$USERPROFILE\.config\mimocode\skills\poster-to-psd\scripts" # Windows PowerShell
# SkillHub 或其它宿主安装后:把 S 指到解压出的 poster-to-psd/scripts 即可
# —— 2) 进入你的【工作目录】(素材与 PSD 都放这里,任意盘均可)——
cd /path/to/your/project # 例:D:/hello_autumn_poster
# —— 3) 干活:所有命令统一走 run.py(首次自动建环境+装依赖,之后秒跑)——
"$(command -v python3 || command -v python)" "$S/run.py" gen_asset.py --prompt "海报主视觉, ..." --size 1920x800 --out ./assets/hero.png
"$(command -v python3 || command -v python)" "$S/run.py" gen_asset.py --prompt "..., isolated on white background, sticker style" --size 1024x1024 --out ./assets/rabbit.png
# 2a) 白底贴纸 → 去白底(阈值从 236 起试,feather 0.8~1.5)
"$(command -v python3 || command -v python)" "$S/run.py" remove_bg.py ./assets/rabbit.png ./assets/rabbit_cut.png --thr 240 --feather 1.5
# 2b) ★发光元素:同色深底生成 + 亮度色键(白底会把光晕切成硬白圈)
"$(command -v python3 || command -v python)" "$S/run.py" gen_asset.py --prompt "..., solid deep dark red background #7a1019 filling the entire frame, no white anywhere" --out ./assets/moon.png
"$(command -v python3 || command -v python)" "$S/run.py" key_dark_bg.py ./assets/moon.png ./assets/moon_cut.png --span 100 --gamma 0.9
# 3) 拼装 PSD(spec.json 用你工作目录下的路径)
"$(command -v python3 || command -v python)" "$S/run.py" assemble_psd.py ./spec.json
# 4) 忠实预览(必须用这个,mock_preview 无法渲染真文字层)
"$(command -v python3 || command -v python)" "$S/run.py" render_preview.py ./poster.psd ./spec.json ./预览.png
# 5) 结构自检
"$(command -v python3 || command -v python)" "$S/run.py" assemble_psd.py --selftest ./poster.psd
# 6) 边缘质检:白底素材去白边 / 深底素材去图层描边 / 逐层报告
"$(command -v python3 || command -v python)" "$S/run.py" defringe.py ./assets/frame_cut.png ./assets/frame_cut.png --lmin 190 --sat 22
"$(command -v python3 || command -v python)" "$S/run.py" qa_edges.py ./assets/frame_cut.png
> 也可显式一次性装好依赖(等价于首次自动安装,适合先验证环境):
> "$(command -v python3 || command -v python)" "$S/bootstrap.py" —— 仅建 .venv 并打印其 python 路径。
> 注意:.venv 是运行时生成的,不要把它打进发布包(见「发布必改」第 11 节)。
> 提示:所有 scripts/*.py 都用 __file__ 自定位,内部依赖(agnes_api.py、mock_preview.py、
> make_text_layer.py、模板 assets/engine_template.bin)全部随包分发且按相对路径解析,
> 不依赖其它 skill、不依赖特定盘符。直接 cd 进 scripts/ 目录运行同目录脚本也一样可用。
★ 发光元素抠图:白底必败,深底色键才对
这是本次最大的坑。 明月、灯笼这类带发光光晕的元素,用「白底生成 + remove_bg.py」必然留下一圈硬白边:光晕是从亮到白的连续渐变,白色阈值一截就是断崖。
正确做法(已验证):
1. 提示词里把背景指定成和海报主色一致的颜色,例如 solid deep dark red background #7a1019 filling the entire frame, no white anywhere, no text。
2. 用 key_dark_bg.py 做亮度色键:alpha = clip((亮度 − 背景亮度) / span)。光晕的渐变被完整保留成平滑半透明,叠回深红底上无缝。
3. span 必须先量再定:测背景亮度 L_bg 和主体最暗处亮度,让「主体最暗处刚好 ≈ 全不透明」。跨度定小了会把背景也带上 alpha(曾出现 98% 像素被保留);定大了主体会变半透明。本次实测:明月(背景 L=20,主体 L=190)用 span=100;祥云(L=43 → 198)用 span=60;灯笼本身偏暗红(L=37 → 114)只能 span=45。
4. 加反预乘(rgb = (pixel − bg*(1−a)) / a)去掉半透明像素里混入的背景色,否则边缘带红边。
5. 验证指标:四角区域平均 alpha 应 < 10/255,且半透明像素占比要小(祥云 0.3%、灯笼 4.8% 属正常)。
6. --gamma 是控制光晕扩散的旋钮:默认 0.85~0.9(<1)会放大弱光晕——大面积 L 40~60 的淡光晕被一起留下,明月实测保留 86.4% 像素、整幅泛白。想收紧就设 --gamma 1.5(>1 抑制弱值):同一张明月降到 49.2%、四角 alpha=0.00,光晕收成干净的月晕。先按默认跑一遍看覆盖率,>60% 就调大 gamma。
顺带的好处:深底生成的元素边缘也顺带更干净——白底贴纸风格会自带一圈白色描边(sticker outline)+ 投影,叠在深红底上很明显;深底生成时直接在提示词里加 no white border, no sticker outline, no drop shadow,出来的元素边缘直接是干净的。
★ 深色主体(毛笔字/墨迹)去白底:反向亮度键 key_light_bg.py
上面两种键控都假定主体比背景亮(亮主体 + 暗底)。毛笔书法、墨迹、深色剪影是反过来的:主体深、背景白。
- ❌ 用 key_dark_bg.py(亮主体)→ 主体全被切掉。
- ❌ 用 remove_bg.py 全局阈值 → 把枯笔、飞白、淡墨的中间过渡层一刀切成硬边,失去毛笔的柔边质感。
- ✅ key_light_bg.py(本次新增,已验证):alpha = clip((背景亮度 − 像素亮度) / span)。墨越浓 → alpha 越接近 1;白底 → 0;中间灰(飞白)→ 平滑半透明。
bash
python scripts/key_light_bg.py in.png out.png --span 200 --floor 18
# span 先量:背景亮度 L_bg(白底≈250)与最浓墨处亮度,让最浓处刚好≈全不透明
同样带反预乘(去白边)。实测毛笔「秋分」两字:内部近白 0.0%、柔边 2.0%,飞白完整保留。
- 判断法:主体亮度中位数 < 背景亮度 → 走 key_light_bg.py;反之走 key_dark_bg.py。
★ AI 把主体抠好了、却自作主张加了一圈「彩色光晕」
AI 生成的白色/浅色主体(白兔、白花、白云)常在主体外围补一圈暖色光晕(如奶橙色),单独看很美,但叠到海报上会盖住后面的元素(实测:白兔的橙晕把背后的虚焦大柿子糊掉)。
- 判据:主体本身近中性(R≈G≈B,spread = max−min 小),光晕是有彩色(spread 大)。
- 做法:按「饱和度」给 alpha 加权——近中性保留、有彩色滤掉:
python
spread = rgb.max(2) - rgb.min(2)
w = 1.0 - np.clip((spread - 40.0) / 80.0, 0, 1) # spread<40 保留;>120 全滤
alpha = alpha * w
实测白兔橙晕残留从 1% → 0.1%,兔毛(近中性)无损失。
- ⚠️ 别滤过头:若主体本身是有色的(粉兔、黄花),这个权重会连主体一起削 → 只对近中性主体用;主体有色的光晕改用「按半径的径向 alpha 衰减」或直接把光晕区裁剪掉。
★ 复刻「水彩虚化」参考图:AI 出的锐利主体要主动柔化
参考图是水彩/虚焦风格时(例:秋分海报里的虚化白兔、虚焦大柿子),AI 出的主体往往是锐利卡通风格,直接叠上去风格突兀。
- 做法:对素材做 GaussianBlur(10~18) + alpha 乘 0.82~0.9(降不透明度),变成「幽灵状/水彩晕染」质感,与参考图的虚化感对齐。
- 程序化虚焦大色块(参考图里那团虚化柿子/光斑):用 numpy 生成径向渐变椭圆 → GaussianBlur(20~30),核心区保持不透明(rr<0.7→alpha=1)、边缘才柔化,比从头 AI 生成更可控、颜色能精确对齐参考图。
- 核对方法:分区块算 mean / orange% / dark%,与参考图同区块对比。实测秋分中部大柿子区块成品与参考图均色完全一致 [245,173,127]。
★ 商品海报:AI 投影必须用「中性亮色」算法清除
AI 生成产品图(礼盒/实物)几乎一定会附带一块投影,而且提示词里写 no drop shadow 也挡不住(实测重生成仍带)。
- 投影的特征:淡灰、低饱和(max−min < ~24)、亮度中高(L 140~238)。
- 为什么阈值法不行:它是渐变的——remove_bg.py --thr 240 只能削掉外层 240+ 的部分,靠近产品的 200~238 灰带会留下(甚至因恰好 <240 而整块不透明残留);调高 --thr 又可能啃掉产品本身的浅色部位。
- 正确做法:投影是「低饱和中性亮色」,产品是「高饱和暖色」,两者在饱和度上天然分离。直接清零 alpha:
python
spread = rgb.max(2) - rgb.min(2); lum = rgb @ [0.299, 0.587, 0.114]
a[(spread < 24) & (lum > 140)] = 0 # 只杀中性亮像素;金色月饼/奶油内衬(spread 大)安全
实测一步清干净,肉眼与 qa_edges.py 均通过。
- 顺位:remove_bg.py 去白底 → defringe.py 去白边 → 再跑这个中性亮色清零。
- ⚠️ 反例:白/浅色主体千万别用这招。如果主体本身就是白或浅色中性(白玉兔、白瓷盘、奶油蛋糕、云朵),它和「中性亮色投影」在饱和度上无法分离——上面这步会把主体自己一起清零。实测:白玉兔跑完只剩耳朵内粉、手里月饼和一圈描边,身子全没了。
此类素材改用「纯黑底生成 + 深色键」:提示词写 isolated on a solid pure black background filling the entire frame, no shadow, no ground shadow,主体亮、背景黑(亮度天然分离),再 key_dark_bg.py --span 200 --gamma 1.0 --edgefade 24 --floor 8,一次得到边缘干净、主体完整的抠图。判断法:主体亮度中位数是否落在 140~240 且饱和 <24?是 → 走黑底深色键,不要走中性亮色清零。
★ 把一张「扁平插画」重建为分层 PSD(用户说"我要的是图层"时)
用户常先要一张扁平插画(文生图),再补一句「没有图层,我要的是图层」。此时不要试图去抠那张成品图——前景/背景往往是同色系(例:绿叶前景 + 绿色背景),无法分割。正确做法是按图层需要,分别重新生成素材:
- 逐层生成,而不是逐层抠:背景一张、主体一张(白底或黑底贴纸风)、前景元素一张。生成时就指定好画布同比例的尺寸(如竖版用
--size 1024x1536),避免 cover 铺满时被拉伸变形。 - 主体底色要选对:浅色/暖色主体(人像肤色)用纯白底 +
remove_bg.py;若生成出来是"暖奶油渐变底"(很常见),直接抠会啃掉浅色皮肤 → 重新生成为isolated on pure white background, sticker style再抠。 - "谁在谁背后"用图层顺序解决,不用抠遮挡:想让「人脸在绿叶背后」,就把女孩层放下面、叶簇层放上面,叶子自然盖住脸——不需要把被遮住的脸补全。
- 小元素(蚂蚁、蜜蜂、碎石)单独成层:按「中性深色」判据提取 ——
ants = (a>100) & (lum<95) & (spread<48);提取后把原图上该位置用周围颜色重模糊填平(GaussianBlur(6)取色回填),这样隐藏小元素层时底图上不会有黑点孔洞。 - 必做校验:把小元素层与底图层
alpha_composite叠回去,与原图逐像素比,平均色差应 ≈ 0.00;若不为 0 说明遮罩漏了或多了。 - 再写 spec →
assemble_psd.py→render_preview.py。注意:初版前景元素常"位置偏低、没压住主体",预览后要把前景层上移放大,才有"从后面探头"的遮挡感。 - 文字层:TySh 真文字层现已做到与真实 Photoshop 逐字节一致(见下文「真文字层」节的字节级基准),结构上不再有已知缺陷;但本机无法启动 PS 验证是否闪退,所以默认仍是双份交付(真文字层 + 光栅化兜底)。
- ★「人物手持道具」必须让道具独立成层,别指望一次生成就分层:直接写「男生手里拿着某物」,模型几乎必然把道具烤进人物层(实测:写"holding a small book"→ 生成结果人物已抱着一本书,道具无法单独移动)。正确做法:人物层单独生成,提示词里显式写
both hands cupped together in front, holding nothing, empty hands, no book, no object between the hands,得到"双手空托"的姿势层;道具另行生成(发光道具走黑底 +key_dark_bg.py,普通道具走白底 +remove_bg.py)→ 各自裁到 bbox → 在 spec 里用left/top/scale把道具层叠到手掌位置。实测这样人物 + 道具是两张干净独立层。 - 生成尺寸可能被"就近吸附"到受支持的画幅:请求
--size 1024x1280(4:5)实测返回 864×1152(3:4)。所以生成后第一件事是量实际像素(Image.open(f).size),并以实际背景尺寸作为画布尺寸(背景cover才不会拉伸),别拿请求尺寸去算坐标。
边框定位:先量像素再排文字
金边框之类的装饰层,不要凭感觉留边。用 numpy 扫一遍边框素材的 alpha 行分布,算出「双线在哪一行 → 放大到画布后对应 y」,再反推文字安全区。本次实测:边框 1568x672 底部双在线源图第 606-627 行 → 拉伸到 800 高后是 y=721-747,所以所有文字底部必须 ≤ 715。
排文字前先量文字盒:ImageDraw.textbbox() 实测 STXINGKA.TTF 122px "中秋节快乐" = 610x121、simhei.ttf 32px "花好月圆 · 阖家团圆" = 320x31,拿真数做竖轴预算,别估。
★ 白底贴纸去背(cut_sticker.py):内部有白色图案时,别用全局阈值
remove_bg.py 是全局阈值——凡 R/G/B 同时 ≥ thr 的像素统统置透明。
对「内部本身就有白色图案」的素材(垃圾桶正面的回收箭头、警示三角、苹果果肉,
瓶子/盒子上的白色图标或文字牌),它会把内部白色一起掏空,成品背景直接透出来。
正确做法:用 scripts/cut_sticker.py(从画布四边泛洪)
python $S/cut_sticker.py bin.png bin_cut.png --thr 232 --feather 1.0 --erode 2
它一次解决两个坑:
| 坑 | 症状 | 解法 |
|---|---|---|
| 全局阈值掏空内部白色 | 桶正面白色图标变透明,透出背景 | 只泛洪「与画布四边连通的近白」,被深色轮廓包住的内部白色自动豁免 |
| 边缘残留一圈白色描边 | RGBA 整体高斯模糊,把背景白色的 RGB 混进了边缘像素 | ① 保留区先腐蚀 1~2px,切掉混了背景色的外层;② 只模糊 alpha 通道,RGB 一律用原图 |
自检:脚本会打印「外部白底置透明 px | 内部保留白色 px」(后者应远大于 0);
再统计边缘带里「不透明且偏白(亮度>200)」的占比,正常应接近 0%
(残留若偏高,先确认是不是主体自身的浅色高光,例如浅绿桶身的高光条)。
⚠️ 纯白元素不能放在白底生成(会整个消失)
白底贴纸里若元素本身就是纯白(白云、白花、白瓷盘、白纸牌),cut_sticker 会把它的白色
当成「外部白底」一起清掉 —— 元素整个消失(实测:纯白云朵去背后报「内部保留白色 0 px」,
只剩一圈若有若无的边)。判据:素材主体是不是「纯白 + 无描边」?是 → 必须重生成,
提示词给它一个有色填充或描边(如 soft sky-blue cloud with a slightly darker blue outline),
去背后主体才完整、边缘才干净。
⚠️ 主体就是奶白/瓷盘(食品类)去边别用 defringe 直接删 alpha
包子/馒头/白瓷盘装食物这类主体本身亮白,和"白边"判据相同,defringe.py 的"低饱和亮点删 alpha"
会把主体一起删掉。正确做法(本次包子海报实测):
1. 生成用黑底(agnes 黑底 + key_dark_bg.py 键控):白盘主体在黑底上是亮白、边缘只剩半透明光晕;
2. 去光晕用"最近内部实心色填充":scipy distance_transform_edt(~solid, return_indices=True)
取最近盘面色填进半透明/白圈像素,保留奶白主体、只清外圈光晕——绝不用"删 alpha",否则主体被掏空;
弱化白圈可把该带 alpha 乘 0.7~0.75。
3. 裁顶部蒸汽:AI 生图常在食物顶上画上升蒸汽/烟雾,在红/深底上呈灰雾 → 直接裁掉
(本次 im.crop((0,160,W,H)) 最干净);别用"最近色填充"(会和底色不一致、留下可见矩形色块)。
⚠️ AI 没给白底、反而生成「深蓝近黑底」→ 改用 cut_dark.py(2026-09-17 科技海报)
科技/夜景/发光题材里,提示词即使写了 PURE WHITE BACKGROUND,AI 也可能整张生成深蓝近黑底
(本次 globe、icon_smart 两张就中了)。此时 cut_sticker 的四边白底泛洪完全失效——
边缘没有近白像素 → keep 全 True → 整张不透明,出现黑方块。
判据:cut_sticker 打印「外部白底 0 px 置透明」=中了这个坑。
对策:改用 scripts/cut_dark.py——只抠「蓝通道占优且整体偏暗」的像素,明亮霓虹线条(青 #22D3EE / 紫 #A855F7)自动保留:
python $S/cut_dark.py neon.png neon_cut.png --max_thr 72 --feather 1.0 --erode 1
细线条 --erode 别超过 1,否则会把发光线蚀断。
★ 边缘质检三连(去背之后必做)
去背 ≠ 干净。三份脚本,按顺序用:
# A) 白底贴纸素材:压掉边缘的中性白边(保留金色高光/粉色主体)
python $S/defringe.py v5_frame3_cut.png v5_frame3_cut.png --lmin 190 --sat 22
# B) 深底色键素材:清掉图层边界残留的半透明"方框描边"(见下)
python $S/key_dark_bg.py moon.png moon_cut.png --span 100 --gamma 0.9 --edgefade 32 --floor 10 --trim-edge 4
# C) 出图后复查:逐层报「中性白边 / 底色残留 / 主体内近白像素」占比
python $S/qa_edges.py v5_frame3_cut.png
三个新增参数(key_dark_bg.py):
| 参数 | 作用 | 何时用 |
|---|---|---|
--edgefade N |
把 alpha 乘以「到图片边缘的距离」蒙版,让 alpha 在图层边界自然归零 | 光晕素材。光晕外沿的极低 alpha(1~15/255)会渲染成一条隐约的矩形描边,贴在渐变底上肉眼可见。明月加 --edgefade 32 后,边缘 8px 带平均 alpha 从 11.84 降到 0.00 |
--floor N |
alpha × 255 < N 的像素直接置 0 | 配 edgefade 一起用,清掉归零前的最后一点尾数 |
--trim-edge N |
删掉「贴在最外 1 圈、但向内 N 像素已透明」的孤立像素 | AI 生图会在素材最外圈画一条满亮度线(灯笼素材最右列 1152/1152 像素全不透明,RGB≈(180,71,72)),叠到海报上就是一道竖缝。trim 后灯笼删掉 1151 个孤立像素、祥云 734 个 |
三种色键模式(--mode)—— 按主体与底色的明暗关系选:
| 模式 | alpha 计算 | 适用场景 |
|---|---|---|
luma(默认) |
clip((亮度 − 底色亮度) / span) |
主体比底色亮。明月、金线波浪、祥云、金色灯笼 |
dist |
clip(色距 / span),色距 = √Σ(RGB 差²) |
主体比底色暗且底色干净均匀。深棕枝条 L≈45 vs 底色 L≈32 |
wb |
clip(((R−B) − axis) / span) |
主体比底色暗且底色带渐变/噪点。dist 会因底色不均而全图糊一层半透明 |
wb(暖蓝差)模式实测最实用:强蓝底色 R−B 为负(−76),暖色主体(棕枝/黄花/橙果/绿叶)R−B 为正(枝条 +51),两簇之间断层很干净。基线 axis 默认取 (R−B) 的 p75(背景簇上沿),也可 --axis 手动指定;该模式下背景色改由「最蓝的深底像素」重估,四角均值会被主体污染。
--bg R,G,B 必用场景:素材被裁过、四角不再全是底色时(例如祥云裁掉装饰金框后,底部四角是云朵),四角均值估出的底色会是奶油色(L≈172)而不是天空紫(L≈18.6),span 全部算错。显式传 --bg 30,4,64 即可。
AI 生图会在素材四周画装饰金框:祥云素材被模型加了顶/底/左/右金线框(顶 y18-21、底 y645+、左 x19-24、右 x1543-1548)。用逐行/逐列亮度扫描定位后 crop 掉,别靠肉眼估。裁完再用 --bg 传真实底色。
qa_edges.py 的判定必须带饱和度门槛:先测过用 R>225 & G>225 & B>215 判"白边",结果把金边框的高光(RGB 235,226,188,R−B=47)误报成白边,47.6% 的边框边缘被标记。正确判定是「亮 且 低饱和」:(亮度 > 195) & (max−min < 24)。金色高光、粉色山峦顶都会自动豁免。
别漏反预乘:色键后必须 rgb = (pixel − bg*(1−a)) / a,否则半透明边缘混着底色,叠在别的层上会有色偏。
★ 缝线扫描的误判(血泪教训)
对整图做逐列/逐行亮度梯度扫描找"图层边界伪影"时,梯度尖峰不一定是你要找的层。
实例:扫描报出 x=1211 处梯度 5.14(邻近均值 1.15,比值 4.47),位置落在「明月」层 bbox(x=704..1216)内,一度怀疑是明月渐隐没做干净。逐层拆解才发现:明月在该点 alpha 严格为 0,真正的来源是「底部山峦波浪」层的山脊线穿过该处——山峦在明月之上叠着、明月 alpha 为 0,山峦的粉脊上沿就露出了。
正确排查顺序:逐层把该点的 (alpha, RGB, 等效亮度 = L*a + L_bg*(1-a)) 打出来,看哪一层在贡献,再决定改谁。不要凭 bbox 范围猜。
★ 透明杯 / 塑料杯去背:杯壁会被泛洪「掏空」(2026-09-17 草莓啵啵奶茶海报)
症状:白底生成的透明塑料杯,去背后杯壁整圈消失,只剩中间的液体和珍珠,像"液体悬浮在空中";
qa 看起来四边 alpha=0 很干净,但杯子不成形。
原因:透明杯壁的像素本身就是近纯白,与白底同色 → 四边泛洪直接穿透杯壁进到杯内。
解法(按优先级):
1. 提示词里给杯壁一个颜色:frosted translucent cup wall with a light pink tint so the cup silhouette stays clearly visible + avoid a floating liquid look。杯壁一旦带淡粉(R>G>B),泛洪阈值就吃不掉它。
2. 仍被穿透时,用「杯身轮廓外推 + 水平镜像」修:
- 用杯身下部完好行的右边界 R(y) 做一次线性拟合,按锥形趋势 Wid = Wmed + (Y_REF1-y)*0.11 反推左边界 Ls = Rs - Wid;
- 对 y ∈ [杯口下, 杯底]、x ∈ [Ls-8, Rs+8] 的空虚处先做镜像填充(镜像位置也要是实心的才采用,否则 cv2.inpaint(radius 12) 兜底);
- 最后把左半边整体替换成右半边的镜像(只在杯身宽度内)→ 左侧白带彻底消失且左右对称。
- 别用「大半径 inpaint」硬吃宽白带:mask 外一圈全是白时,inpaint 会把白色从另一侧又传播回来。
3. 杯底那道浅灰横带是 AI 投影:把杯底范围放宽(Y_CUP1 取 0.93×h),再用「低饱和亮色清零」清掉。
★ 「低饱和亮色清零」的禁区(同批)
a[(spread<28) & (lum>130)] = 0 这招清 AI 投影很好用,但主体本身是白色/奶白时会连主体一起清。
实测:奶茶整杯跑完 → 奶油顶整块消失、透明杯壁也没了。
判据:素材里有白色/奶白实体(奶油、奶泡、白瓷、云朵)→ 该层禁用这招;
只对「有色主体 + 浅灰投影」的素材(草莓、樱桃、有色水果)使用。
铁律(血泪经验,必须遵守)
- 必须透明图层(用户 2026-09-17 最终要求):所有 AI 生图素材键控去底后保持透明底,不能带白边或其他元素残留,每个元素都是独立的透明 PNG 图层。
- 全元素 AI 生成 + 文字设计感(2026-09-17 新增):海报里每一张图、每一个图案/装饰件/图标都必须用 AI 生图生成,严禁用 PIL 程序画像素图作为画面主体元素。文字不能只是干巴巴放上去的标准字体 —— 必须有设计感(艺术字特效、渐变/描边/发光、装饰背景等)。无论是否有参考图,这条每次执行。
- 键控必须彻底,边缘干净,无背景色残留。
- PSD 必须
PSDImage.new("RGBA", (W,H)),绝不用 RGB(否则透明变蒙版)。 - 不透明度 0-255,不是 0-100;
op(pct)=round(pct*255/100)。 psd.composite()忽略 opacity/blend/group —— 预览一律用render_preview.py,自检直接读layer.opacity/blend_mode。- 图层组不能设 position。
真文字层(make_text_layer.py)— EngineData 必须"抄真 PS 的模板"
> ### ★★★ 2026-09-16 结案:PS 26.0 闪退的真凶是 EngineData(不是 TySh 外壳)
>
> 用户在 Photoshop 26.0 上实测:editable:true 的版本打开即闪退,光栅化兜底版正常。
> 因为两版只差 TySh,所以问题锁定在文字层;再逐字段比对用户桌面上真 PS 文件(PS 2021 写出,
> Desktop/原来桌面/未标题-1.psd 等)后,定位到旧版 3 处硬缺陷:
>
> | # | 缺陷 | 真 Photoshop | 旧版 |
> |---|---|---|---|
> | 1 | EngineData 结构残缺(根因) | EngineDict 7 键:Editor / ParagraphRun / StyleRun / GridInfo / AntiAlias / UseFractionalGlyphWidths / Rendered;ResourceDict 12 键;顶层还有 DocumentResources | EngineDict 2 键;ResourceDict 8 键;无 DocumentResources |
> | 2 | 颜色值顺序反了 | Values = [alpha, R, G, B] | [R, G, B, alpha] |
> | 3 | 正文结尾 / 索引 | Editor.Text 结尾必带段落符 \r;TextIndex = 0 | 无 \r;TextIndex = 1 |
>
> 颜色顺序的实证方法(别猜,用像素反推):真 PS 文字层自带光栅化字形像素,
> 量它 bbox 内不透明像素的中位色,与 EngineData 的 FillColor.Values 对齐:
> > 学校名 Values[1.0,0,0,0] -> 实测 (0,0,0) => 解释 [A,R,G,B]
> 金字 Values[1.0,1.0,0.69966,0.17647] -> 实测 (255,178,45)
> 红字 Values[1.0,1.0,0,0] -> 实测 (255,0,0)
>
>
> 修法(现行实现,勿退回手搓):不再凭文档手搓 EngineData。用
> scripts/extract_engine_template.py 从真 PS 文件里抽一份原生 EngineData 当模板
> (assets/engine_template.bin,8481 字节),生成文字层时只改 4 处:
> 正文(Editor.Text = 文本 + \r)、字号、颜色、字体(FontSet[0].Name),
> 其余结构 100% 保持 Photoshop 自己的原样。所有键名兼容 str/bytes(_key()/_get()/_put())。
> make_type_layer(..., engine_mode="legacy") 可切回旧写法,仅用于复现闪退做对照。
>
> 交付约定:用户要"文字就是文字"时,一次给两份:
> xxx.psd(editable:true 真文字层)+ xxx_光栅化兜底.psd(保证能打开)。
> 两份 spec(spec_xxx.json / spec_xxx_raster.json)内容一致、只差文字层写法。
> 若真文字层仍崩,退路是给用户一个 .jsx 脚本:在 PS 里 文件 → 脚本 → 浏览 执行,
> 由 Photoshop 自己把文字层建出来 —— 这条绝对不崩,只是需要用户点一下。
>
> 本机没有 Photoshop,无法实测。所以每次改文字层后,都要产出「探针 PSD」让用户点一下验证,
> 别等做完一整张海报才发现还崩。探针生成脚本参考 D:/anti_bully/probe/make_probes.py:
> ①新版写法 ②新版去掉 Rendered ③把真 PS 文件用 psd-tools 原样重存(判定容器是否兼容)。
> 用户明确要求「文字就是文字、不要变成图层」时:不要争论兼容性——照做,一次给两份
> (见上一节「交付约定」)。
>
> 真文字层的描边:真文字层不能"画"描边,必须写进 EngineData 样式数据。
> make_type_layer(..., stroke_rgb=(r,g,b) 0..1, stroke_weight=px) 写 StyleSheetData 的
> StrokeColor / StrokeFlag / OutlineWidth;assemble_psd.py 的 editable 分支自动从 spec
> 读 stroke + stroke_width 传入。
TySh 字节级基准(真实 Photoshop 实测,勿改)
TySh.version=1 text_version=50 warp_version=1 left/top/right/bottom = 0,0,0,0
Transform = (1, 0, 0, 1, em_left, em_top + ascent) # 1:1 缩放 + 文档坐标平移
text_data = DescriptorBlock(version=16, classID='TxLr') # 恰好 8 项
'Txt ' String '文字\x00' # 必须带终止符
'textGridding' Enumerated typeID='textGridding' enum='None'
'Ornt' Enumerated typeID='Ornt' enum='Hrzn'
'AntA' Enumerated typeID='Annt' enum='antiAliasSharp'
'bounds' Descriptor classID='bounds' {Left,Top ,Rght,Btom}
'boundingBox' Descriptor classID='boundingBox' {Left,Top ,Rght,Btom}
'TextIndex' Integer 0 # ★ 真 PS 是 0,写 1 会出问题
'EngineData' RawData <Adobe engine-data bytes>
warp = DescriptorBlock(version=16, classID='warp') # 恰好 5 项
'warpStyle' Enumerated typeID='warpStyle' enum='warpNone'
'warpValue' Double 0
'warpPerspective' Double 0
'warpPerspectiveOther' Double 0
'warpRotate' Enumerated typeID='Ornt' enum='Hrzn'
bounds是文本本地 EM 盒(基线 y=0):x: 0→W,y: -ascent→descent(descent取负值)。boundingBox是墨迹盒在本地坐标下的位置(用 PILgetbbox()得)。- 所有 Descriptor 的
name字段必须是"\x00",不能是""——这是 Photoshop 的怪癖,
写成空串会让 warp 序列化多/少 1 字节,无法与真实文件逐字节对齐。这是最容易漏的一步。 TextIndex必须是Integer型,EngineData必须是RawData型。- 不要写
substitutesUsed:12 个真实文字层里只有 1 个有它,规范形态是 8 项(不加)。
手写 TySh 的 5 个坑
ResourceDict在 EngineData 顶层,不在EngineDict里面。放错 →typesetting._build_fonts报AttributeError: 'NoneType'。- 图层名不能在建层时传中文:
LayerRecord.name是 macroman 编码,传中文直接UnicodeEncodeError。先传 ASCII 名建层,再layer.name = "中文"(setter 把 record 写成?、真串写进luniunicode 标签)。 - 别用
tagged_blocks.set_data():它会走校验,text_version默认 1 而校验器只认 50。直接赋值layer._record.tagged_blocks[Tag.TYPE_TOOL_OBJECT_SETTING] = TaggedBlock(key=..., data=ttsh)。 pixel_data_irrelevant必须是False(不是True):真实 Type 层仍带一层空白像素通道。写成 True 会让 PS 当作无数据层处理。engine_data模块有自己的Bool,和psd_tools.psd.descriptor.Bool不是同一个类。样式数据里的布尔值要用前者,混用会序列化失败。engine_data.String/Integer/Float/Bool都不是 Python 内建类型的子类(继承自ValueElement):
它们没有len()、没有endswith(),且str(x)返回的是带引号的 repr。
取值一律用getattr(x, "value", x),否则校验会误报(已踩:以为\r丢了,其实是str()把引号也带出来了)。
EngineData 一律走模板,不要手搓。真 PS 的结构是这样(Adobe 标记法文本,UTF-16BE 字符串带 BOM):
<<
/EngineDict # 7 键
/Editor << /Text (…正文…\r) >> # 结尾必须有 \r
/ParagraphRun << /DefaultRunData … /RunArray [ 段落属性 ] /RunLengthArray [ n+1 ] /IsJoinable 1 >>
/StyleRun << /DefaultRunData … /RunArray [ run0 run1 ] /RunLengthArray [ n 1 ] /IsJoinable 2 >>
/GridInfo /AntiAlias /UseFractionalGlyphWidths /Rendered # Rendered 是通用空壳,不是字形缓存
/ResourceDict # 12 键
/KinsokuSet /MojiKumiSet /TheNormalStyleSheet /TheNormalParagraphSheet
/ParagraphSheetSet /StyleSheetSet /FontSet /Superscript*(4) /SmallCapSize
/DocumentResources # 与 ResourceDict 同构的另一份
>>
StyleRun.RunLengthArray = [n, 1]:真 PS 是「正文一个 run + 段落符单独一个 run」,两个 run 同样式。- 样式键只能从 PS 认识的那 34 个里挑(
Font/FontSize/FauxBold/FauxItalic/AutoLeading/Leading/
HorizontalScale/VerticalScale/Tracking/AutoKerning/Kerning/BaselineShift/FontCaps/
FontBaseline/Underline/Strikethrough/Ligatures/DLigatures/BaselineDirection/Tsume/
StyleRunAlignment/Language/NoBreak/FillColor/StrokeColor/FillFlag/StrokeFlag/FillFirst/
YUnderline/OutlineWidth/CharacterDirection/HindiNumbers/Kashida/DiacriticPos)。
不要写StrokeWeight/StrokeOffset/FontSynthetic—— 真 PS 的 StyleSheetData 里没有这几个键,
描边宽度用OutlineWidth。 psd_tools.psd.engine_data里的类名是Integer(不是Int)、Float、Bool、String、Dict、List;
顶层键是str,嵌套键可能是bytes,所以取值要_key()/_get()/_put()两边都试。- 抽模板:
python scripts/extract_engine_template.py <真PS文件.psd> <输出.bin> [文字内容]。
验证方法(交付前必跑)
1) TySh 外壳:拿任意一份真实 Photoshop PSD 当基准,比对 warp 序列化字节:
import io
from psd_tools import PSDImage
from psd_tools.constants import Tag
def warp_hex(path):
for L in PSDImage.open(path).descendants():
if L.kind == "type":
b = io.BytesIO()
L._record.tagged_blocks[Tag.TYPE_TOOL_OBJECT_SETTING].data.warp.write(b)
return b.getvalue()
assert warp_hex("真实PS.psd") == warp_hex("产物.psd")
2) EngineData 结构(新版必查,这是 PS 26 闪退的根因所在):
from psd_tools import PSDImage
from psd_tools.constants import Tag
def _k(d,n):
for k in (n, n.encode()):
if k in d: return d[k]
raise KeyError(n)
def _txt(x): # engine_data 的 String 不是 str 子类!
return getattr(x, "value", x) # str(x) 会连引号一起返回,必须取 .value
for L in PSDImage.open("产物.psd").descendants():
if L.kind != "type": continue
td = L._record.tagged_blocks[Tag.TYPE_TOOL_OBJECT_SETTING].data.text_data
ed = td[b"EngineData"].value
eng = _k(ed, "EngineDict")
ssd = _k(_k(_k(_k(eng,"StyleRun"),"RunArray")[0],"StyleSheet"),"StyleSheetData")
fv = [_txt(x) for x in _k(_k(ssd,"FillColor"),"Values")]
assert len(list(eng.keys())) >= 6, "EngineDict 必须 6~7 键(去 Rendered 实验时为 6)"
assert len(list(_k(ed,"ResourceDict").keys())) == 12, "ResourceDict 必须 12 键"
assert len(list(ed.keys())) == 3, "顶层必须 EngineDict/ResourceDict/DocumentResources"
assert _txt(_k(_k(eng,"Editor"),"Text")).endswith("\r"), "正文结尾必须带 \\r"
assert _txt(td[b"TextIndex"]) == 0, "TextIndex 必须 0"
assert fv[0] == 1.0, "颜色必须是 [alpha,R,G,B]"
print("EngineData 结构 OK", L.text, fv)
通过标准:上面两条全过 + text_data 8 项 + warp 5 项 + version/text_version/warp_version = 1/50/1
+ l=t=r=b=0 + pixel_data_irrelevant is False。
> ⚠️ pixel_data_irrelevant 在 layer._record.flags.pixel_data_irrelevant 上,
> 不在 TySh 的 data 里 —— 写成 tt.data.pixel_data_irrelevant 会 AttributeError(已踩)。
3) 探针实测(唯一能确认"PS 真的不崩"的办法,本机没有 PS):生成 2~3 个小探针 PSD 交用户点开,
参考 D:/anti_bully/probe/make_probes.py。
布局定位陷阱(双偏移)
image 非 cover 模式下,图层像素就是缩放后的元素本身,靠 layer.left/top 定位即可。如果把元素先贴到整张透明画布上再设 left/top,元素会落到 (2×left, 2×top) —— 表现为「人物/元素整体飞出画面」。这是 v3 版人物消失的根因。
⚠️⚠️ 漏写 scale = 所有素材按原始尺寸(1024×1024)粘贴(2026-09-17「你好秋天」海报,排版全乱的真凶)
assemble_psd.py 的 image 层只有 spec 里给了 scale 才缩放(if "scale" in layer:),
否则直接把素材原图贴上去。而生成的素材多是 1024×1024 画布、内容只占中间一小块,
于是「素材按 1024px 贴、left/top 却按缩小后的尺寸算」→ 尺寸全错、位置全偏。
外在表现就是用户说的「排版很乱」「某元素变成一大团糊在画面中间」
(本次「胶囊底衬」就这样变成正中一团 1024px 的淡金色大块)。
排查口诀:预览里出现"不该那么大的元素"或"元素集体偏移",先查 spec 的 image 层有没有 scale。
★★ 复刻参考图版式的正解:按素材「实际内容框」(ink bbox) 对位
素材四周都有大片透明留白,按画布尺寸缩放定位必然偏。正确做法是先量 ink bbox 再算 scale:
a = np.asarray(Image.open(p).convert("RGBA"))[:, :, 3]
ys, xs = np.nonzero(a > 12) # 素材真正有像素的范围
ix, iy, iw, ih = xs.min(), ys.min(), xs.max()-xs.min()+1, ys.max()-ys.min()+1
scale = (H * h_frac) / ih # 让 ink 高度 = 目标高度(或按宽)
layer = {"type": "image", "src": p,
"left": round(cx*W - ix*scale - iw*scale/2), # 用 ink 中心对齐
"top": round(cy*H - iy*scale - ih*scale/2),
"scale": round(scale, 6)}
即 left = 目标X − ix*scale,用 ink 偏移抵消素材自身的透明留白。
配套姿势(本次实测有效):
1. 先量参考图(_ref_poster.png)—— 依次扫描出标题/枫叶/女孩/徽章/底部色条的归一化 bbox,
不是凭感觉摆。
2. 写一个 compare.py:把「参考图 | 新预览 | 50% 叠加」拼成三联图自己看。
叠加图能一眼看出谁偏了、偏多少,比单看预览可靠得多。
3. 逐个元素「对号入座」后再微调,一次就能收敛。
4. 同一素材可复制多份(不同角度/位置)当独立图层用,如 ginkgo_front 同时当左下/右下前景叶。
装饰元素必须落在「没被其它层盖住」的位置:初版把 2 朵装饰云放在 y=86(在卡片层的下面),
预览里完全看不见 —— 等于白做两层。摆放装饰前先想清楚该处有没有更上层的图层覆盖;
放完一定要看预览确认它真的露出来了。堆叠顺序:背景渐变 → 装饰 → 卡片/容器 → 内容 → 文字。
高密度多栏填充(两遍布局法)
用户给的参考图如果是「每栏塞满、到边到角」的密排版式(如 4 栏防欺凌海报、宣传单),
固定 y + 固定间距的写法必然在短栏底部留出大片空白(踩过:第 1/4 栏各空 ~100px)。
正确做法是两遍布局:
- 把每栏的内容拆成「块」,每块记录
(自然高度, emit 闭包),先不真正落点,只累计total。 - 算全局间距:
gap = clamp((avail - total) / (n-1), 10, 38)。
-avail= 卡片内可用高度(卡片底 − 卡片顶 − 上下内边距)。
- 间距下限 10 防挤死、上限 38 防散架;最满的那栏决定全局字号/行高,其余栏用 gap 吸收余量。 - 第二遍按
top = top0 + 累计高度 + i*gap逐块emit,各栏自然填到卡片底。
兜底填充:若某栏内容仍不足(算出来 gap 触顶 38 还填不满),往该栏追加「提示框 / 补充段」块,
不要留白。装饰(标题弧带、白卡、迷你卡底、序号圆、方块)一律用 PIL 程序化生成,
风格统一、边缘干净,别交给 AI(AI 出的装饰常带硬边/不一致)。
实测:avail=786 的 4 栏(各 518 宽),两遍法后 130 层、68 真文字层,四栏全部填到卡片底,无空白。
预览(render_preview.py)
psd-tools 无法光栅化文字层,所以预览做法是:合成所有 pixel/gradient/color 层(带 opacity + blend)→ 再用 PIL 叠字。
定位直接读 TySh.transform 的 (e, f) —— e = 首字左侧的文档 x,f = 首字基线的文档 y,这才是与 PS 像素级对齐的正确做法(读不到 transform 时才退回 top + (bottom-top)*0.85 的近似算法)。
真文字层要带描边:d.text(..., stroke_width=int(t.get("stroke_width",0) or 0), stroke_fill=t.get("stroke"))——否则规里有描边的标题,预览会"看不见描边"、和 PS 内观感不一致(已修)。
★ 文字绝不能被画两遍(2026-09-17 修):psd_tools 保存 PSD 时会把文字层的像素数据一并写进文件,
mock_preview.render() 合成像素层时会把它们也叠进来。此时若再叠一次 PIL 画的文字,
西文/数字(字距度量与中文字体差异大)就会明显「重影/错位」——
中文因字面方正常常只表现为轻微发糊,极易漏掉(本次「HUIDONG…」和徽章「2020/NEW」双影即此)。
render_preview.py 现行做法:合成像素层前先把所有 kind=="type" 的层 visible=False,
合成完再恢复,然后统一用 PIL 按 TySh.transform 画一次:
type_layers_all = [l for l in psd.descendants() if l.kind == "type"]
for l in type_layers_all: l.visible = False
img = render(psd)
for l in type_layers_all: l.visible = True
d = ImageDraw.Draw(img) # 之后照旧叠字
自查方法:单独跑 mock_preview.render(psd)(不叠字)存一张,若里面已经有文字,
说明文字层带像素数据 —— 必须走上面的隐藏流程。
字体
font(PIL 测量)用系统 TTF 文件名,font_ps 用 PostScript 名写进 FontSet:SimHei(黑体)、STXingkai(华文行楷,喜庆标题好看)、STXinwei(华文魏楷)、Microsoft YaHei、SimSun。PS 里找不到同名字体时会自动替换成中文字体,不会出豆腐块。
竖排中文(节气/国风海报高频)
- 多行竖排:
spec里把text写成 每字一行("夏\n意\n犹\n未\n尽")+line_height(≈字号×1.13),anchor:center。assemble_psd.py的render_text已支持\n多行(向后兼容单行),一行=一列,从右往左排需要手工指定每列的 x。 - ★ 传统竖排必须从右往左读:第一列在最右。例:「夏意犹未尽,忽觉秋意来」→ 右列「夏意犹未尽」、左列「忽觉秋意来」(写反了意思就错了)。排的时候把先读的那列放到更大的 x。
- 字体:竖排诗句/对联用毛笔行楷(
STXINGKA.TTF= 华文行楷)才对味,用雅黑会显得很"通知"。参考图若是书法体,务必换行楷/魏楷。 - 竖排多行建议走光栅化层(
editable不设 true):真文字层的多段落结构在 PS 里未验证,规避闪退风险;单行文字仍用真文字层。
限制与建议
- AI 生图产出的是「合成好的一张图」,不是天然分层。要可编辑图层,靠「分元素分别生成 → 去背 → 叠成层」最稳;纯插画从零创作也可走 RGBA 拼装 + 文字/调色层。
- 嫦娥/玉兔等人物若生成在「主视觉」整图里,则属于同一层;若需其独立可移动,单独生成贴纸并去背成独立层。
- 透明底质量取决于「白底贴纸」提示词,偶有白边需微调阈值。
- 真文字层的
TySh+EngineData现已是「真 PS 外壳 + 真 PS EngineData 模板」,
结构上已与 Photoshop 自己写出的文件对齐(warp逐字节相同、EngineDict 7 键 / ResourceDict 12 键、
颜色[A,R,G,B]、正文带\r、TextIndex=0)。但本机无 PS,无法自证不闪退 ——
所以:① 交付仍双份(真文字层 + 光栅化兜底);② 改完文字层后先出探针 PSD 让用户点一下。
兼容性优先时请使用光栅化文字层。 - 交付 PSD 后建议同时给预览 PNG,因为部分看图软件不支持文字层。
- 部署/分享:PSD 是二进制,建议本地交付或压缩后分享;不要在公开页面预填真实隐私。
依赖
- Python 3.10+ 即可(系统自带或任意 venv 均可)。
- 依赖已随包提供,无需手动
pip install。skill 自带离线 wheel(wheels/,已锁定版本)与引导器(scripts/run.py/scripts/bootstrap.py):
首次运行任意命令时,run.py会在 skill 目录内自动建隔离环境.venv并离线装好Pillow/psd-tools/requests/numpy/aggdraw等全部依赖,之后秒跑;
若离线 wheel 与本机 Python 版本不符(罕见),自动退回 PyPI 在线安装。 - 也可显式一次性装好:
python3 scripts/bootstrap.py(建好.venv并打印其 python 路径,等价首次自动安装)。 - AI 生图认证(AGNES_API_KEY)⚠️ 安装后使用前必配:本 skill 已内置
scripts/agnes_api.py,不再依赖agnes-image-genskill。 - 功能分两类,请分清:① 拼装 PSD / 预览 / 示例验证 → 无需 Key,纯本地离线即用;② AI 生图(生成海报素材) → 必须配
AGNES_API_KEY,否则生图会被拦下。 - Key 免费申请(每月 500 张免费额度):登录 https://agnes-ai.com →「API Key 管理」→ 创建并复制。
- 配置方式二选一:① 在 skill 根目录(放
SKILL.md的文件夹)建.env写AGNES_API_KEY=你的key(脚本自动读取,参考同目录.env.example);② 或导出环境变量export AGNES_API_KEY=你的key。 - 没配 Key 时不会崩溃/静默失败:
gen_asset.py会打印一段中文指引(去哪申请、怎么配.env),配好再跑即可。安装说明.md 顶部「⚠️ 安装前必读」已用表格把这条讲清楚,别人安装时一眼可见。 - 本 skill 完全自包含:
scripts/下已内置agnes_api.py、mock_preview.py、make_text_layer.py等全部依赖,
模板assets/engine_template.bin随包分发,无需安装 image-to-psd / agnes-image-gen 即可独立运行。 - 字体:中文字体(SimHei / STXingkai / STXinwei / Microsoft YaHei / SimSun)按系统目录自动探测
(WindowsC:/Windows/Fonts、macOS/System/Library/Fonts、Linux/usr/share/fonts),缺字时降级到默认字体,不会报错。