README, ARCHITECTURE.md und FILE_FORMAT.md beschreiben jetzt alle Funktionen bis v0.11 (Text, Bilder, PDF-Import, Seitenformate, Lineal, durchgehendes Scrollen, Musterfarben, Dateiformat-Version 8). Werbliche Formulierungen durch überprüfbare Angaben ersetzt, auch in Paket- beschreibung, Desktop-Datei, AppStream-Metadaten und Release-Notizen. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
174 lines
7.2 KiB
Markdown
174 lines
7.2 KiB
Markdown
# Dateiformat von ink-notes
|
||
|
||
Dieses Dokument beschreibt, wo und wie ink-notes Notizbücher speichert, und die Versionen des Dateiformats. Aktuell ist **Version 8**.
|
||
|
||
---
|
||
|
||
## 1. Ordner
|
||
|
||
```text
|
||
~/.local/share/ink-notes/ ($XDG_DATA_HOME/ink-notes)
|
||
├── notebooks/
|
||
│ ├── <notizbuch-id>/
|
||
│ │ ├── manifest.json Metadaten und Seitenreihenfolge
|
||
│ │ ├── manifest.json.bak Kopie des vorherigen Manifests
|
||
│ │ ├── page_1.json eine Datei je Seite
|
||
│ │ ├── page_2.json
|
||
│ │ ├── page_7.json.corrupt-<ts> beiseitegelegte beschädigte Seite (falls vorgekommen)
|
||
│ │ ├── assets/ eingefügte Bilder und importierte PDFs
|
||
│ │ │ ├── <sha256>.png
|
||
│ │ │ └── <sha256>.pdf
|
||
│ │ └── .lock Sperrdatei, solange das Notizbuch geöffnet ist
|
||
│ └── .trash/ gelöschte Notizbücher (<id>_<zeitstempel>)
|
||
└── recovery/ Notfall-Sicherungen ungespeicherter Seiten
|
||
|
||
~/.config/ink-notes/ui.json ($XDG_CONFIG_HOME) Einstellungen der Oberfläche
|
||
```
|
||
|
||
- **Notizbuch-IDs** werden aus dem Titel und dem Zeitpunkt gebildet (z. B. `mathematik_1775460000`); das erste Notizbuch heißt `default`.
|
||
- **Assets** sind nach dem SHA-256-Hash ihres Inhalts benannt; dasselbe Bild wird nur einmal gespeichert. Bilder werden als PNG oder (unverändert) JPEG abgelegt, PDFs unverändert.
|
||
- Alle Dateien werden mit Rechten `0600`, Ordner mit `0700` angelegt.
|
||
|
||
---
|
||
|
||
## 2. Manifest (`manifest.json`)
|
||
|
||
```json
|
||
{
|
||
"format_version": 8,
|
||
"id": "vorlesung_1775460000",
|
||
"title": "Vorlesung",
|
||
"created_at": 1775460000,
|
||
"modified_at": 1775463600,
|
||
"current_page_index": 0,
|
||
"pages": [
|
||
{ "id": 1, "background": "Blank", "title": "Einführung", "size": [2970.0, 1671.0] },
|
||
{ "id": 2, "background": "Grid5mm" }
|
||
],
|
||
"page_ids": [1, 2],
|
||
"next_stroke_id": 42,
|
||
"next_page_id": 3
|
||
}
|
||
```
|
||
|
||
| Feld | Bedeutung |
|
||
| :--- | :--- |
|
||
| `format_version` | Formatversion (siehe Abschnitt 5) |
|
||
| `id`, `title` | ID (= Ordnername) und Anzeigename |
|
||
| `created_at`, `modified_at` | UNIX-Zeit in Sekunden |
|
||
| `current_page_index` | zuletzt geöffnete Seite |
|
||
| `pages` | Seiten in ihrer Reihenfolge: `id`, `background`, optional `title` und `size` (siehe unten) – damit Seitenübersicht und Ansicht sie kennen, ohne jede Seite zu laden |
|
||
| `page_ids` | dieselbe Reihenfolge als reine ID-Liste (für ältere Versionen) |
|
||
| `next_stroke_id`, `next_page_id` | nächste freie IDs |
|
||
|
||
---
|
||
|
||
## 3. Seite (`page_<id>.json`)
|
||
|
||
Jede Seite liegt in einer eigenen Datei (kompaktes JSON, hier zur Lesbarkeit umbrochen):
|
||
|
||
```json
|
||
{
|
||
"format_version": 8,
|
||
"id": 1,
|
||
"background": "Blank",
|
||
"title": "Einführung",
|
||
"size": [2970.0, 1671.0],
|
||
"pdf_background": { "asset": "<sha256>.pdf", "page": 0, "width_pt": 960.0, "height_pt": 540.0 },
|
||
"strokes": [ … ]
|
||
}
|
||
```
|
||
|
||
| Feld | Bedeutung |
|
||
| :--- | :--- |
|
||
| `background` | Muster: `Blank`, `Grid5mm`, `Lined`, `DotGrid` |
|
||
| `title` | Seitentitel (optional) |
|
||
| `size` | Seitengröße `[breite, höhe]` in Seiteneinheiten (10 = 1 mm); fehlt bei DIN A4 hoch (`[2100, 2970]`) |
|
||
| `pdf_background` | importierte PDF-Seite als Hintergrund (optional): Datei, Seitennummer ab 0, Größe in Punkt |
|
||
| `strokes` | alle Objekte der Seite, von unten nach oben |
|
||
|
||
### 3.1 Objekte (`strokes`)
|
||
|
||
Jedes Objekt hat eine `id`, eine `color` (RGBA, linear, Werte 0–1) und `samples`: Punkte mit `x`, `y` (Seiteneinheiten), `p` (Druck 0–1) und `t` (Zeit in Sekunden). Die Dreiecke für die Anzeige werden beim Laden aus den Punkten berechnet und nicht gespeichert.
|
||
|
||
**Tinte**
|
||
|
||
```json
|
||
{ "id": 7, "color": [0.12, 0.12, 0.14, 1.0],
|
||
"samples": [{ "x": 350.2, "y": 420.5, "p": 0.65, "t": 12.345 }, …],
|
||
"tool": "pen", "width": 5.0, "finalized": true }
|
||
```
|
||
|
||
- `tool`: `pen`, `highlighter` oder `shape`
|
||
- `width`: Strichbreite in Seiteneinheiten (5.0 = 0,5 mm)
|
||
- `finalized`: Der Strich wurde nach dem Absetzen nachbearbeitet (Glättung, Ecken, Endkappen)
|
||
|
||
**Formen** haben zusätzlich `shape`, z. B.:
|
||
|
||
```json
|
||
"shape": { "Rectangle": { "corners": [[500, 600], [800, 600], [800, 800], [500, 800]] } }
|
||
```
|
||
|
||
Mögliche Formen: `Line { start, end }`, `Arrow { start, end, double }`, `Circle { center, radius }`, `Ellipse { center, radius_x, radius_y, rotation }`, `Rectangle { corners }`, `Triangle { corners }`, `Rhombus { corners }`, `Polygon { vertices }`, `CoordinateSystem { origin, x_axis_end, y_axis_end, with_arrows }`, `Octahedron { … }`. Punkte sind `[x, y]`.
|
||
|
||
**Bilder** haben `image` (Dateiname in `assets/`); die vier `samples` sind die Ecken oben links, oben rechts, unten rechts, unten links:
|
||
|
||
```json
|
||
{ "id": 9, "color": [1, 1, 1, 1], "samples": [ …4 Ecken… ], "width": 0.0, "image": "<sha256>.png" }
|
||
```
|
||
|
||
**Textfelder** haben `text`; die vier `samples` sind wie bei Bildern die Ecken des Felds:
|
||
|
||
```json
|
||
"text": { "content": "Überschrift\nzweite Zeile", "size": 42.3, "font": "Sans", "wrap_width": 1500.0 }
|
||
```
|
||
|
||
- `size`: Schriftgröße (Em) in Seiteneinheiten (12 pt ≈ 42,3)
|
||
- `font`: `Sans` (Ubuntu Light) oder `Mono` (Hack)
|
||
- `wrap_width`: Umbruchbreite in Seiteneinheiten; die Ecken bilden das Feld dieser Breite (und der Höhe des gesetzten Texts) ab, Drehung und Skalierung stecken in den Ecken
|
||
|
||
---
|
||
|
||
## 4. Sicherheit beim Speichern
|
||
|
||
### Atomares Schreiben
|
||
1. Der Inhalt wird in `<datei>.tmp.<pid>.<nanosekunden>` geschrieben und mit `fsync` auf den Datenträger gebracht.
|
||
2. `rename` ersetzt die alte Datei in einem Schritt.
|
||
3. Danach wird auch der Ordner mit `fsync` gesichert.
|
||
|
||
Ein Absturz während des Speicherns hinterlässt deshalb entweder die alte oder die neue Datei, nie eine halbe.
|
||
|
||
### Beschädigte Dateien
|
||
- Lässt sich eine Seite nicht lesen, wird sie nach `page_<id>.json.corrupt-<zeitstempel>` verschoben (nicht gelöscht) und die Seite schreibgeschützt angezeigt.
|
||
- Ist `manifest.json` beschädigt oder fehlt, wird `manifest.json.bak` verwendet.
|
||
|
||
### Neuere Versionen
|
||
Dateien mit einer höheren `format_version` als der unterstützten werden nicht geöffnet – so kann eine ältere App-Version keine Inhalte verlieren, die sie nicht kennt.
|
||
|
||
### Migration
|
||
Beim ersten Speichern einer unversionierten Datei (Version 0) wird vorher eine Kopie `*.v0.bak` angelegt.
|
||
|
||
---
|
||
|
||
## 5. Versionen
|
||
|
||
| Version | App | Änderung |
|
||
| :--- | :--- | :--- |
|
||
| 0 | – | ohne `format_version` |
|
||
| 1 | – | `format_version`, Hintergründe im Manifest |
|
||
| 2 | – | `tool`, `width`, `finalized` an Strichen; kompaktes JSON |
|
||
| 3 | 0.3.7 | Formen `Ellipse` und `Arrow` |
|
||
| 4 | 0.5 | Bilder (`image`, Ordner `assets/`) |
|
||
| 5 | 0.6.1 | importierte PDF-Seiten (`pdf_background`) |
|
||
| 6 | 0.7 | Seitentitel (`title` in Seite und Manifest) |
|
||
| 7 | 0.10 | Textfelder (`text`) |
|
||
| 8 | 0.11 | Seitengröße je Seite (`size` in Seite und Manifest) |
|
||
|
||
Jede neue Version ändert nur Ergänzungen; ältere Dateien werden ohne Umwandlung gelesen und beim nächsten Speichern mit der aktuellen Versionsnummer geschrieben.
|
||
|
||
---
|
||
|
||
## 6. Warum JSON
|
||
|
||
Ein Binärformat (z. B. `postcard`) würde Dateien um weitere 20–30 % verkleinern. JSON bleibt trotzdem, weil es sich ansehen, vergleichen und im Notfall von Hand reparieren lässt und das Speichern einer Seite auch bei vielen Strichen deutlich unter einer Millisekunde dauert.
|