Warning
Vibe Coding Disclaimer: This entire project was developed through Vibe Coding (AI-assisted rapid pair-programming and exploratory development). While the parser has been verified against test corpora, code and architecture choices reflect an experimental AI-driven iteration style. Use at your own discretion!
An independent, open-source, fully typed Python toolkit for inspecting and parsing user-supplied GoodNotes 5 and 6 .goodnotes
archives. It decodes protobuf wire format directly, parses Apple LZ4 framed streams, decodes Troy Hanson TPL memory images, extracts observed RGBA stroke data, and exports documents to JSON and SVG.
This project is not affiliated with, endorsed by, sponsored by, or officially connected to Goodnotes Limited. See LEGAL-NOTICE.md for release and usage notes.
It deliberately does NOT use heuristic float scanning.
Rendering ComparisonMotivation & Under the HoodFeaturesInstallation & SetupCLI UsagePython Library APIDocumentation
| Source Archive | GoodNotes Original (.jpg ) |
This Project SVG Export (.svg ) |
|---|---|---|
Example 1: Handwritten Formulas & Images(
ex1.goodnotes |
||
Example 2: Brush Styles & Stroke Variations(
ex2.goodnotes |
||
Example 3: Multi-Layer Images & Chinese Text(
ex3.goodnotes |
GoodNotes is an amazing note-taking app, but its closed ecosystem has always been a pain point. If you want to export your notebooks while keeping the vector strokes editable, you're pretty much out of luck—you either have to stick with the proprietary .goodnotes
file format or export to a flattened PDF that loses editability. To solve this, I built an open-source parser that can decode .goodnotes
files.
I have zero background in reverse engineering, and decoding this format wasn't easy (I haven't seen many successful projects tackling this). So, I built this entirely through "vibe coding" using LLMs—primarily Gemini 3.1 Pro, Gemini 3.6 Flash, and Claude Sonnet 5.
Here is how the parsing works under the hood:
ZIP & Protobuf: After some analysis, it turns out that.goodnotes
is essentially a ZIP archive. The main stroke data is stored page by page in thenotes/
directory as serialized Protobuf files. Since I didn't have the official.proto
schemas, the project blindly parses the Protobuf via the underlying Wire Format to construct an Abstract Syntax Tree (AST).Apple LZ4: Next, we discovered that some data fields start withbv41
orbv4-
and end withbv4$
. This is a signature for Apple's proprietary Framed LZ4 compression. We successfully decompressed this by maintaining a 64KB sliding history window and using bitwise operations to handle the LZ4 tokens.Troy Hanson's TPL: The decompressed plaintext starts with the magic bytestpl\0
, revealing it as Troy Hanson's TPL format (a C serialization library). By inferring the format strings, we were able to extract the raw stroke data—which consists of discrete points containing pressure values.SVG Ribbons: Finally, to render the strokes with natural variable widths, the parser calculates the normal vectors between adjacent points and applies a sliding average for smoothing. It then pushes the edges outward based on the pressure values, stitching the discrete points into a closed SVG polygon ribbon.
Currently, the project can parse the binary files inside a .goodnotes
archive to extract strokes, text, and other elements, exporting them directly to .svg
or .pdf
. Unlike standard exports from the GoodNotes app itself, this parser completely unlocks the raw data. The ultimate goal is to allow conversions to other open vector formats (like InkML) so users can migrate to other apps and prevent their hard work from being locked in by a single vendor.
For more detailed parsing principles, please refer to the GitHub Wiki.
Protobuf Wire Decoder: Lossless parsing of framed and unframed protobuf messages, preserving unknown fields.** Apple LZ4 & Troy Hanson TPL Decoder**: Decompressesbv41
LZ4 streams and decodes embedded TPL format strings (vuA(v)A(S(uu))...
) into structured stroke points and variable-width ribbons.Stroke & Color Parsing: Direct protobuf trailer decoding afterbv4$
to extract exact RGBA colors and highlighter transparency.Stroke Support: Parses a growing set of dots, lines, curves, pen tools, highlighters, erasers, shapes, and moved/copied elements observed in the test corpus.Page & Background Resolution: Automatic PDF background/MediaBox
dimension detection (A4, Letter, landscape vs. portrait), page ordering, text fragments, and sticky notes (便條紙) content extraction.CLI Tools:gn-inspect
,gn-dump
,gn-diff
,gn-export-json
,gn-export-svg
,gn-export-pdf
.
Fountain Pen, Ballpoint Pen, Brush Pen, Highlighter: Full support with custom colors and line thicknesses.** Text**: Typed text fragments.** Sticky Notes**: Sticky note contents and formatting.** Images**: Embedded images.** Auto-Shapes & Shapes**: Vector shape rendering.** Eraser**: Eraser strokes and line cuts.** Lasso Tool**: Transformed, copied, or moved elements.** PDF-backed Notebooks**: PDF background pages with dimensions (/MediaBox
).
Pencil: Visible output, but pressure sensitivity is incorrect and tilt sensitivity is missing.** Arrows**: Render output is currently highly unstable.** Stickers / Elements**: Certain vector lines/strokes may fail to export.** Audio Recordings**: Not yet implemented.
Using uv:
uv sync
Or standard pip install:
pip install -e .
Run the unit test suite with:
uv run pytest
Note:samples/
is intentionally ignored and is not part of the public source tree. Do not publish.goodnotes
files or extracted assets unless you have permission to redistribute them.
gn-inspect sample.goodnotes
gn-dump sample.goodnotes index.notes.pb
gn-diff before.goodnotes after.goodnotes
gn-export-json sample.goodnotes -o document.json
gn-export-svg sample.goodnotes -o pages-svg
gn-export-svg sample.goodnotes -o pages-svg --pdf
gn-export-pdf sample.goodnotes -o document.pdf
Or via module invocation:
PYTHONPATH=src python3 -m goodnotes_re.cli export-svg sample.goodnotes -o pages-svg
For the full CLI reference (including all flags and batch export examples), see cli.md.
from goodnotes_re import GoodNotesDocument
with GoodNotesDocument.open("sample.goodnotes") as doc:
members = doc.inventory()
pages = doc.pages()
for page in pages:
print(f"Page {page.index + 1}: {page.dimensions.width}x{page.dimensions.height} pt")
print(f"Strokes: {len(page.strokes)}")
for stroke in page.strokes:
print(f" Stroke {stroke.uuid}: color {stroke.color_hex}, alpha {stroke.alpha}, points {len(stroke.points)}")
fragments = doc.text_fragments()
for frag in fragments:
print(f"[{frag.source_path}] {frag.format}: {frag.text}")
for page in pages:
for element in page.elements:
print(element.kind, element.uuid, element.attachment_uuid, element.related_uuids)
| Document | Description |
|---|---|
cli.md |
GitHub WikiLEGAL-NOTICE.md
See also the Contributing Guide.
Important
Legal & Trademark Notice: "Goodnotes" and related names, logos, and marks are the property of Goodnotes Limited. Document Parser for GoodNotes is an independent, community-developed project and is not affiliated with, endorsed by, sponsored by, or officially connected to Goodnotes Limited. For full legal, trademark, privacy, and redistribution details, please read LEGAL-NOTICE.md.
Warning
Vibe Coding 免責聲明:本專案完全採用 Vibe Coding(AI 輔助快速結對程式設計與探索式開發)進行構建。雖然解析器已通過測試樣本驗證,但程式碼結構與架構選擇體現了 AI 驅動的實驗性疊代風格。請自行評估並謹慎使用!
一套獨立、開源且完整型別化的 Python 工具組,用於檢視與解析使用者提供的 GoodNotes 5 與 GoodNotes 6 .goodnotes
封存檔。它可直接解碼 protobuf wire format、解析 Apple LZ4 框架串流、解碼 Troy Hanson TPL 記憶體映像、擷取觀察到的 RGBA 筆跡資料,並將文件匯出為 JSON 與 SVG。
本專案與 Goodnotes Limited 沒有任何關聯、背書、贊助或官方合作關係。 發布與使用注意事項請參閱 LEGAL-NOTICE.md。
本專案刻意不使用啟發式浮點數掃描。
| 原始封存檔 | GoodNotes 原版渲染 (.jpg ) |
本專案 SVG 匯出 (.svg ) |
|---|---|---|
範例 1:手寫公式與插圖(
ex1.goodnotes |
||
範例 2:多款筆刷與色彩筆跡(
ex2.goodnotes |
||
範例 3:多層圖文疊加與中文手寫(
ex3.goodnotes |
GoodNotes 是一個很棒的筆記軟體,但由於其封閉性,除了匯出成封閉的 .goodnotes
專屬檔案,或是會失去編輯能力的 PDF 之外,幾乎沒有其他保留向量筆跡的選擇,因此我開發了一個解析工具來解析 .goodnotes
檔案。
因為我對這類工程沒有經驗,且解析此 .goodnotes
不是那麼容易(目前沒有看到幾個有成功解析出來的專案),因此我使用 vibe coding (主要是 Gemini 3.1 Pro, Gemini 3.6 Flash 及 Claude Sonnet 5) 來完成這個專案。
以下是解析原理:
**壓縮與 Protobuf 盲解:**經過分析,得知.goodnotes
本質上是一個壓縮檔,而主要的筆跡資訊以頁為單位存在notes/
下,各頁面的筆跡資訊存為 Protobuf 序列化檔案。在這個專案中,使用了 Wire Format 的方式來解析,將其構建成抽象語法樹。**Apple LZ4 逆向:**接著,我們發現了部分欄位以bv41
或bv4-
開頭,並且以bv4$
結尾,代表這是經過 Apple LZ4 壓縮的資料,我們利用維護 64KB 的歷史滑動視窗,靠位元運算處理 LZ4 的 token 來將其成功解壓。**TPL 記憶體映像:**解壓後的明文以tpl\0
開頭,因此得知他是 Troy Hanson's TPL 格式,並透過格式字串推導出他的筆跡資訊(帶有壓感的離散點)。**向量幾何重建:**接著,透過計算相鄰兩點間的法向量並進行滑動平均平滑化,再結合壓感值向外推移,就能將離散點縫合成封閉的多邊形,最終輸出為帶有自然粗細變化的 SVG 向量筆跡。
目前,專案可以分析 .goodnotes
中的二進制檔案來獲取筆跡、文字等資訊,並輸出成 .svg
或 .pdf
格式。與直接從 GoodNotes app 匯出不同,本專案因為解析了 .goodnotes
格式,因此未來完全可以將解析出的資料,轉換為其他開源格式(如 InkML),讓使用者的心血不再被單一廠商綁架。
詳細的解析原理可以前往 GitHub Wiki 區查看。
Protobuf Wire Decoder:無損解析有框架與無框架的 protobuf 訊息,並保留未知欄位。** Apple LZ4 與 Troy Hanson TPL Decoder**:解壓縮bv41
LZ4 串流,並將嵌入的 TPL 格式字串解碼為結構化筆跡點與可變寬度筆跡帶。筆跡與色彩解析:解析bv4$
後的 protobuf trailer,以擷取精確 RGBA 顏色與螢光筆透明度。筆跡與幾何元件支援:解析測試樣本中觀察到的單點、直線、曲線、鋼筆/原子筆工具、螢光筆、橡皮擦切口、圖形及移動/複製元素。頁面與背景解析:自動偵測 PDF/MediaBox
尺寸(A4、Letter、橫向與直向)、頁面順序、文字片段與便條紙內容。CLI 工具:gn-inspect
、gn-dump
、gn-diff
、gn-export-json
、gn-export-svg
、gn-export-pdf
。
鋼筆、原子筆、畫筆、螢光筆:可處理不同顏色及粗細。文字:打字文字內容。便利貼:便條紙內容。圖片:內嵌圖片。自動形狀、形狀:幾何圖形。橡皮擦:橡皮擦筆跡與切口。套索工具:選取、移動與變形的元素。有 PDF 的筆記本:PDF 背景與頁面尺寸。
鉛筆:可顯示,但壓感有問題,以及沒有傾斜感知。箭頭:目前輸出極度不穩定。素材(貼紙):可能有部分線條無法輸出。錄音:還沒實作。
使用 uv:
uv sync
或使用標準 pip:
pip install -e .
執行測試:
uv run pytest
注意:samples/
刻意被忽略,且不屬於公開原始碼樹。除非您擁有重新發布的權限,否則請勿發布.goodnotes
檔案或擷取出的資產。
gn-inspect sample.goodnotes
gn-dump sample.goodnotes index.notes.pb
gn-diff before.goodnotes after.goodnotes
gn-export-json sample.goodnotes -o document.json
gn-export-svg sample.goodnotes -o pages-svg
gn-export-svg sample.goodnotes -o pages-svg --pdf
gn-export-pdf sample.goodnotes -o document.pdf
完整 CLI 參考請參閱 cli.md。
from goodnotes_re import GoodNotesDocument
with GoodNotesDocument.open("sample.goodnotes") as doc:
members = doc.inventory()
pages = doc.pages()
for page in pages:
print(f"Page {page.index + 1}: {page.dimensions.width}x{page.dimensions.height} pt")
print(f"筆跡數量: {len(page.strokes)}")
for stroke in page.strokes:
print(f" Stroke {stroke.uuid}: color {stroke.color_hex}, alpha {stroke.alpha}, points {len(stroke.points)}")
fragments = doc.text_fragments()
for frag in fragments:
print(f"[{frag.source_path}] {frag.format}: {frag.text}")
for page in pages:
for element in page.elements:
print(element.kind, element.uuid, element.attachment_uuid, element.related_uuids)
| 文件 | 說明 |
|---|---|
cli.md |
GitHub WikiLEGAL-NOTICE.md
另請參閱 貢獻指南。
Important
法律與商標聲明:「Goodnotes」及相關名稱、標誌與標誌均為 Goodnotes Limited 所有。Document Parser for GoodNotes 是一套獨立且由社群開發的開源專案,與 Goodnotes Limited 沒有任何附屬、背書、贊助或官方合作關係。完整法律、商標、隱私及重新發布注意事項,請參閱 LEGAL-NOTICE.md。