excalidraw-use
Place existing images onto an Excalidraw whiteboard, turn a slide deck into clean per-slide images first, and inspect what is inside a .excalidraw file. Use whenever someone mentions Excalidraw, a whiteboard or canvas, a .excalidraw file, 白板 or 画板 — especially "put these screenshots on my board", "add my old workshop images to the canvas", "space them out so I don't have to adjust spacing by hand", "turn these slides into images I can draw on", or "what's in this scene file". Also use when a talk or workshop is delivered by hand-drawing over screenshots on a shared canvas, or when someone needs a pick tray of many images to choose from. Not for generating a diagram from a text description — that is a different job.
1How It Works & Mechanism
The core trick is packaging a collection of images onto a whiteboard as an independent scene file (a pick tray), rather than directly modifying the board the user is currently working on. Job 1 scales N images proportionally, arranges them centered in a grid, and embeds them into the files map as base64 strings.
The scene is a *pick tray*: open it in a second Excalidraw tab, select what you want, and copy-paste it onto your real board. It is never written into the board file itself — see references/paste_workflow.md for why that matters.
Image dimensions are parsed directly from file headers without relying on external libraries: reading the 8-byte width and height in IHDR for PNG, and scanning segments up to the SOF marker to extract width and height for JPEG.
"""(width, height) in pixels for PNG or JPEG, read from the file's own header."""
Forces an on-disk read-back verification after every write: checks element count, sha1 consistency between fileId and base64 content, aspect ratio match against the original image, and pairwise bounding box non-overlap. Any mismatch results in a SystemExit.
Deliberately re-reads from disk rather than checking the in-memory objects: the failure this catches is a bad write, and an in-memory assert cannot see one.
Deduplication and exclusion are based on content hashes (sha256) rather than filenames, correctly identifying renamed duplicates.
skip = {hashlib.sha256(p.read_bytes()).hexdigest() for p in exclude if p.exists()}
--template-from solves the void where the official JSON schema omits image element fields by deep-copying an authentic image element directly from the user's board to extract the field schema.
for el in scene.get("elements", []): if el.get("type") == "image" and not el.get("isDeleted"): return copy.deepcopy(el)
The delivery pipeline deliberately chooses the clipboard over Open or drag-and-drop: the latter two will entirely overwrite the user's active scene.
The clipboard carries elements *and* their embedded image payloads, so copy-paste merges instead of replacing:
Job 2 captures screenshots without missing content by relying on three pillars: extracting slide id and fragmentCount via regex from source code, performing full page.goto per slide (instead of hash navigation), and checking computed opacity rather than innerText to determine if fragments are actually visible.
// Fragments that never revealed stay in the DOM at opacity 0, so any check // based on text content reports them as present while the picture is blank. // Measure what actually renders.
The two fragment progression modes are treated as mutually exclusive and strictly enforced at the script level: --advance only accepts pdf or keys.
console.error(`--advance must be "pdf" or "keys", got ${ADVANCE}`)
inspect_scene.py in Job 3 performs a read-only health check: element type counts, embedded payload sizes, sha1 alignment between fileId and content, and the occupied extent (used to tell the user where to scroll before pasting).
# Excalidraw keys files by a content hash; when it holds, a # mismatch means the payload was swapped without rekeying.
split_scene.py calculates division before naming chunks based on the actual chunk count (avoiding situations like "1-of-4 without 4-of-4"), and bundles only the files referenced by that chunk so the split provides actual payload reduction.
"files": {e["fileId"]: files[e["fileId"]] for e in part},
2Core Capabilities
3External Dependencies
| Kind | Dependency / Endpoint |
|---|---|
| package | puppeteer(Job 2 无头浏览器,第三方 npm 包,需可被解析,skill 不负责安装) |
| cli | node(执行 shoot_deck.mjs) |
| cli | npx vite build(把 deck 从源码构建到 /tmp/deck-dist) |
| cli | python3 / python3 -m http.server(本地静态服务器) |
| network | http://127.0.0.1:8080(无头浏览器取页地址;注意 http.server 默认监听 0.0.0.0) |
| network | https://excalidraw.com(不是调用点:只作为生成 scene 的 source 字段字符串,以及文档里指向站点的手工操作说明) |
| network | https://docs.excalidraw.com/docs/codebase/json-schema(参考文献里引用的官方文档链接,非运行时代码) |
4Recommended Governance & Evidence
- scripts/: Executes local scripts or commands: excalidraw-use/scripts/build_scene.py (python3, pure standard library)
- 网络端点: Declares external network access: http://127.0.0.1:8080 (local static server for headless browser loading of /tmp/deck-dist; python3 -m http.server)
- 本地工作目录: File I/O and artifact generation: <out>.excalidraw (generated by build_scene.py, containing base64 embedded images; automatically creates parent directory)
5Second-Pass Verification
- [ok] external_deps 每条的调用点是否真实存在 — puppeteer(shoot_deck.mjs 第 1 行 import)、node(shebang)、npx vite build 与 python3 -m http.server(SKILL.md Job 2 命令行)、127.0.0.1:8080(--url 实参)全部实读命中,非推测。
- [discrepancy] excalidraw.com 是否被当作网络端点误报 — 〔第二遍标注:第一遍易误报,已修正〕build_scene.py 与 split_scene.py 里的 https://excalidraw.com 只是写入 scene 的 "source" 字段值,脚本从不请求该域名;README/文档里的链接同理。已在 external_deps 中显式标注『不是调用点』,避免把字符串常量当成外发。
- [discrepancy] exec( 命中是否意味着命令执行 — 〔第二遍标注:扫描器误报,已澄清〕shoot_deck.mjs 的 const frag = /fragmentCount:\s*(\d+)/.exec(m[2]) 是 JavaScript RegExp.prototype.exec,属正则匹配,不执行进程。全目录无 child_process / spawn / execSync / subprocess。
- [ok] 『不写用户现场板子』是否只是口号 — 代码层:build_scene.py 只写 --out 指定路径,从不读入并回写用户的板子文件;文档层:SKILL.md 有独立小节『## Never write into their live board file』并给出两条独立理由(标签页不监听文件、磁盘文件通常旧于浏览器状态)。
- [ok] 写入后校验是否真做(不是文档承诺) — build_scene.py 的 verify() 在 build() 末尾被调用且从磁盘重读;split_scene.py 对每个分片做同样校验。两道都是可执行代码,失败即 SystemExit。
- [unlocatable] python3 -m http.server 绑定范围 — 〔第二遍标注:文档未声明,属解释器默认行为〕SKILL.md 只写 python3 -m http.server 8080,未写 --bind 127.0.0.1;http.server 的默认绑定是 0.0.0.0。这是 CPython 的既定行为而非本 skill 的代码,故标注为「文档未声明」并在 security.reason 中提示,不写成 skill 缺陷。
- [ok] Job 2 的 --advance 互斥是否代码强制 — shoot_deck.mjs 中 if (!['pdf','keys'].includes(ADVANCE)) 报错退出,且 query 只在 ADVANCE === 'pdf' 时拼 ?pdf=1;SKILL.md 有对应警告段,代码与文档一致。
- [ok] 元数据(license/stars/last_push) — 取自同 repo 同 commit 的池内既有值(MIT、1385★、2026-09-09T12:33:29Z);本次未联网复核 gh。
6Verdict & Scenarios
0c4ae44eb4c051d3…