一句话
把 Illustrator 里已经编好组的矢量图形,一次性拆成一张张透明 PNG。
名字里的「矢量图」指的是 Adobe Illustrator 的
.ai矢量文件,不是“人工智能”。 这个工具原来叫「AI 分组导出工具」—— 那个 AI 是 Illustrator 的缩写, 但十个人里有九个会读成人工智能,所以改成了现在这个名字。
要解决的事
矢量素材(图标、字标、装饰元素…)常常是一个 .ai 文件里躺着几十个图形,
每个都编好了组,但 Illustrator 本身没有「把每个组各存一张图」这个功能。
手工做法是:选中一个组 → 新建文档 → 粘贴 → 把画板裁到图形边界 → 导出 → 关掉文档。 一个图形六步,几十个就是几百次点击,而且手一抖裁歪一个就得重来。
这个工具把这一串动作交给脚本:打开文件 → 遍历组 → 每个组独立成图。 全程不碰源文件 —— 所有文档(包括临时新建的那些)都以「不保存」方式关闭。
能力一览
| 能力 | 说明 |
|---|---|
| 批量 | 一次处理整个文件夹的 .ai,或单个文件内的全部对象 |
| 两种范围 | 按分组(一个组一张图)/ 按单个图形(没编组的散图形也各出一张) |
| 分辨率 | PPI 任意填,默认 300(72 PPI ≈ 1:1,300 ≈ 4.17 倍线长) |
| 格式 | PNG(透明 PNG24)/ PNG8(8 位透明,更小)/ JPG(不透明)/ SVG(矢量) |
| 裁剪 | 以 controlBounds 裁画板 → 尊重裁剪蒙版,得到紧贴图形的透明图 |
| 过滤 | 最小边长过滤(丢掉碎屑)、跳过与画板同尺寸的「整版背景组」 |
| 图层 | 自动解锁 / 显示所有图层,藏在锁定图层上的组也能导出 |
| 命名 | 模板化,{doc} {i} {i2} {i3} {i4} {name},重名自动去重 |
| 两种入口 | 图形界面(tkinter)与命令行(argparse),同一套内核 |
下载
| 文件 | 体积 | 说明 |
|---|---|---|
| ai_png_exporter.exe | 10.9 MiB | 主程序,双击即用,无需安装 Python |
| ai_group_exporter.py | 32 KB | 源码(图形界面 + 命令行 + COM 内核,全在一个文件里) |
| export_groups.jsx | 4 KB | 兜底脚本,直接在 Illustrator 里运行,不需要 Python |
EXE 里已经把 Python 运行时、tkinter、pywin32、Pillow 全部打进包里了。 唯一的前置条件是本机装了 Adobe Illustrator(2020 及以上) —— 工具是 用 Windows 的 COM 接口遥控 Illustrator 干活的,Illustrator 本身必须在场。
EXE 与窗口图标是同一份素材(app_icon.ico,由白底 LOGO 转透明生成),
打包时一起进了 --icon 和 --add-data:
快速上手(EXE)
- 双击
ai_png_exporter.exe,先读面板顶部那段「使用说明(导出前必读)」 - 选「单个文件」或「文件夹(批量)」→「浏览…」选中
.ai - 点「分析文件」可以先看它认出了多少个对象(会临时启动 Illustrator,看完自动关)
- 在「导出参数」里选导出范围:按分组(默认)/ 按单个图形
- 设分辨率、留白、格式、输出目录、命名模板
- 点「开始导出」,日志区实时滚动(每个对象一行,带导出后的像素尺寸)
界面(2026-09-22 更新)
窗口现在可以自由缩放,全部内容装在一个可滚动的画布里 —— 把窗口拉矮、或者屏幕本来就不大时, 右侧滚动条和鼠标滚轮都能用,不会再出现“下面半截点不到”。
启动时窗口会按内容所需高度自动定尺寸,上限是屏幕高度的 92%,超出的部分交给滚动条。
(旧版是写死的 760×640 + 一段固定 10 行高的说明框,小屏上底部的按钮会被裁掉 —— 这一版就是冲着这个改的。)
面板自上而下:使用说明(导出前必读) → 导出操作 → 选择要导出的 .ai → 导出参数 → 日志区。 「开始导出 / 停止 / 打开输出目录」这回单独拎成一块、紧贴在说明下方,不用再拖到最后去找。
两种导出范围
这是整个工具里最需要说清楚的一个选项 —— 它决定了「什么算一张图」。
| 按分组(默认) | 按单个图形 | |
|---|---|---|
| 遍历的是 | 文档的顶层 GroupItems |
每个图层的顶层 PageItems |
| 一个已编组的图标 | 整组一张图 | 整组一张图 |
| 一个没编组的散图形 | 不会被单独导出 | 自己一张图 |
| 适合 | 文件已经规规矩矩分好组 | 文件是散的,想全量拆出来 |
| 「递归导出嵌套分组」 | 有效(可以连子组也各出一张) | 自动失效(已经是逐对象了) |
导出参数(命令行)
图形界面里的每一项,命令行都有对应参数 —— 适合挂到批处理里跑。
| 参数 | 含义 | 默认 |
|---|---|---|
-i / --input |
输入 .ai 文件或文件夹 |
必填 |
-o / --out |
输出目录 | 必填 |
--ppi |
分辨率 | 300 |
--pad |
四周留白(pt) | 0 |
--template |
命名模板 | {doc}_{i3} |
--recursive |
递归导出嵌套分组(仅 scope=group 有效) |
关 |
--min-size |
最小边长(pt),小于则跳过 | 0(不过滤) |
--skip-full-artboard |
跳过与画板同尺寸的背景组 | 关 |
--no-aa |
关闭抗锯齿 | 关 |
--format |
png / png8 / jpg / svg |
png |
--scope |
group / item |
group |
--visible |
显示 Illustrator 窗口(默认后台静默) | 关 |
# 一个文件夹里的 .ai 全拆成 PNG
python ai_group_exporter.py -i "D:/图标源文件" -o "D:/导出" --ppi 300 --template "{doc}_{i3}"
# 文件没编组?换「按单个图形」,散图形也会各出一张
python ai_group_exporter.py -i "图标.ai" -o "导出" --scope item
命名模板
| 变量 | 含义 |
|---|---|
{doc} |
源文件名(不含扩展名) |
{i} |
序号,从 1 开始,不补零 |
{i2} / {i3} / {i4} |
序号补零到 2 / 3 / 4 位 |
{name} |
分组在 Illustrator 里的名字(为空时退回 groupN) |
模板 {doc}_{i3} → SSS_001.png、SSS_002.png…… 分组名里的
\ / : * ? " < > | 与换行制表符会被换成 _,超过 60 字截断 —— 免得导出一个
Windows 上根本存不下去的文件名。
导出成果
下面这张是拿 SSS.ai 跑完整批的结果:40 个分组 → 40 张图,按导出顺序排开。
每一张都是透明背景、各自裁到自己的图形边界:
单独看几张的成品质量 —— 下面这几张的浅色底只是为了让透明 PNG 看得清, 文件本身没有背景:
同一个文件里图形的比例差得很远 —— 有 354×354 的正方形小图标,也有 3467×675 的长条(那排箭头和条纹)。它们各自按自己的边界裁切,所以同一次导出里 「一张 600×600、下一张 3500×680」是正常的,居中、留白都各归各的。
前置操作(导出前必做)
工具不猜你的意图,它只认 Illustrator 里的结构。所以导出前:
- 按分组模式要先编组:在 Illustrator 里选中一个图标的全部图形,
Ctrl+G。 没编组的图形在「按分组」模式下会被直接跳过(不是丢失,是它不属于任何组)。 - 别把整版底纹留在文件里:如果文件里有个覆盖整张画板的背景 / 底纹分组,
它会作为「一个分组」被导出成一张巨图 —— 本次的示例文件里就有这么一组
(序号 1,8000×4500 的满画板斜条纹)。要么在 Illustrator 里删掉它,
要么勾上
--skip-full-artboard/ 界面上的同名选项。 - 锁定 / 隐藏图层不用管:工具会先把所有图层解锁并显示,导出完不影响源文件 (不保存关闭)。
已知的坑(写给下一个改这份代码的人)
几个都是踩过才写下来的,见模块页里的完整拆解。
UserInteractionLevel = 2才是「静默全部弹窗」。直觉上「1 = 少弹」, 实际 1 反而会弹,2(aiSilenceAll)才安静 —— 否则缺字体、缺链接的对话框 会在无人值守时把批处理卡死。- 选中对象要用列表:
app.Selection = [g]。直接传单个对象在部分版本上对 散落的PathItem会复制失败。 - 下标从 1 开始(COM),而同一套逻辑写成 JSX 时是从 0 开始 —— 两份实现对拷时这是最容易错的一处。
ControlBounds而不是VisibleBounds:前者尊重裁剪蒙版,后者会把 被蒙版裁掉的部分也算进边界,导出的图四周会多出一圈空白。--skip-full-artboard的判据是「尺寸近似 + 原点近似」:容差 2pt。 背景组几乎都是严格贴画板的,所以这个宽松判据够用,也不会误伤真的贴边图形。- 兜底脚本里
{i4}没实现:JSX 版只做了{i}{i2}{i3}{name}, 够用就没补。
常见问题
提示找不到 Illustrator / pywin32?
EXE 版只可能是没装 Illustrator(或者装的是很老的版本)。源码版则是
pip install pywin32 没成功 —— 注意要在跑脚本的那个 Python 环境里装。
有些分组没导出? 先看日志里的「跳过」行:多半是尺寸小于最小边长过滤、或者与画板同尺寸被当成 背景组跳过了。如果不是这两种,常常是空分组(里面什么都没有),读不到边界。
导出很慢? 正常。每个对象都要「新建文档 → 粘贴 → 光栅化 → 导出 → 关闭」一轮, 组越多、文件越大越慢。命令行模式放后台跑就行。
能在 Mac 上用吗?
不能。整条链路建立在 Windows COM 上。Mac 上可以用 export_groups.jsx
那条路(Illustrator 自带的 ExtendScript,两端都能跑)。
想自己重新打包 EXE
pyinstaller --onefile --windowed --name ai_png_exporter ^
--icon app_icon.ico --add-data "app_icon.ico;." ^
--distpath dist --workpath build ai_group_exporter.py
⚠️ 必须用「带 tkinter 的 Python」来打包。精简版 Python(比如各种嵌入式 /
托管发行版)不含 tcl-tk,PyInstaller 会报 tkinter installation is broken
然后把它排除掉,生成的 EXE 一启动就崩。用本机标准安装的 Python
(自带 tcl/tk 8.6)建个 venv,装 pyinstaller pywin32 pillow 再打。
Python 导出内核
一个 775 行的文件里装着图形界面、命令行与 COM 内核三套东西。这里是它怎么把「选中一个组」变成「一张透明 PNG」的完整拆解。
一个文件,三种身份
ai_group_exporter.py 里同时住着三套东西,靠 main() 一次分流:
def main():
# 提供 --input 即进入命令行模式;--help / -h 也走 CLI 以显示参数说明
if "--input" in sys.argv or "-i" in sys.argv or \
"--help" in sys.argv or "-h" in sys.argv:
run_cli()
else:
run_gui()
双击 EXE → 无参数 → 图形界面;带 --input → 命令行。两边共用同一套内核
(connect_illustrator / collect_targets / export_group / run_export),
所以「界面上能做的事」和「脚本里能做的事」永远是同一件事,不会出现
「GUI 有的功能 CLI 没有」这种分叉。
数据流
选中的 .ai
│
├─ app.Open(path) ─────────── 打开源文档(全程不保存)
│
├─ unlock_layers(doc) ─────── 所有图层 visible=True, locked=False
│
├─ collect_targets(doc, scope, recursive)
│ ├─ scope=group → doc.GroupItems 递归遍历(可选进子分组)
│ └─ scope=item → 每个 layer.PageItems(顶层图元)
│
└─ for 每个对象 ───────────── export_group()
│
│ ① app.Selection = [g] 选中
│ ② app.Copy() 复制
│ ③ Documents.Add(RGB) 新建临时文档
│ ④ Paste() + 量边界 + 裁画板
│ ⑤ Export(path, type, opt) 导出
│ ⑥ Close(不保存) 关掉临时文档
│
└─ 回到源文档继续下一个 …… 最后 doc.Close(不保存)
注意整条链路里没有任何一步写源文件 —— 打开它、读完、不保存关掉。 临时文档也只活在 ④ 到 ⑥ 之间。
逐个函数拆
connect_illustrator(visible) —— 连接与静默
app = win32com.client.gencache.EnsureDispatch("Illustrator.Application")
try:
app.UserInteractionLevel = 2 # 2 = aiSilenceAll
except Exception:
pass
gencache.EnsureDispatch 而不是 Dispatch:前者会生成类型库缓存,
后面 app.Documents.Add(AI_RGB, 100, 100) 这类调用才能按真实签名传参,
不然得靠位置参数硬试。
UserInteractionLevel = 2 是这里唯一一个反直觉的常量:直觉上「1 少弹、
2 全弹」,实际相反 —— 1 会弹对话框,2(aiSilenceAll)才静默。批处理里
但凡漏掉这一句,遇到缺字体 / 缺链接的 .ai 就会停在弹窗上无人值守地卡死。
unlock_layers(doc) —— 先解放图层
遍历 doc.Layers,逐个 Visible = True、Locked = False。
听起来像副作用很大的操作,其实不是:文档最后是「不保存」关闭的,
这些改动不会落盘。但少了它,锁定或隐藏图层上的分组既枚举不到、也复制不了
(doc.GroupItems 会跳过它们),会出现「我文件里明明有 40 个图标,只导出了 12 个」
这种最难排查的问题。
collect_targets(doc, scope, recursive) —— 两种「什么算一张图」
if scope == "item":
# 逐图层取顶层图元:分组(整体)+ 未编组的散落图形
for L in range(1, doc.Layers.Count + 1):
lay = doc.Layers.Item(L)
for i in range(1, lay.PageItems.Count + 1):
result.append(lay.PageItems.Item(i))
return result
# group 模式:递归遍历文档全部顶层分组
def walk(container):
for i in range(1, container.GroupItems.Count + 1):
g = container.GroupItems.Item(i)
result.append(g)
if recursive:
walk(g)
scope=group走GroupItems,只认组。未编组的散图形不属于任何组, 于是不会被导出 —— 这是设计如此,不是 bug。scope=item走layer.PageItems(图层的顶层图元)。一个组本身也是一个 顶层图元,所以「已编组的图标整组出一张 + 散图形各出一张」可以同时成立。- 两个分支都用
range(1, Count + 1)—— COM 的集合下标从 1 开始。
export_group(app, g, out_path, opts) —— 六步的核心
app.Selection = [g] # ① 用列表,见下
app.Copy() # ②
newdoc = app.Documents.Add(AI_RGB, 100, 100) # ③ 100×100pt 只是个临时画布
try:
newdoc.Paste() # ④
...
for i in range(1, newdoc.PageItems.Count + 1):
bb = list(it.ControlBounds) # 尊重裁剪蒙版
xs += [bb[0], bb[2]]; ys += [bb[1], bb[3]]
l, tp, r, bo = min(xs), max(ys), max(xs), min(ys)
if r - l < 1: r = l + 1 # 兜底:零尺寸对象
if tp - bo < 1: tp = bo + 1
rect = [l - pad, tp + pad, r + pad, bo - pad]
newdoc.Artboards.Item(1).ArtboardRect = rect # ⑤ 画板裁到图形
etype, _ext, exp = build_export_options(opts)
newdoc.Export(out_path, etype, exp) # ⑥
finally:
newdoc.Close(AI_DO_NOT_SAVE) # 无论成败都关掉,别攒一堆临时文档
四个细节值得留一句:
app.Selection = [g]必须是列表。传裸对象g在部分 Illustrator 版本上 对散落的PathItem会静默复制失败,然后你得到一张空白图。代码里做了三级兜底 (列表 → 裸对象 →g.Selected = True),任何一级成功就继续。- 边界取
ControlBounds而不是VisibleBounds。前者把裁剪蒙版算进去, 后者会把被蒙版裁掉的部分也算进包围盒 —— 用后者导出的图四周会多出一圈透明留白。 - 裁的是新建文档的第 1 个画板,不是源文档的。源文件是几块画板都不影响: 多画板文件以第 1 块为基准,每个分组各成一张图,互不干扰。
finally里关文档。导出抛异常时如果不关,批处理跑几十个组会攒出几十个 未保存的临时文档,Illustrator 会越来越慢直到内存吃满。
build_export_options(opts) —— 一张格式表 + 一堆 try
EXPORT_FORMATS = {
"png": (5, "png", "PNG24"), # 透明 PNG(默认)
"png8": (4, "png", "PNG8"), # 8 位透明,体积更小
"jpg": (1, "jpg", "JPEG"), # 不支持透明,背景为白
"svg": (3, "svg", "SVG"), # 矢量
}
整数是 Illustrator 类型库里的 AiExportType 实测值(PNG24 = 5)。
ExportOptions 类的名字也是字符串拼出来的:"Illustrator.ExportOptions%s" % cls
—— 四种格式各对应一个类,属性各不相同(PNG 有 Transparency、
JPEG 有 Quality、SVG 有 EmbedRasterImages)。
所有属性赋值都包在 try/except 里,看起来像偷懒,其实是必须的:
不同 Illustrator 版本暴露的属性集不一样,某个属性不存在时 COM 会直接抛异常。
这里的选择是「属性不存在就跳过」而不是「整体失败」—— 因为多数属性只是锦上添花
(抗锯齿、SaveAsHTML=False),真正决定成败的是 ExportType 和画板尺寸。
分辨率走的是缩放而不是 DPI 元数据:
scale = opts.ppi / 72.0 * 100.0 # PostScript 1pt = 1/72 英寸
exp.HorizontalScale = scale
exp.VerticalScale = scale
72 PPI 就是 100%(1:1),300 PPI 是 417%。光栅格式才设缩放,SVG 是矢量, 设了也没意义。
命名:sanitize / make_name / dedupe
_BAD = re.compile(r'[\\/:*?"<>|\r\n\t]+')
def make_name(template, doc_base, idx, gname):
raw = (gname or "").strip() or ("group%d" % idx)
safe = sanitize(raw)
return (template.replace("{doc}", doc_base)
.replace("{i4}", "%04d" % idx)
.replace("{i3}", "%03d" % idx)
.replace("{i2}", "%02d" % idx)
.replace("{i}", str(idx))
.replace("{name}", safe))
- 正则里那串字符是 Windows 文件名非法字符 + 换行制表符,替换成
_; 再长截 60 字。 - 替换顺序
{i4}→{i3}→{i2}→{i}不能反。反了的话{i3}会先被%03d的产物污染({i}是{i3}的前缀,先替短的会毁掉长的)。 - 分组名可能是空的(Illustrator 允许无名组),退回
groupN。 dedupe用字典记数,重名变成名字_2、名字_3—— 不然同一次导出会互相覆盖。
process_file(...) —— 过滤与编排
在真正导出之前拦两道:
w, h = b[2] - b[0], b[1] - b[3]
if opts.min_size and max(w, h) < opts.min_size:
... 跳过(尺寸过小)
if opts.skip_full_artboard and aw > 0 and ah > 0:
if (abs(w - aw) < 2 and abs(h - ah) < 2 and
abs(b[0]) < 2 and abs(b[1]) < 2):
... 跳过(与画板同尺寸的背景分组)
「最小边长」用的是 max(w, h) —— 阈值只卡最长边,所以一根细长的竖条不会被
误杀(它的最大边很长)。而「跳过背景组」的判据是尺寸近似 + 原点近似
(容差 2pt):背景矩形几乎都是严格贴死画板的,这个宽松判据够用,也不会误伤
恰好贴边的小图形。
run_gui() —— 为什么所有导出都在后台线程里
def start():
stop_event.clear()
threading.Thread(target=worker, daemon=True).start()
tkinter 是单线程的:主线程跑 mainloop() 负责界面响应。导出动辄几分钟,
直接在按钮回调里写循环,界面会整个冻住(连「停止」都点不动)。
所以 worker() 里做三件事:解析参数 → connect_illustrator(visible=False)
→ run_export(...),日志通过 log_msg 回调回主线程。跨线程更新控件在这里
没做 after() 排队 —— 因为 log_msg 只往 ScrolledText 里 append 文本,
是实测稳定的一条路(真出问题再加队列)。
visible=False:GUI 模式默认让 Illustrator 在后台跑,屏幕上不会闪出一堆窗口。stop_event:一个threading.Event。导出循环每一轮开头检查一次, 所以点「停止」是在完成当前分组之后停下 —— 中断时机落在对象边界上, 不会留下半张损坏的图片。daemon=True:关窗口时线程跟着进程一起结束,不会挂住。
「分析文件」按钮是同样的套路:另起一条线程打开文件数一遍对象,数完关掉、把 数字回填到标签上,并且顺手先把 Illustrator 起来 —— 后面真导出时就不用再等启动。
三处常量,各自有出处
| 常量 | 值 | 出处 |
|---|---|---|
AI_PNG24 |
5 |
AiExportType.aiPNG24,本机 Illustrator 2020 类型库实测 |
AI_RGB |
1 |
AiDocumentColorSpace.aiRGB —— 导出用的临时文档必须建在 RGB 下 |
AI_DO_NOT_SAVE |
2 |
AiDocumentSaveOptions.aiDoNotSaveChanges |
UserInteractionLevel |
2 |
aiSilenceAll(1 不是,见上) |
代码里特意注释了「BMP / PDF 的 ExportOptions 类在本机不可用,故不提供」——
这就是那份格式表只有四项的原因,不是漏了。
JSX 兜底脚本
同一套导出逻辑的 ExtendScript 版本。不装 Python、不用 Windows,直接在 Illustrator 里跑 —— 代价是参数写死在文件顶部。
为什么还要留一份 JSX
Python 那套是主力,但它有两个死条件:Windows(COM 只在 Windows 上存在)
和装 Python(虽然打包成 EXE 之后这条免了)。export_groups.jsx 是这两条
都不成立时的退路:
- 在 Mac 上也能用(Illustrator 的 ExtendScript 两端通用)
- 不用装任何东西 —— Illustrator 自己就是运行时
- Python/COM 出问题(类型库缓存坏了、杀软拦了 COM 调用)时的替代路径
代价很明确:参数写在文件顶部,改一次要编辑源文件。没有界面,没有命令行, 没有格式选择。
怎么跑
文件 > 脚本 > 其他脚本... 选中 export_groups.jsx,然后:
- 弹窗选「输入文件夹」(里面放
.ai) - 弹窗选「输出文件夹」
- 跑完弹一个「导出完成:成功 N / 失败 M」
顶部三个常量就是全部可调项:
var PPI = 300; // 导出分辨率
var PAD = 0; // 四周留白(pt)
var TEMPLATE = "{doc}_{i3}"; // 命名模板:{doc}文件名 {i}序号 {i3}补零
与 Python 版的关系:同一套算法,两套下标
两份实现是逐行对应的 —— 同样的「选中 → 复制 → 新建 RGB 文档 → 粘贴 → 按 controlBounds 裁画板 → 导出 PNG24 透明 → 关掉不保存」。差异集中在三处:
| Python 版(COM) | JSX 版 | |
|---|---|---|
| 集合下标 | 从 1 开始,loop(1, Count+1) |
从 0 开始,loop(0, length) |
| 选中对象 | app.Selection = [g](列表更稳) |
app.selection = g(替换式选择) |
| 导出调用 | doc.Export(路径, ExportType, opts) |
doc.exportFile(new File(路径), ExportType.PNG24, opts) |
| 常量 | 类里查(AI_PNG24 = 5) |
全局枚举(ExportType.PNG24) |
| 关文档 | Close(AI_DO_NOT_SAVE) |
close(SaveOptions.DONOTSAVECHANGES) |
下标这把刀最容易割手:把 Python 版逻辑搬到 JSX 时如果忘了 -1,
最轻是漏掉第一个对象,最重是最后一次循环越界抛异常。文件顶部专门留了一行注释
钉这件事:
// 注意:JSX 中集合下标从 0 开始(COM/Python 从 1 开始)。
少了什么(和 Python 版对比)
JSX 版是够用就好的那一档,刻意没做的:
| 功能 | Python 版 | JSX 版 |
|---|---|---|
按单个图形导出(--scope item) |
有 | 无(只遍历顶层 groupItems) |
| 递归嵌套分组 | 有 | 无 |
| 格式选择(PNG8 / JPG / SVG) | 有 | 无(固定 PNG24 透明) |
| 最小边长过滤 / 跳过背景组 | 有 | 无 |
| 进度日志 | 有(逐对象一行) | 无(只在过程中看 Illustrator,结束时弹总数) |
{i4} 变量 |
有 | 无(只做到 {i3}) |
原因就是它的定位:兜底。真要用到按图形导出、格式转换、过滤这些, 说明你在批量处理一批不太规整的素材 —— 那种活本来就该用 Python 版 (或者干脆挂到命令行里跑)。
有一处它反而更稳
exportGroup 里量边界的那段:
var bb = nd.pageItems[i].controlBounds; // [l, t, r, b]
JSX 里 controlBounds 是普通属性,nd.pageItems[i] 直接能读。
Python 走 COM 时必须 list(it.ControlBounds) 转一下 —— COM 返回的是一个
代理对象,不转成 Python 列表,后面 min()/max() 会得到意料之外的结果。
另外 JSX 的 controlBounds 和 COM 的 ControlBounds 语义完全一致
(都尊重裁剪蒙版),所以两条路导出的图是同一张图 —— 这一点在
「Python 版导出结果和 JSX 版对不上」的排查里很关键:如果对不上,
先看是不是下标差 1,而不是去怀疑裁剪逻辑。