PDF 高清压缩 Skill
完整交互流程
第一步:介绍 + 判断账号类型
先用文字介绍,再带出问题——不要直接弹问题,要先说:
figma-to-pdf 帮你把 Figma 设计稿导出成可以控制大小的高清 PDF。
你的 Figma 账号是哪种类型?
然后用 AskUserQuestion 工具弹出选项:
问题:你的 Figma 账号类型?
选项:
- Professional / Organization / Enterprise
→ 描述:支持从 Figma 链接直接导出,也可以用本地文件夹
- Starter(免费版)
→ 描述:需要先在 Figma 手动导出图片,再给我文件夹路径打包
第二步:判断图片来源(仅 Professional 以上账号)
若用户选择 Professional 以上账号,再用 AskUserQuestion 工具问:
问题:图片从哪里来?
选项:
- 从 Figma 链接导入(推荐)
→ 描述:粘贴 Figma 文件链接,自动按画布顺序导出所有 Frame
- 从本地文件夹导入
→ 描述:已在 Figma 手动导出图片,给我文件夹路径直接打包
- 选"从 Figma 链接导入"→ 进入模式 B
- 选"从本地文件夹导入"→ 进入模式 A(跳过手动导出引导,直接问路径)
- Starter 账号 → 进入模式 A(显示手动导出引导)
模式 A:本地文件夹
A-1. 引导导出(仅免费账号显示)
若用户是免费账号,先引导手动导出:
在 Figma 里手动导出步骤:
- 选中画布上所有要导出的 Frame(Ctrl/Cmd+A 全选,或逐个点选)
- 右侧面板底部找到 Export
- 格式选 PNG,Scale 选 2x
- 点 Export [n] layers,选择保存文件夹
- 导出完成后把文件夹路径告诉我
付费账号选了本地文件夹,直接跳到 A-2。
A-2. 询问路径
请提供图片文件夹路径(直接粘贴或把文件夹拖进终端)
A-3. 采样估算大小
扫描文件夹,均匀取 5 张图(第 1、1/4、1/2、3/4、最后一张)分别用 3 种 quality 压缩,按总页数推算预估体积:
imgs = sorted(glob.glob(f"{src_dir}/*.jpg") + glob.glob(f"{src_dir}/*.png"),
key=lambda p: int(re.sub(r'\D', '', os.path.basename(p)) or 0))
n = len(imgs)
sample_indices = list({0, n//4, n//2, 3*n//4, n-1})
samples = [imgs[i] for i in sample_indices]
for quality in [92, 85, 75]:
total = 0
for path in samples:
img = Image.open(path).convert("RGB")
img.save(tmp, "JPEG", quality=quality)
total += os.path.getsize(tmp)
avg = total / len(samples)
estimated_mb = avg * n / 1024 / 1024
A-4. 询问目标大小
用 AskUserQuestion 工具弹出选项(填入采样估算值):
问题:目标文件大小?
选项:
- 高清(quality=92)→ 约 XX MB,几乎无损,适合本地存档
- 均衡(quality=85)→ 约 XX MB,肉眼无差别,适合邮件/微信(推荐)
- 压缩(quality=75)→ 约 XX MB,深色背景有色带风险,适合平台上传
- 自定义:我来指定目标大小
若用户选"自定义",回复:
请输入目标大小(如 15MB),我自动调参命中。
收到目标值后,根据采样数据估算对应 quality,生成后若偏差超过 1 MB 则微调再生成,直到落入目标范围。若目标值低于 quality=75 能达到的大小,提示用户有色带风险并询问是否继续。
A-5. 询问保存路径
在开始压缩前用 AskUserQuestion 弹出选项:
问题:PDF 保存到哪里?
选项:
- 桌面(默认)
→ 描述:保存到 ~/Desktop/
- 自定义路径
→ 描述:选择后在下方 Other 框内填入文件夹路径
A-6. 执行
- 读取所有 JPG/PNG,按文件名数字排序
- 按选定 quality 压缩,动态计算每张图 DPI 嵌入元数据,用 img2pdf 打包,直接写入目标路径
- 报告最终大小,用
open命令打开文件,完成确认
模式 B:Figma 直接导出
前提:需要 Figma Professional 以上付费账号 + Full/Dev 席位。
免费 Starter 账号的 /v1/images 导出限额为 6次/月,无法支持大文件导出,请使用模式 A。
B-1. 收集链接与 Token
请提供 Figma 链接,支持三种类型:
- 整个文件:导出文件内所有 Page 的顶层 Frame
- 某个 Page(推荐):只导出该 Page 下的顶层 Frame
- 某个 Frame / Section:只导出该 Frame 或 Section 内的内容
推荐粘 Page 级别的链接——在 Figma 左侧切换到目标 Page,复制地址栏链接即可。
⚠️ 注意:Page 内不能有多余的 Frame,所有顶层 Frame 都会被导出。请确认画布里只保留要打包的页面。
⚠️ 导出顺序按画布从左到右排列,请先在 Figma 里确认 Frame 的横向摆放顺序正确。
Token 获取逻辑:
Step 1:检查环境变量
TOKEN="$FIGMA_TOKEN"
- 若有,直接使用,跳过以下步骤
- 若无,引导用户生成 token
Step 2:引导生成 Token(环境变量未配置时显示)
需要先生成一个 Figma Personal Access Token,步骤如下:
- 打开 Figma,点右上角头像 → Settings
- 顶部选 Security
- 滚动到 Personal access tokens → 点 Generate new token
- 名字随便填(如
pdf-export)- 权限只勾选 File content → Read-only,其余不用勾
- 过期时间按需选(建议 90 天或更长)
- 点 Generate,复制 token(只显示这一次,复制后再关闭)
生成好了把 token 粘给我。
Step 3:询问是否永久保存(用户粘 token 后)
用 AskUserQuestion 工具弹出选项:
问题:要把 token 保存到 ~/.zshrc 吗?以后不用再粘。
选项:
- 保存(推荐):写入 ~/.zshrc,长期有效
- 不保存:只用这一次
若选保存,执行:
echo 'export FIGMA_TOKEN="figd_xxxx..."' >> ~/.zshrc
Token 失效处理(API 返回 403):
若导出时遇到 {"status": 403, "err": "Invalid token"},提示用户:
Token 已失效或过期,请重新生成一个。
生成步骤:
- 打开 Figma,点右上角头像 → Settings
- 顶部选 Security
- 滚动到 Personal access tokens → 点 Generate new token
- 名字随便填(如
pdf-export)- 权限只勾选 File content → Read-only,其余不用勾
- 过期时间按需选(建议 90 天或更长)
- 点 Generate,复制 token(只显示这一次,复制后再关闭)
生成好了把新 token 粘给我,我同时帮你更新 ~/.zshrc。
B-2. 读取 Frame 列表并确认顺序
调用 /v1/files/:file_key API,获取所有顶层 Frame 及其 absoluteBoundingBox,按画布 X 坐标升序排列,展示给用户确认:
找到 N 个 Frame,按画布从左到右排列如下:
- Cover
- Problem ...
顺序有问题吗?没问题直接回"确认",需要调整告诉我。
若用户要调整:按用户指定的顺序重新排列,再次展示确认后进入下一步。
B-3. 分批导出
/v1/images 每次调用都触发服务端渲染,帧越大越容易超时。固定每批 5 个,超时自动降级重试。
导出逻辑(用 bash curl 执行):
curl -s "https://api.figma.com/v1/images/{file_key}?ids={ids}&scale=2&format=jpg" \
-H "X-Figma-Token: {token}" -o batch.json
错误处理逻辑:
读取 batch.json 的 err 字段:
err == "Rate limit exceeded"
→ 停止导出,提示用户:
"已触发 Figma API 限速。限速规则取决于文件所在的位置,不只是账号类型——
即使是 Pro 账号,文件如果放在个人草稿(Drafts)里,API 仍按 Starter 限额计算(约每分钟 10 次)。
解决方案:
1. 把 Figma 文件移到 Pro 团队项目里,限额提升到每分钟 50 次,再重试
2. 或改用模式 A:在 Figma 里手动导出帧为 PNG 2x,给我文件夹路径直接打包"
err == "Render timeout, ..."(或其他 err)
→ 缩小本批次:5 个 → 重试 3 个 → 重试 1 个
→ 1 个还超时:跳过该帧,继续下一个,最后告知用户哪些帧跳过了
err == null
→ 成功,继续下一批
node_ids 格式:冒号编码为 %3A,逗号不编码,例如 5%3A544,5%3A629
B-4. 下载图片
下载所有图片到 /tmp/figma_export_<timestamp>/,按序号命名(01.jpg、02.jpg…)。
B-5. 采样估算 + 询问目标大小
图片下载完成后,均匀取 5 张采样估算(同模式 A 的采样逻辑),再用 AskUserQuestion 弹出选项:
问题:目标文件大小?
选项:
- 高清(quality=92)→ 约 XX MB,几乎无损,适合本地存档
- 均衡(quality=85)→ 约 XX MB,肉眼无差别,适合邮件/微信(推荐)
- 压缩(quality=75)→ 约 XX MB,深色背景有色带风险,适合平台上传
- 自定义:我来指定目标大小
若用户选"自定义",回复:
请输入目标大小(如 15MB),我自动调参命中。
B-6. 询问保存路径
在开始压缩前用 AskUserQuestion 弹出选项:
问题:PDF 保存到哪里?
选项:
- 桌面(默认)
→ 描述:保存到 ~/Desktop/
- 自定义路径
→ 描述:选择后在下方 Other 框内填入文件夹路径
B-7. 压缩打包与收尾
- 同模式 A 的压缩逻辑,动态计算每张图的 DPI,直接写入目标路径
- 报告最终文件大小,用
open命令打开 PDF,完成确认 - 清理
/tmp/figma_export_<timestamp>/临时目录
技术实现
依赖检查
pip3 install --break-system-packages Pillow img2pdf -q
压缩打包代码模板
import img2pdf, os, re, time, glob
from PIL import Image
TARGET_WIDTH_INCHES = 10 # 所有页面统一宽度:10 英寸(254mm),适合屏幕阅读
def compress_and_pack(img_paths, dst, jpeg_quality=85):
tmp_dir = f"/tmp/portfolio_compress_{int(time.time())}"
os.makedirs(tmp_dir, exist_ok=True)
compressed = []
for i, src in enumerate(img_paths):
img = Image.open(src)
if img.mode != "RGB":
img = img.convert("RGB")
# 根据每张图的实际宽度动态计算 DPI,使所有页面宽度一致
effective_dpi = img.width / TARGET_WIDTH_INCHES
out = os.path.join(tmp_dir, f"{i+1:02d}.jpg")
img.save(out, "JPEG", quality=jpeg_quality, optimize=True, dpi=(effective_dpi, effective_dpi))
img.close()
compressed.append(out)
# 不传 dpi 参数,img2pdf 自动读取每张图嵌入的 DPI
with open(dst, "wb") as f:
f.write(img2pdf.convert(compressed))
for f in compressed:
os.remove(f)
os.rmdir(tmp_dir)
return os.path.getsize(dst) / 1024 / 1024
参数调优策略
按目标大小调整 jpeg_quality:
| 目标 | quality | 说明 |
|---|---|---|
| ~50 MB | 92 | 几乎无损 |
| ~20 MB | 85 | 均衡,推荐默认 |
| ~15 MB | 80 | 可接受 |
| ~10 MB | 75 | 深色渐变背景会出现明显色带,慎用 |
| < 10 MB | 降 quality + 缩小尺寸 | 明显有损 |
quality 建议不低于 75——低于此值深色渐变背景会出现明显色带。
若目标值低于 quality=75 能达到的大小,提示用户:
继续压缩需要缩小图片尺寸,深色区域会有轻微色带,是否继续?
关键原则
- 必须用
img2pdf:零转码嵌入,不产生渐变波浪纹 - DPI 动态计算:根据每张图的实际像素宽度计算 effective_dpi,嵌入 JPEG 元数据,img2pdf 自动读取——所有页面统一 10 英寸宽,高度按各自比例自动适配,不传全局 dpi 参数
- 不用 PyMuPDF / fitz:会重新编码导致色带
- 不用 Ghostscript:破坏复杂布局
- 缩放必须用
Image.LANCZOS - Figma token 不写入项目文件(保存到 ~/.zshrc 作为环境变量是允许的)