{"id":44933283,"url":"https://github.com/nrontsis/boox-note-optimizer","last_synced_at":"2026-02-26T12:07:35.283Z","repository":{"id":339088001,"uuid":"1160423981","full_name":"nrontsis/boox-note-optimizer","owner":"nrontsis","description":"Boox note optimizer","archived":false,"fork":false,"pushed_at":"2026-02-17T23:27:46.000Z","size":429,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-02-18T09:43:06.469Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/nrontsis.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-02-17T23:19:45.000Z","updated_at":"2026-02-17T23:27:49.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/nrontsis/boox-note-optimizer","commit_stats":null,"previous_names":["nrontsis/boox-note-optimizer"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/nrontsis/boox-note-optimizer","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nrontsis%2Fboox-note-optimizer","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nrontsis%2Fboox-note-optimizer/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nrontsis%2Fboox-note-optimizer/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nrontsis%2Fboox-note-optimizer/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/nrontsis","download_url":"https://codeload.github.com/nrontsis/boox-note-optimizer/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nrontsis%2Fboox-note-optimizer/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29858465,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-26T08:51:08.701Z","status":"ssl_error","status_checked_at":"2026-02-26T08:50:19.607Z","response_time":89,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-02-18T06:32:58.890Z","updated_at":"2026-02-26T12:07:35.275Z","avatar_url":"https://github.com/nrontsis.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Boox Onyx `.note` file optimizer — debloats and previews .note files\n\n✨ **[Try the Web App Live Here!](https://nrontsis.github.io/boox-note-optimizer)** ✨\n\nThis web app is a PWA with offline support. A service worker (`web/sw.js`) caches the app shell (HTML, JS, WASM, icons) on install using a cache-first strategy for local assets and network-first for CDN resources. The `demo.note` file is excluded from caching due to its size. \n\u003e [!IMPORTANT]\n\u003e Bump the `CACHE_NAME` version in `sw.js` when deploying changes to force clients to re-fetch.\n\n### Android APK\n\nA pre-built APK is available in [GitHub Releases](https://github.com/nrontsis/boox-note-optimizer/releases). Install it on your Boox device for the best experience:\n\n- **Open in Notes**: After optimization, tap \"Open\" to launch the result directly in the Boox Notes app\n- **Receive shared files**: Share a `.note` file from the Notes app to Note Optimizer\n- **Offline**: Works offline after first load (the web app is cached by the service worker)\n\nExported files are saved to `Downloads/Note Optimizer/` and automatically cleaned up on each export.\n\nTo build from source (Docker, no local Android SDK needed):\n\n```bash\ncd android\n./build.sh              # generates keystore on first run, outputs NoteOptimizer.apk\n```\n\n## Overview of the document\n\nA `.note` file is a ZIP archive produced by Boox/Onyx e-ink tablets. It stores handwritten strokes as sequences of pressure/tilt-sensitive points, plus per-stroke metadata (pen type, color, thickness, transform) in protobuf. The rendering model is pen-type-dependent: some pens produce constant-width line segments, others produce pressure-modulated variable-width strokes, filled polygons, raster textures, or scanline fills.\n\nThis document describes the file format and rendering rules inferred from examining `.note` files and comparing against device-exported PDFs.\n\n## Boox `.note` File Format\n\nInferred from multiple test files and cross-referenced against device-exported PDFs. The following related repos were particularly helpful:\n- https://github.com/RobertCsordas/OnyxNoteRenderer\n- https://github.com/hhornbacher/boox-note-parser\n\n\u003e [!WARNING]\n\u003e Details might vary across firmware versions — this repo was only tested with Note Air 5c files generated with latest firmware as of 2026/02/18.\n\n### ZIP Structure\n\nA `.note` file is a ZIP archive. It can contain either a single note or multiple notes.\n\n**Single-note archive** — the most common format from device export. Contains `note/pb/note_info` with the note's metadata. All paths are rooted at `\u003cnoteId\u003e/`:\n\n```\n\u003cnoteId\u003e/\n├── note/pb/note_info                                              # protobuf: note metadata\n├── template/json/\u003cpageUUID\u003e.template_json                         # JSON: page template config\n├── virtual/doc/pb/\u003cnoteId\u003e                                        # protobuf: document model\n├── virtual/page/pb/\u003cpageUUID\u003e                                     # protobuf: virtual page model\n├── document/\u003cnoteId\u003e/template/json/\u003cpageUUID\u003e.template_json       # duplicate template config\n├── pageModel/pb/\u003cpageModelUUID\u003e                                   # protobuf: page model entry\n├── resource/pb/\u003cresourceUUID\u003e#\u003ctimestamp\u003e                         # binary: embedded resources\n├── point/\u003cpageUUID\u003e/\u003cpageUUID\u003e#\u003cpointsDocUUID\u003e#points             # BINARY: stroke point data\n├── shape/\u003cpageUUID\u003e#\u003cshapeDocUUID\u003e#\u003ctimestamp\u003e.zip                # nested ZIP → protobuf: stroke metadata\n├── extra/pb/extra                                                 # protobuf: extra metadata\n└── stash/                                                         # undo history (safe to drop)\n    ├── shape/                                                     # current undo buffer\n    └── archivedShape/\u003ctimestamp\u003e/\u003cpageUUID\u003e#\u003cshapeUUID\u003e#\u003cts\u003e.zip  # archived undo entries\n```\n\n**Multi-note archive** — contains a `note_tree` file in the root with protobuf metadata for all notes. Each note's files are nested under `\u003cnoteId\u003e/` with the same structure as above.\n\n**Key UUID cross-references:**\n- `pageUUID` — appears in point path, shape path, template filenames\n- `pointsDocUUID` — appears in point path and shape protobuf field 16\n- `shapeDocUUID` — appears in shape path and shape protobuf field 18\n- `noteId` — root folder name, also in virtual/doc and document paths\n- `shapeUUID` — per-stroke ID, appears in both `#points` index and shape protobuf field 1\n\n### `#points` Binary Format\n\nMain stroke data blob. All multi-byte integers are **big-endian**.\n\n```\n┌─────────────────────── HEADER (76 bytes) ───────────────────────┐\n│ u32       : version or page count (observed: 1)                 │\n│ 36B ASCII : pageUUID (may be hyphenated or condensed+space-padded) │\n│ 36B ASCII : pointsDocUUID (always hyphenated)                   │\n├─────────────────────── STROKE DATA (contiguous) ────────────────┤\n│ For each stroke:                                                │\n│   4B       : zero padding (always 0x00000000)                   │\n│   N × 16B  : points (see Point Format below)                   │\n├─────────────────────── INDEX ───────────────────────────────────┤\n│ For each stroke (44 bytes):                                     │\n│   36B ASCII : shapeUUID (matches shape protobuf field 1)        │\n│   u32       : offset (absolute from start of blob, incl header) │\n│   u32       : size (4B pad + N × 16B points)                    │\n│                                                                 │\n│ u32  : index_start_offset (absolute from start of blob)         │\n└─────────────────────────────────────────────────────────────────┘\n```\n\nThe last 4 bytes of the entire blob always point to where the index begins.\n\n**Parsing:** Read the header, then read the last 4 bytes to get the index start offset, parse the index to get stroke UUIDs / offsets / sizes, then parse each stroke's points using those offsets.\n\n### Point Format\n\nEach point is 16 bytes, big-endian (`\u003effBBHI` in [struct](https://docs.python.org/3/library/struct.html) notation):\n\n| Offset | Size | Type   | Field      | Range     | Notes |\n|--------|------|--------|------------|-----------|-------|\n| 0      | 4    | f32 BE | x          | ~0–1860   | Horizontal coordinate (PDF points) |\n| 4      | 4    | f32 BE | y          | ~0–2480   | Vertical coordinate (PDF points) |\n| 8      | 1    | u8     | tilt_x     | 0–255     | Stylus tilt X component, wraps at 256 |\n| 9      | 1    | u8     | tilt_y     | 0–255     | Stylus tilt Y component, wraps at 256 |\n| 10     | 2    | u16 BE | pressure   | 0–4095    | Stylus pressure (hardware max 4095) |\n| 12     | 4    | u32 BE | time_delta | ms        | Time delta from previous point |\n\n**Coordinate system:** Coordinates are in PDF points (1:1 mapping to device-exported PDFs). Despite the 1404×1872 display resolution, the stored coordinates span the full 1860×2480 page. Observed ranges: x ≈ 0–1860, y ≈ 0–2480. Verified by direct comparison of `.note` point coordinates against device-exported PDF path segments.\n\n**Tilt wrapping:** Tilt values wrap at 256 (e.g., 254 → 0 is a +2 change, not -254). Continuous tilt must be unwrapped with modular arithmetic before interpolation.\n\n**Time delta:** Milliseconds since the previous point within the same stroke. The first point's time_delta appears to be an absolute offset.\n\n### Shape Protobuf (inside nested ZIP)\n\n`shape/*.zip` contains another ZIP with a single protobuf file. The top-level message contains repeated field 1 submessages, one per stroke:\n\n| Field | Wire | Type    | Content            | Notes |\n|-------|------|---------|--------------------|-------|\n| 1     | 2    | string  | shapeUUID          | Matches `#points` index UUID |\n| 2     | 0    | varint  | Created timestamp  | Epoch ms |\n| 3     | 0    | varint  | Modified timestamp | Epoch ms |\n| 4     | 0    | varint  | Color              | ARGB u32 (see below) |\n| 5     | 5    | float32 | Thickness          | Line width |\n| 7     | 2    | string  | Bounding box JSON  | `{\"bottom\",\"empty\",\"left\",\"right\",\"stability\",\"top\"}` |\n| 8     | 2    | string  | Transform matrix JSON | `{\"values\":[a,b,tx,c,d,ty,0,0,1]}` — 3×3 row-major affine. Applied to strokes that were moved/scaled on-device. |\n| 11    | 2    | string  | Pen config JSON    | See below |\n| 12    | 0    | varint  | Pen type           | Brush tool identifier (see Pen Types) |\n| 16    | 2    | string  | pointsDocUUID      | Same as `#points` header/path |\n| 17    | 2    | string  | Line style JSON    | `{\"lineStyle\":{\"phase\",\"type\"}}` |\n| 18    | 2    | string  | shapeDocUUID       | Same as shape ZIP path |\n| 20    | 2    | string  | Extra JSON         | Contains `featureCollection` for pen_type=40 geometric shapes. See **Geometric Shapes** section. |\n| 21    | 2    | string  | Unknown            | Observed: `\"[]\"` |\n| 22    | 2    | string  | Rich text HTML     | HTML-formatted text content for text boxes (pen_type=6, 16). |\n| 25    | 2    | bytes   | Point list         | Binary point data for geometric shapes (see **Point List Format** below) |\n| 26    | 2    | string  | Repo JSON          | Observed: `'{\"repo\":{}}'` |\n\n**Color encoding (ARGB u32):**\n```\nAlpha = (color \u003e\u003e 24) \u0026 0xFF\nRed   = (color \u003e\u003e 16) \u0026 0xFF\nGreen = (color \u003e\u003e  8) \u0026 0xFF\nBlue  = (color      ) \u0026 0xFF\n```\n\n**Pen config JSON** (field 11):\n```json\n{\n  \"penType\": 5,\n  \"maxPressure\": 4095.0,\n  \"displayScale\": 0.9435484,\n  \"dpi\": 320.0,\n  \"alphaFactor\": 1.0,\n  \"pressureSensitivity\": ...\n}\n```\nNot all strokes have pen config — a minority lack it entirely. On some devices/firmware versions this field may contain a simpler `displayScale`-only JSON (with `maxPressure`, `revisedDisplayScale`, `source`).\n\n**Ordering:** Shape protobuf submessage order may differ from the `#points` index order. They are cross-referenced by shapeUUID (protobuf field 1 = index UUID).\n\n### Note Metadata Protobuf (`note/pb/note_info` or `note_tree`)\n\nThe note metadata protobuf (field tags from reverse engineering):\n\n| Field | Type    | Content                | Notes |\n|-------|---------|------------------------|-------|\n| 1     | string  | noteId                 | Note UUID |\n| 2     | uint64  | Created timestamp      | Epoch ms |\n| 3     | uint64  | Modified timestamp     | Epoch ms |\n| 6     | string  | Note name              | |\n| 8     | uint32  | Flag                   | |\n| 9     | float   | Pen width              | |\n| 10    | float   | Scale factor           | |\n| 11    | string  | Pen settings JSON      | Detailed pen config with quick pen list |\n| 12    | string  | Canvas state JSON      | Page dimensions, zoom info, layer list per page |\n| 13    | string  | Background config JSON | Page background settings |\n| 14    | string  | Device info JSON       | Device name and screen dimensions |\n| 15    | uint32  | Fill color             | |\n| 16    | uint32  | Pen type               | |\n| 20    | string  | Active pages JSON      | `{\"pageNameList\": [\u003cpageUUID\u003e, ...]}` |\n| 21    | string  | Reserved pages JSON    | Same format as active pages |\n| 22    | float   | Canvas width           | |\n| 23    | float   | Canvas height          | |\n| 24    | string  | Location               | |\n| 44    | string  | Detached pages JSON    | Same format as active pages |\n\nMulti-note archives use a `note_tree` file whose protobuf wraps repeated note metadata messages at tag 1.\n\n### Page Model Protobuf (`pageModel/pb/`)\n\n| Field | Type    | Content         | Notes |\n|-------|---------|-----------------|-------|\n| 1     | string  | pageUUID        | |\n| 2     | string  | Layers JSON     | `{\"layerList\": [{\"id\": N, \"lock\": bool, \"show\": bool}, ...]}` |\n| 5     | uint64  | Created timestamp | Epoch ms |\n| 6     | uint64  | Modified timestamp | Epoch ms |\n| 7     | string  | Dimensions JSON | `{\"top\", \"right\", \"bottom\", \"left\", \"empty\", \"stability\"}` |\n\n### Virtual Doc Protobuf (`virtual/doc/pb/`)\n\n| Field | Type    | Content         | Notes |\n|-------|---------|-----------------|-------|\n| 1     | string  | virtualDocUUID  | |\n| 2     | uint64  | Created timestamp | Epoch ms |\n| 3     | uint64  | Modified timestamp | Epoch ms |\n| 4     | string  | Template UUID   | References a pageUUID |\n| 5     | float   | Stability       | |\n| 9     | string  | Content JSON    | Content ID, page ID, page size, relative path, content type |\n\n### Virtual Page Protobuf (`virtual/page/pb/`)\n\n| Field | Type    | Content          | Notes |\n|-------|---------|------------------|-------|\n| 1     | string  | pageUUID         | |\n| 2     | uint64  | Created timestamp | Epoch ms |\n| 3     | uint64  | Modified timestamp | Epoch ms |\n| 4     | float   | Zoom scale       | |\n| 6     | string  | Dimensions JSON  | Same format as page model |\n| 7     | string  | Layout JSON      | |\n| 8     | string  | Geo JSON         | |\n| 10    | string  | Template path    | |\n| 12    | string  | Page number      | |\n\n### Pen Types\n\nField 12 in the shape protobuf identifies the brush tool. Observed values and their rendering behavior (determined by comparing `.note` stroke data against device-exported PDFs):\n\n| pen_type | Brush Name (approx) | Rendering | Notes |\n|----------|---------------------|-----------|-------|\n| 2  | Ballpoint / Fineliner | Stroked line segments, constant width | Pressure-agnostic. Width = thickness. |\n| 5  | Fountain Pen | Stroked line segments, varying width per segment | Pressure-sensitive. Width derived from pressure per point. |\n| 15 | Highlighter | Stroked line segments, constant width | Very thick. Constant width = thickness. Multiply blend mode at ~50% opacity. |\n| 21 | Marker | Stroked line segments, varying width per segment | Pressure-sensitive, similar to pen_type 5. |\n| 22 | Charcoal | Per-stroke raster image | Tilt-sensitive. See **Charcoal Raster Rendering** section below. |\n| 37 | Fill | Scanline fill rectangles | Points are interleaved scanline pairs: even-indexed = left edge, odd-indexed = right edge. Each pair defines one horizontal fill span. Thickness always 1.0. |\n| 40 | Geometric Shapes | GeoJSON-based rendering | Uses field 20 `extra` JSON with `featureCollection`. See **Geometric Shapes (pen_type=40)** section. |\n| 60 | Calligraphy Brush A | Filled polygon (no stroke) | Tilt-sensitive. Closed filled path (~5x more segments than input points). No per-segment widths. |\n| 61 | Calligraphy Brush B | Filled polygon (no stroke) | Tilt-sensitive. Same as 60 but different fill tessellation. |\n\n**Rendering summary:**\n- **Stroked types** (2, 5, 15, 21): Each point maps to approximately one line segment. Width is either constant (types 2, 15) or derived from pressure (types 5, 21).\n- **Fill type** (37): Points encode a scanline fill — even/odd interleaved pairs define horizontal spans that tile the filled region.\n- **Filled types** (60, 61): The stroke outline is tessellated into a closed polygon. Segment count is much larger than point count (~5x). No per-segment width — the shape is filled.\n- **Raster types** (22): Each stroke is a separate raster image. See **Charcoal Raster Rendering** below.\n- **Text types** (6, 16): Text boxes with plain text (field 10) and/or HTML rich text (field 22). See **Text Boxes** section.\n- **Geometric shapes** (40): GeoJSON-based vector shapes using field 20. See **Geometric Shapes** section.\n\n### Rendering Pipeline\n\nEach stroke is rendered by:\n1. **Looking up metadata** — the shape protobuf provides pen_type, thickness, color (ARGB), and an optional affine transform matrix\n2. **Applying the transform** — if the stroke was moved/scaled on-device, the 3×3 affine matrix (`{\"values\":[a,b,tx,c,d,ty,0,0,1]}`) is applied to each point: `x' = a*x + b*y + tx`, `y' = c*x + d*y + ty`\n3. **Unwrapping tilt** — 8-bit tilt values that wrap at 256 are unwrapped with modular arithmetic to produce continuous angles\n4. **Computing width** — per-point width is computed from pressure and pen-type-specific formulas (see Width Formulas below)\n5. **Drawing** — the pen type determines the drawing primitive (line segments, filled polygon, raster texture, or scanline rectangles)\n\n**Compositing:**\n- Most pen types use normal (source-over) blending at full opacity\n- Highlighter (pen_type 15) uses **multiply** blend mode at ~50% opacity\n- All stroked types use **round** line caps and **round** line joins\n- Strokes are rendered bottom-to-top in creation-timestamp order (field 2 in shape protobuf); later strokes occlude earlier ones\n\n**Color:** Stored as ARGB u32 in protobuf field 4. For most pen types, alpha comes from the ARGB value. The highlighter overrides alpha to ~50%.\n\n### Width Formulas (Pressure-Sensitive Pens)\n\nFitted by comparing `.note` stroke data against device-exported PDF output. Thickness values in `.note` are already in PDF points — no scaling needed.\n\n| pen_type | Formula | Params | RMSE |\n|----------|---------|--------|------|\n| 2 (Ballpoint) | `w = thickness` (constant) | — | exact |\n| 5 (Fountain) | `w = thickness × 1.37 × (pressure/4095)^0.59` | k=1.37, exp=0.59 | 0.063 |\n| 15 (Highlighter) | `w = thickness` (constant) | — | exact |\n| 21 (Marker) | `w = thickness × 2.35 × (pressure/4095)^0.43` | k=2.35, exp=0.43 | 1.207 |\n\nFor variable-width pens (5, 21), each segment uses the average pressure of its two endpoints. Width is clamped to a minimum of 0.5pt.\n\n### Charcoal Raster Rendering (pen_type=22)\n\nCharcoal strokes are **not** rendered as vector paths. On the device, each charcoal stroke is exported as a raster image: a solid-color RGB layer with a binary alpha mask that creates the \"grain\" texture.\n\n**Width envelope:** The charcoal stroke's outline follows the same pressure-dependent variable-width model as the fountain pen: `w = thickness × 1.37 × (pressure/4095)^0.59`, rendered as a filled polygon (same `fill_stroke_outline` approach as calligraphy).\n\n**Texture characteristics:**\n- The alpha mask forms a scattered dot pattern along the stroke path — sparse individual pixels with gaps between them\n- Density varies along the stroke, roughly correlating with pressure\n- The pattern resembles charcoal on textured paper — not a solid filled path\n- `tilt_x` encodes pen azimuth (wraps near 0/255); `tilt_y` encodes elevation (narrow range)\n- The exact algorithm mapping (position, tilt, pressure) → pixel mask is unknown, but the texture appears to be a deterministic scattered pattern\n\n**Rendering approach:**\nCharcoal is approximated with procedural stippling: a 64×64 tiled grain pattern (solid color with ~30% of pixels erased) is used as a `CanvasPattern` fill inside the stroke's variable-width polygon outline. The pattern's RNG is seeded per-stroke (from UUID hash) for deterministic output. This produces a visually similar scattered grain effect without reverse-engineering the exact device algorithm.\n\n### Calligraphy Brush Rendering (pen_type=60, 61)\n\nSee **[calligraphy.md](calligraphy.md)** for the detailed chisel-tip model, width formula, smoothing pipeline, and known limitations.\n\n### Templates \u0026 Backgrounds\n\nPages can have templates (ruled lines, grids, dot grids) and background images.\n\n**Templates** are stored at `template/json/\u003cpageUUID\u003e.template_json`. The JSON contains:\n```json\n{\n  \"layoutType\": \"LayoutResVector\",\n  \"properties\": {\n    \"resourceAttr\": { \"resName\": \"template/\u003ctemplateName\u003e\" },\n    \"spacing\": 68.0,\n    \"shaderRect\": { \"left\": 0, \"right\": 1860, \"top\": 0, \"bottom\": 2480 }\n  }\n}\n```\nThe template SVG is fetched from the Boox CDN at `https://static.send2boox.com/device/note/template/{templateName}.svg`. Observed template names: `ic_horizontal_line_24` (ruled lines), `new_scribble_back_ground_grid_point` (dot grid).\n\n**Backgrounds** are stored in the `note_info` protobuf field 13 as a JSON string:\n```json\n{\n  \"useDocBKGround\": true,\n  \"docBKGround\": { \"type\": 1, \"resId\": \"\u003cresourceUUID\u003e\" },\n  \"pageBKGroundMap\": { \"\u003cpageUUID\u003e\": { \"type\": 1, \"resId\": \"\u003cresourceUUID\u003e\" } }\n}\n```\nBackground type `1` = image file. The `resId` references a resource in `resource/pb/`. Per-page overrides are in `pageBKGroundMap`.\n\n### Text Boxes (pen_type=6, 16)\n\nText boxes are shape entries with pen_type 6 or 16. They may appear in the `#points` index (with 2 points defining the bounding box corners) or only in the shape protobuf.\n\n**Content fields:**\n- Field 10: plain text content\n- Field 22: HTML rich text (e.g. `\u003cp\u003e\u003cspan style=\"...\"\u003etext\u003c/span\u003e\u003c/p\u003e`)\n- Field 9: text style JSON with formatting properties:\n  ```json\n  { \"textSize\": 32, \"textBold\": false, \"textItalic\": false, \"alignType\": 0, \"textSpacing\": 1.2 }\n  ```\n  `alignType`: 0=left, 1=center, 2=right.\n\n**Positioning:** From bounding_rect (field 7) or from 2 points in the `#points` data defining opposite corners of the text box.\n\n### Geometric Shapes (pen_type=40)\n\nGeometric shapes drawn with the Boox shape tools (lines, arrows, polygons, ovals, curves, brackets, etc.) use pen_type=40 and store their geometry in protobuf field 20 (`extra`) instead of field 25 (`pointList`).\n\n**Field 20 format:** A JSON string containing a `featureCollection` key whose value is itself a JSON string in GeoJSON-like format:\n```json\n{\n  \"featureCollection\": \"{\\\"type\\\":\\\"FeatureCollection\\\",\\\"features\\\":[...]}\"\n}\n```\n\n**Coordinate system:** Coordinates in the geometry are in local space. The matrix (field 8) transforms local → page coordinates: `x' = a*x + b*y + tx`, `y' = c*x + d*y + ty`. The matrix can include Y-flips (negative d), scaling, and rotation.\n\n**Geometry types observed in `.note` files:**\n\n| geometry.type | subType | coords format | Rendering |\n|---|---|---|---|\n| LineString | \"\" | `[[x0,y0],[x1,y1]]` | Straight line |\n| LineString | WaveLine | `[[x0,y0],[x1,y1]]` | Sine wave between endpoints |\n| DirectionLine | \"\" | `[[x0,y0],[x1,y1]]` | Line with arrowhead at end |\n| BidirectionalLine | \"\" | `[[x0,y0],[x1,y1]]` | Line with arrowheads at both ends |\n| MultiLineString | \"\" | `[[[x0,y0],[x1,y1]], ...]` | Multiple line segments (used for arrow head lines) |\n| Polygon | \"\" | `[[[start,end], ...]]` | Pairs of segment endpoints forming a closed ring |\n| MultiPoint | Oval | `[[x0,y0],[x1,y1]]` | Bounding box → ellipse |\n| MultiPoint | Curve | `[[start],[control],[end]]` | Quadratic Bezier curve |\n| MultiPoint | Arc | `[[bboxMin],[bboxMax],[angleCtrl]]` | Elliptical arc within bounding box. 3rd point x=0 → upper half, x=180 → lower half |\n| MultiPoint | Bracket | `[[tip],[topEnd],[bottomEnd]]` | Bracket/brace: tip is the apex, topEnd/bottomEnd are the open ends |\n| FeatureCollection | Surface | nested `features[]` | Recurse into sub-features. Used for compound shapes: 3D solids (cube, pyramid, cylinder), shapes with hidden edges |\n\n**Polygon coordinate format:** Unlike standard GeoJSON, polygon coordinates are stored as pairs of `[start_point, end_point]` for each edge segment, not as a simple vertex list. The first point of each pair forms the polygon vertex.\n\n**SubType location:** The geometry subtype (Oval, Arc, Curve, Bracket, WaveLine, Surface) is stored in `feature.properties.subType`, not on the geometry object itself.\n\n**Styling:** Color from field 4, thickness from field 5. Individual features may have a `strokeAttr` object with `lineWidth` and `color` overrides.\n\n**Dashed lines:** Features can specify `lineStyle: {\"dashLineIntervals\": [8.0, 5.0], \"phase\": 0.0, \"type\": 1}` for dashed rendering. Used for hidden edges in 3D shapes.\n\n**WaveLine properties:** WaveLine features include `waveAttr` in `feature.properties`: `{\"wavyLength\": 24.0, \"wavyPeak\": 6.0, \"wavyOffset\": 0.0}` controlling wavelength, amplitude, and phase offset.\n\n### Point List Format (field 25)\n\nUsed by geometric shapes with pen_type 0 (ellipse), 1 (rectangle), 7 (line), 8/10–12/17/18/24/26/27 (polygons), 28 (arrow), 31 (polyline). Binary format: 4-byte header followed by 16-byte records (same layout as stroke points: x:f32 BE, y:f32 BE, then 8 bytes of size/pressure/event_time). Only x,y are used for shape geometry.\n\n### Page Geometry \u0026 Coordinate Mapping\n\nThe page size is **1860 x 2480 points** (approximately 25.83 x 34.44 inches at 72 DPI). Coordinates in `.note` files map 1:1 to device-exported PDF coordinates — no scaling is needed.\n\n### Stroke Z-Ordering\n\nStrokes are rendered bottom-to-top sorted by creation timestamp (shape protobuf field 2). Within a page, the `#points` index order may differ from creation order; the shape metadata's timestamp is the authoritative sort key. Later strokes render on top of earlier ones.\n\n**Fill-first ordering:** Fill strokes (pen_type 37) are rendered before all other strokes, regardless of timestamp. This ensures fills appear behind the outline strokes that border them, matching the device's observed rendering behavior.\n\n### Stash (Undo History)\n\n`stash/` contains undo history (~46% of total file size in typical files). Safe to drop entirely — the device does not require it for rendering. The debloater strips this directory on export.\n\n### Decimation (Optimization) Algorithm\n\nThe optimizer reduces file size by removing redundant points from strokes using a modified **Ramer-Douglas-Peucker** algorithm that operates in 5 dimensions: spatial (x, y) plus attribute (pressure, tilt_x, tilt_y). The cost of removing a point is the maximum of:\n\n- **Spatial deviation**: perpendicular distance from the point to the line segment connecting its neighbors\n- **Attribute deviation**: maximum interpolation error across pressure and tilt channels (scaled by user-configurable equivalence factors)\n\nPoints at sharp turns (\u003e30° angle change, i.e. `cos(angle) \u003c 0.866`) are never removed. First and last points of each stroke are always preserved. A priority queue processes points in ascending cost order, updating neighbor costs after each removal. The threshold parameter controls the quality/size tradeoff.\n\n### Older Backup Format (different!)\n\nThe [OnyxNoteRenderer](https://github.com/RobertCsordas/OnyxNoteRenderer) project handles a **different** format: SQLite-based `.note` backup files where points are `Nx6 float32` (byteswapped) in a `NewShapeModel` table, with coordinates in normalized 0–1 range. This is the cloud backup/export format, not the same as standalone `.note` ZIP files from the device.\n\nKey differences from the standalone format:\n- SQLite database vs ZIP archive\n- Normalized 0–1 coordinates vs device-unit coordinates\n- 6 floats per point (x, y, pressure, ?, ?, ?) vs 16-byte packed struct\n- `shapeType = 5` used for pressure-sensitive rendering in the backup renderer\n\n### Rendering Comparison Tool (`compare.py`)\n\nA CLI tool for testing rendering accuracy against gold reference PNGs (device-exported screenshots or PDF rasterizations). Requires [uv](https://docs.astral.sh/uv/) — dependencies are declared inline via PEP 723.\n\n**Subcommands:**\n\n```bash\n# Render a .note file to PNG via headless Chromium\nuv run compare.py render shapes.note [-o rendered.png] [--page 0]\n\n# Compare two existing PNGs (gold vs rendered)\nuv run compare.py diff gold.png rendered.png [--note shapes.note] [--non-overlapping] [-o diff.png]\n\n# Render + compare in one step (most common)\nuv run compare.py check shapes.note shapes.png [--non-overlapping] [-o diff.png] [--page 0]\n```\n\n**How it works:**\n1. Spins up a local HTTP server serving the `web/` directory\n2. Uses Playwright (headless Chromium) to load `headless.html`, which initializes the WASM renderer\n3. Passes the `.note` file as base64 to `window.renderNote()`, captures the canvas as a PNG\n4. Compares rendered vs gold pixel-by-pixel, computing MAE (mean absolute error), max error, and percentage of differing pixels\n5. If a `.note` file is provided, extracts per-shape bounding boxes from the protobuf and reports per-region metrics\n6. Outputs a diff visualization with errors amplified 4× and bounding boxes outlined in green (MAE \u003c 1) or red\n\n**Options:**\n- `--non-overlapping`: Only report metrics for shapes whose bounding boxes don't overlap with any other, avoiding ambiguous attribution of errors\n- `--page N`: Render page N (0-indexed, default 0)\n- `-o FILE`: Output path for the diff image\n\n**Output example:**\n```\nOverall: MAE=1.64  Max=255  Diff pixels: 42,139/4,612,800 (0.91%)\n\nPer bounding box (15 regions):\n  [ 841, 310 → 1174, 615] pen=40  MAE=  7.40  Max=255  diff=5.2%  DIFF\n  [ 100, 200 →  400, 500] pen=40  MAE=  0.45  Max= 12  diff=0.3%  OK\n```\n\n### Open Questions\n\n1. **Header u32**: Version number or page count? Only value `1` observed.\n2. **Bounding boxes**: Must they be updated when points change, or does the device recompute? (Currently we do not update them and the device accepts the file.)\n3. **Pen type completeness**: 10 values observed (2, 5, 6, 15, 16, 21, 22, 37, 40, 60, 61). The Boox app offers additional brush tools (pencil, etc.) whose pen_type integers have not yet been observed in test files.\n4. **Pen config absence**: Why do some strokes (~12%) lack pen config JSON (field 11)? Possibly firmware version dependent. On some devices/firmware versions this field may contain a simpler `displayScale`-only JSON (with `maxPressure`, `revisedDisplayScale`, `source`).\n5. **Charcoal texture algorithm**: The exact device algorithm for (position, tilt, pressure) → pixel mask is unknown. Our procedural stipple approximation is visually similar but not pixel-identical. Tilt_x encodes pen azimuth (256 units = full circle, typically 0–25 range); tilt_y encodes elevation (typically 15–33 range).\n6. **Velocity effects on width**: Velocity is not stored in `.note` point data (only `time_delta` is stored, from which velocity could theoretically be recomputed using inter-point distances). The marker pen's higher RMSE (1.207 vs 0.063 for fountain) may partly reflect unmodeled velocity effects in the width formula.\n7. **Firmware variation**: Format details observed on our device may differ across firmware versions or device models. The [boox-note-parser](https://github.com/hhornbacher/boox-note-parser) project (based on Note Air 4 C, app version 42842) reports some differences in shape protobuf field interpretation — their field 11 contains a simpler `displayScale` JSON and they don't identify fields 4 (color) or 12 (pen_type). These may be firmware-dependent or represent a different interpretation of the same data.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnrontsis%2Fboox-note-optimizer","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnrontsis%2Fboox-note-optimizer","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnrontsis%2Fboox-note-optimizer/lists"}