Convert Apple iWork documents — Pages (.pages), Numbers (.numbers), and Keynote (.key) — into Markdown. Pure Python, no third-party dependencies.
iwork2md reads the iWork '13+ bundle format directly:
file.pages / file.numbers / file.key (ZIP or directory bundle)
└── Index.zip
└── *.iwa (Snappy-framed Protobuf payloads)
It decodes the non-standard Snappy framing and Protobuf containers, extracts text and media references, and reconstructs modern Numbers tables (row/column grids with strings, numbers, and formulas).
- No dependencies — pure Python 3 standard library (
zipfile,struct,re). Works on any macOS/Linux/Windows box. - Text extraction — Pages body text, Numbers cell/header text, Keynote slide text, speaker notes, and comments.
- Numbers table reconstruction — resolves
Table (6001)→DataList (6005)linkages and rebuilds the full grid, decoding:- strings (field 3)
- numbers (IEEE-754 doubles, field 5 →
f42) - formulas (rendered as
=formula) - empty cells are padded so the layout is preserved
- Robust layout handling — works on both ZIP-bundle files and
.pagesdirectory packages. - Noise filtering — drops locale codes (
en_HK), timezones, month-name lists, and number-format strings that would otherwise pollute the output. - SkillHub-ready — ships with
SKILL.md(slug/version/displayName),LICENSE.txt(MIT), and areferences/FORMAT.mdformat note.
Clone the repo:
git clone https://github.com/slearnAI/iwork2md.git
cd iwork2mdNo pip install required — just run the script. (Optional: chmod +x scripts/iwork2md.py.)
Convert a single file:
python3 scripts/iwork2md.py path/to/document.numbersWrite to a specific output path:
python3 scripts/iwork2md.py path/to/deck.key output.mdIf no output path is given, Markdown is printed to stdout.
Drop the folder into your skills directory:
cp -r iwork2md ~/.qclaw/skills/Then ask naturally: "convert this .pages file to markdown", "extract text from my Numbers sheet", "read that Keynote file" — the skill triggers and runs the bundled converter.
- Pages / Keynote → headings, paragraphs, lists, and a
## Mediasection listing referenced images/video by filename. - Numbers → a
## Tablessection with one Markdown table per sheet, plus any stray text fragments under## Other text.
Example (Numbers):
# My Spreadsheet
## Tables
| 南航積分 | 酒店積分 | 曼谷 |
| --- | --- | --- |
| CZ3062 | 機票 | 稅費 |
| =formula | | |The underlying decoder (scripts/iwa.py) handles three layers:
- Bundle — unzip the
.iwafiles (or walk a directory package). - Snappy framing — each
.iwais a sequence of chunks:1-byte type+3-byte little-endian length+ raw Snappy stream. The raw stream begins with an uncompressed-length varint. - Protobuf container —
varint(archiveInfoLength)+ArchiveInfo{identifier, message_infos[]}+ payload. The messagetypeselects the schema;Table (6001)andDataList (6005)drive table reconstruction.
See references/FORMAT.md for the full field-by-field mapping and the engineering notes behind table extraction.
A self-test decodes a synthetic .iwa round-trip:
python3 scripts/test_iwa.py
# OK: parser decodes synthetic .iwa correctlyMIT © 2026 Stephen Lau