工具矢量图分组导出工具

矢量图分组导出工具

驱动本机 Illustrator,把 .ai 矢量文件里编好组的图形批量拆成透明 PNG —— 一个分组一张图,300 PPI,自动裁到图形边界。已打包成免安装 EXE。

已完成2026/9/22 更新工具Illustrator批量导出Python已完成

下载这个工具主程序,双击即用,无需安装 Python

一句话

把 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:

程序图标在 5 档尺寸下的样子,上下两行分别是浅色底与深色底

快速上手(EXE)

  1. 双击 ai_png_exporter.exe,先读面板顶部那段「使用说明(导出前必读)」
  2. 选「单个文件」或「文件夹(批量)」→「浏览…」选中 .ai
  3. 点「分析文件」可以先看它认出了多少个对象(会临时启动 Illustrator,看完自动关)
  4. 在「导出参数」里选导出范围:按分组(默认)/ 按单个图形
  5. 设分辨率、留白、格式、输出目录、命名模板
  6. 点「开始导出」,日志区实时滚动(每个对象一行,带导出后的像素尺寸)

界面(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 张图,按导出顺序排开。 每一张都是透明背景、各自裁到自己的图形边界:

40 张导出成果拼版:斜条纹、箭头、粗箭形、栅格、二维码、靶心、多边形等黑色几何图形

单独看几张的成品质量 —— 下面这几张的浅色底只是为了让透明 PNG 看得清, 文件本身没有背景:

导出成品:斜条纹与嵌套箭头组成的方形图形
导出成品:同款图形旋转 45° 后的版本
导出成品:同心圆靶心
导出成品:黑色几何图形,透明背景

同一个文件里图形的比例差得很远 —— 有 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)         # 无论成败都关掉,别攒一堆临时文档

四个细节值得留一句:

  1. app.Selection = [g] 必须是列表。传裸对象 g 在部分 Illustrator 版本上 对散落的 PathItem 会静默复制失败,然后你得到一张空白图。代码里做了三级兜底 (列表 → 裸对象 → g.Selected = True),任何一级成功就继续。
  2. 边界取 ControlBounds 而不是 VisibleBounds。前者把裁剪蒙版算进去, 后者会把被蒙版裁掉的部分也算进包围盒 —— 用后者导出的图四周会多出一圈透明留白。
  3. 裁的是新建文档的第 1 个画板,不是源文档的。源文件是几块画板都不影响: 多画板文件以第 1 块为基准,每个分组各成一张图,互不干扰。
  4. 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,然后:

  1. 弹窗选「输入文件夹」(里面放 .ai)
  2. 弹窗选「输出文件夹」
  3. 跑完弹一个「导出完成:成功 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,而不是去怀疑裁剪逻辑。