@textmode/document 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,10 +2,11 @@
2
2
 
3
3
  ## Choose a decoder
4
4
 
5
- Use `parseTextmodeDocumentJSON(text, options)` when you already know the file is a
6
- textmode document. Use `parseJSON(text)` when several formats share a `.json`
7
- extension, then route by the parsed object's `format` and pass it to
8
- `decodeTextmodeDocument(value, options)`. A filename or MIME type is only a hint.
5
+ Use `parseTextmodeDocumentJSON(text, options)` when you know the file is a
6
+ textmode document. If your app accepts several JSON formats, use `parseJSON(text)`
7
+ first. Check the parsed object's `format`, then pass textmode documents to
8
+ `decodeTextmodeDocument(value, options)`. A filename or MIME type alone cannot
9
+ identify the format.
9
10
 
10
11
  ```js
11
12
  import { parseJSON, decodeTextmodeDocument } from '@textmode/document';
@@ -29,14 +30,14 @@ if (!parsed.ok) {
29
30
  }
30
31
  ```
31
32
 
32
- `parseJSON` uses native `JSON.parse`. It does not detect duplicate object keys;
33
- the last value wins. Avoid duplicate keys when producing files. Syntax success
34
- alone says nothing about whether a value is a valid textmode document.
33
+ `parseJSON` uses JavaScript's `JSON.parse`. If an object repeats a key, the last
34
+ value wins. Producers should use unique keys. Valid JSON is not necessarily a
35
+ valid textmode document.
35
36
 
36
37
  ## Handle results and budgets
37
38
 
38
- Malformed input returns `{ ok: false, error }`. Errors contain `code`, `message`,
39
- and an optional JSON path such as `$.layer.cells.rows[0][0].foreground`.
39
+ Invalid input returns `{ ok: false, error }`. The error includes a `code`,
40
+ `message`, and sometimes a JSON path, such as `$.layer.cells.rows[0][0].foreground`.
40
41
 
41
42
  | Code | Meaning |
42
43
  | ---------------------- | ----------------------------------------------------------------- |
@@ -48,99 +49,111 @@ and an optional JSON path such as `$.layer.cells.rows[0][0].foreground`.
48
49
  | `UNSUPPORTED_ENCODING` | The cell encoding is unsupported. |
49
50
  | `LIMIT_EXCEEDED` | A resource budget or safe numeric range is exceeded. |
50
51
 
51
- Successful results contain `document` and `warnings`. A migrated legacy file
52
- returns `LEGACY_DOCUMENT_MIGRATED`. `acceptLegacy` defaults to `true`; set it to
53
- `false` to accept only current documents. App-specific failures, such as a missing
54
- font or unsupported blend mode, belong to the app after protocol decoding.
52
+ Successful results contain a canonical v3 `document` and a `warnings` array.
53
+ Migrated `textmode.layer` v1 files return `LEGACY_DOCUMENT_MIGRATED`. Set
54
+ `acceptLegacy: false` to reject them; the default is `true`. Supported v2 files
55
+ remain readable and return `DOCUMENT_VERSION_MIGRATED`.
55
56
 
56
- Every configured limit must be a positive safe integer. Invalid configuration
57
- throws `TypeError`; it is a programming error rather than a document failure.
58
- Limits and their defaults are listed in the [specification](specification-2.0.md#safety-limits).
57
+ After decoding, your app must check its own requirements, such as available fonts
58
+ and supported blend modes. The decoder normalizes known values and omits unknown
59
+ properties, so decoding and serializing does not preserve extensions. The format
60
+ does not store font identity or post-processing.
59
61
 
60
- Check file bytes before reading. Parsing occurs before decoded-cell budgets can
61
- apply, so `maxCells` does not cap JSON parsing memory or input bytes. Use lower
62
- app-specific budgets and check cancellation between file read, decode, and import.
63
- Large files can be decoded in a worker; the library itself is synchronous.
62
+ Set limits to positive safe integers. Invalid limit configuration throws
63
+ `TypeError`. See the [default limits](specification-3.0.md#validation-budgets-and-compatibility).
64
64
 
65
- Decoding preserves known values subject to normalization. It omits unknown
66
- properties, so decode-and-serialize is not a lossless round trip for extensions.
67
- It also cannot add missing font identity or recreate post-processing.
65
+ Check file size before reading: decoder limits apply after JSON parsing and do not
66
+ limit input bytes or parser memory. Choose budgets for your app and check for
67
+ cancellation between reading, decoding, and importing. The library is synchronous;
68
+ you can decode large files in a worker.
68
69
 
69
70
  ## Use JSON Schema
70
71
 
71
72
  Node 24 can import the schema with an import attribute:
72
73
 
73
74
  ```js
74
- import schema from '@textmode/document/schema/2.0' with { type: 'json' };
75
+ import schema from '@textmode/document/schema/3.0' with { type: 'json' };
75
76
  ```
76
77
 
77
- Bundlers may use their own JSON import syntax. A filesystem-based tool can resolve
78
- the same package subpath and read it locally. All schema references are local.
78
+ Bundlers may use different JSON import syntax. Tools that read files can resolve
79
+ the same package subpath. All references within the schema are local.
79
80
 
80
81
  For example, with Ajv installed in the consumer:
81
82
 
82
83
  ```js
83
84
  import Ajv2020 from 'ajv/dist/2020.js';
84
85
  import addFormats from 'ajv-formats';
85
- import schema from '@textmode/document/schema/2.0' with { type: 'json' };
86
+ import schema from '@textmode/document/schema/3.0' with { type: 'json' };
86
87
 
87
88
  const ajv = new Ajv2020({ validateFormats: true });
88
89
  addFormats(ajv, { mode: 'full' });
89
90
  const validate = ajv.compile(schema);
90
91
  ```
91
92
 
92
- Enable date-time format assertions to check timestamps. Schema validation checks
93
- structure and local constraints; decoding also checks row/grid agreement,
94
- coordinates, pixel extents, Unicode well-formedness, and resource budgets. A
95
- schema-valid document can fail decoding. Configurable decoder budgets can also
96
- differ from schema constraints: for example, a glyph limit above 64 does not
97
- change the published schema's `maxLength: 64`.
93
+ Enable date-time format assertions to check timestamps. The schema checks
94
+ structure and local constraints. The decoder also checks relationships such as
95
+ row sizes and coordinates, plus Unicode and resource limits. A schema-valid file
96
+ can still fail decoding. Changing a decoder limit does not change the published
97
+ schema; for example, the schema's glyph `maxLength` remains 64.
98
98
 
99
- ## Complete selected document
99
+ ## Complete single-layer document
100
100
 
101
- A selected document contains cell data and grid metrics. It carries no layer
102
- presentation or font identity.
101
+ A single-layer document uses the same structure as a layer stack. The receiving
102
+ application chooses the font.
103
103
 
104
104
  ```json
105
105
  {
106
106
  "format": "textmode.document",
107
- "formatVersion": "2.0.0",
108
- "target": "selected",
109
- "canvas": { "width": 8, "height": 16 },
110
- "grid": { "cols": 1, "rows": 1, "cellWidth": 8, "cellHeight": 16 },
111
- "layer": {
112
- "id": "base",
113
- "cells": {
114
- "encoding": "object-rows-v1",
115
- "rows": [
116
- [
117
- {
118
- "x": 0,
119
- "y": 0,
120
- "character": "A",
121
- "foreground": "#ffffffff",
122
- "background": "#00000000",
123
- "transform": { "invert": false, "flipX": false, "flipY": false, "rotation": 0 }
124
- }
107
+ "formatVersion": "3.0.0",
108
+ "layers": [
109
+ {
110
+ "visible": true,
111
+ "opacity": 1,
112
+ "blendMode": "normal",
113
+ "offsetX": 0,
114
+ "offsetY": 0,
115
+ "rotationZ": 0,
116
+ "grid": {
117
+ "cols": 1,
118
+ "rows": 1,
119
+ "cellWidth": 8,
120
+ "cellHeight": 16
121
+ },
122
+ "id": "base",
123
+ "cells": {
124
+ "encoding": "object-rows-v1",
125
+ "rows": [
126
+ [
127
+ {
128
+ "x": 0,
129
+ "y": 0,
130
+ "character": "A",
131
+ "foreground": "#ffffffff",
132
+ "background": "#00000000",
133
+ "transform": {
134
+ "invert": false,
135
+ "flipX": false,
136
+ "flipY": false,
137
+ "rotation": 0
138
+ }
139
+ }
140
+ ]
125
141
  ]
126
- ]
142
+ }
127
143
  }
128
- }
144
+ ]
129
145
  }
130
146
  ```
131
147
 
132
148
  ## Complete layer stack
133
149
 
134
- An `all` document contains one or more layers in stack order, bottom first.
135
- Each layer has its own grid and presentation values. The first grid determines
136
- the legacy root pixel extent; that extent does not specify a composite viewport.
150
+ Layers appear in stack order, with the bottom layer first. Each layer has its own
151
+ grid and presentation values. The document has no root canvas or viewport.
137
152
 
138
153
  ```json
139
154
  {
140
155
  "format": "textmode.document",
141
- "formatVersion": "2.0.0",
142
- "target": "all",
143
- "canvas": { "width": 8, "height": 16 },
156
+ "formatVersion": "3.0.0",
144
157
  "layers": [
145
158
  {
146
159
  "id": "base",
@@ -150,7 +163,12 @@ the legacy root pixel extent; that extent does not specify a composite viewport.
150
163
  "offsetX": 0,
151
164
  "offsetY": 0,
152
165
  "rotationZ": 0,
153
- "grid": { "cols": 1, "rows": 1, "cellWidth": 8, "cellHeight": 16 },
166
+ "grid": {
167
+ "cols": 1,
168
+ "rows": 1,
169
+ "cellWidth": 8,
170
+ "cellHeight": 16
171
+ },
154
172
  "cells": {
155
173
  "encoding": "object-rows-v1",
156
174
  "rows": [
@@ -161,7 +179,12 @@ the legacy root pixel extent; that extent does not specify a composite viewport.
161
179
  "character": "A",
162
180
  "foreground": "#ffffffff",
163
181
  "background": "#000000ff",
164
- "transform": { "invert": false, "flipX": false, "flipY": false, "rotation": 0 }
182
+ "transform": {
183
+ "invert": false,
184
+ "flipX": false,
185
+ "flipY": false,
186
+ "rotation": 0
187
+ }
165
188
  }
166
189
  ]
167
190
  ]
@@ -175,7 +198,12 @@ the legacy root pixel extent; that extent does not specify a composite viewport.
175
198
  "offsetX": 4,
176
199
  "offsetY": 0,
177
200
  "rotationZ": 15,
178
- "grid": { "cols": 1, "rows": 1, "cellWidth": 8, "cellHeight": 16 },
201
+ "grid": {
202
+ "cols": 1,
203
+ "rows": 1,
204
+ "cellWidth": 8,
205
+ "cellHeight": 16
206
+ },
179
207
  "cells": {
180
208
  "encoding": "object-rows-v1",
181
209
  "rows": [
@@ -186,7 +214,12 @@ the legacy root pixel extent; that extent does not specify a composite viewport.
186
214
  "character": "*",
187
215
  "foreground": "#ff000080",
188
216
  "background": "#00000000",
189
- "transform": { "invert": false, "flipX": true, "flipY": false, "rotation": 90 }
217
+ "transform": {
218
+ "invert": false,
219
+ "flipX": true,
220
+ "flipY": false,
221
+ "rotation": 90
222
+ }
190
223
  }
191
224
  ]
192
225
  ]
@@ -0,0 +1,236 @@
1
+ # Visual guide to document format 2.0
2
+
3
+ > Versioned v2 reference. Current v3 documentation is in the
4
+ > [v3 specification](specification-3.0.md) and [visual guide](schema-3.0.md).
5
+ > The current decoder validates these older shapes and migrates them to v3.
6
+
7
+ These diagrams show the structure of `textmode.document@2.0.0`. For the complete
8
+ rules, see the [v2 specification](specification-2.0.md) and [JSON Schema](../schemas/2.0/schema.json).
9
+ For decoding examples and error handling, see the [consumer guide](consumer-guide.md).
10
+
11
+ ## Document shapes
12
+
13
+ A v2 document has one of two shapes. Both include the format, version, and
14
+ `canvas` extent; metadata is optional. The `target` field selects the shape.
15
+
16
+ ```mermaid
17
+ flowchart TD
18
+ Root["Root object"] --> Format["format = textmode.document<br/>required"]
19
+ Root --> Version["formatVersion = 2.0.0<br/>required"]
20
+ Root --> Target{"target<br/>required; oneOf"}
21
+ Root --> Canvas["canvas: pixel extent<br/>required in both shapes"]
22
+ Root -. "optional" .-> Metadata["metadata"]
23
+
24
+ Target -->|selected| Selected["Selected snapshot"]
25
+ Selected --> SelectedGrid["grid: dimensions + cell metrics"]
26
+ Selected --> SelectedLayer["layer: id + cells"]
27
+
28
+ Target -->|all| All["Layer stack"]
29
+ All --> Layers["layers: one or more stack layers"]
30
+ ```
31
+
32
+ ### Root fields
33
+
34
+ | Field | Required | Meaning |
35
+ | --------------- | ------------- | ------------------------------------------------------------------------------------ |
36
+ | `format` | Yes | Must be `textmode.document`. |
37
+ | `formatVersion` | Yes | Must be `2.0.0`. This file-format version is independent of the npm package version. |
38
+ | `target` | Yes | Must be `selected` or `all`; selects the matching shape. |
39
+ | `canvas` | Yes | Legacy pixel extent. It is not necessarily the final or composite viewport. |
40
+ | `grid` | Selected only | Grid metrics shared by the selected snapshot's cells. |
41
+ | `layer` | Selected only | One selected cell snapshot with an informational ID. |
42
+ | `layers` | All only | Nonempty ordered stack of layers, bottom first. |
43
+ | `metadata` | No | Creation timestamp and producer identity. If present, both members are required. |
44
+
45
+ Ordinary objects allow unknown fields. The decoder omits them from canonical
46
+ output, so decoding and reserializing does not preserve them.
47
+
48
+ ## Selected snapshot and layer stack
49
+
50
+ The selected shape stores one cell snapshot. It has no layer visibility,
51
+ opacity, blend mode, offsets, rotation, or font identity. The `all` shape stores
52
+ one or more independently measured layers and preserves their stack order.
53
+
54
+ ```mermaid
55
+ flowchart LR
56
+ subgraph Selected["target: selected"]
57
+ SCanvas["canvas<br/>width, height in pixels"]
58
+ SGrid["grid<br/>cols, rows<br/>cellWidth, cellHeight"]
59
+ SLayer["layer<br/>id + cells"]
60
+ SGrid --> SLayer
61
+ SCanvas -. "width = cols × cellWidth<br/>height = rows × cellHeight" .-> SGrid
62
+ end
63
+
64
+ subgraph All["target: all"]
65
+ ACanvas["canvas<br/>first layer's pixel extent"]
66
+ Stack["layers[]<br/>one or more; bottom to top"]
67
+ L0["first stack layer"]
68
+ L1["next layer"]
69
+ More["zero or more further layers"]
70
+ ACanvas -. "legacy extent follows first layer grid" .-> L0
71
+ Stack --> L0 --> L1 --> More
72
+ end
73
+ ```
74
+
75
+ For a selected document, `canvas.width` and `canvas.height` equal the grid's
76
+ pixel extent. For a stack document, root `canvas` follows the **first layer's**
77
+ grid. It does not describe the combined bounds, placement, or clipping of the
78
+ stack. Every stack layer has its own grid, cell metrics, and cells.
79
+
80
+ | Object | Required fields | Local constraints |
81
+ | ---------------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
82
+ | `canvas` | `width`, `height` | Positive safe integers in pixels. |
83
+ | `grid` | `cols`, `rows`, `cellWidth`, `cellHeight` | Positive safe integers. Pixel products must remain safe integers; the decoder checks the extent relationship. |
84
+ | Selected `layer` | `id`, `cells` | `id` is a nonempty string. |
85
+ | Cell collection | `encoding`, `rows` | `encoding` is exactly `object-rows-v1`; `rows` is an array. |
86
+
87
+ In a selected document, `grid` is at the root. In a stack, each layer has a
88
+ grid and the root `canvas` is checked against the first layer's grid.
89
+
90
+ Each stack layer requires all of the following fields:
91
+
92
+ | Field | Constraint or meaning |
93
+ | -------------------- | ------------------------------------------------------------------------------------------------------------------------ |
94
+ | `id` | Nonempty, informational source ID. It is not a persistent identity and need not be unique. |
95
+ | `visible` | Boolean presentation state. Hidden layer cells remain in the document. |
96
+ | `opacity` | Number from `0` to `1`, separate from cell alpha. |
97
+ | `blendMode` | Nonempty mode name. The decoder preserves names it does not recognize; consumers decide what they can render. |
98
+ | `offsetX`, `offsetY` | Pixel offsets. In the textmode.js compositor, positive x moves right and positive y moves down. |
99
+ | `rotationZ` | Layer rotation in degrees. In textmode.js it is clockwise around the rectangular layer center; the decoder preserves it. |
100
+ | `grid` | This layer's columns, rows, and cell pixel dimensions. |
101
+ | `cells` | This layer's complete dense cell data. |
102
+
103
+ The textmode.js exporter writes `normal`, `additive`, `multiply`, `screen`,
104
+ `subtract`, `darken`, `lighten`, `overlay`, `softLight`, `hardLight`,
105
+ `colorDodge`, `colorBurn`, `difference`, and `exclusion`. These are producer
106
+ values, not an exhaustive enum in the schema.
107
+
108
+ ## Grid and dense cell storage
109
+
110
+ Both shapes store cells in `object-rows-v1` encoding. The row and column arrays
111
+ are dense and row-major. Every grid position has a cell, including cells with
112
+ transparent foreground and background colors.
113
+
114
+ ```mermaid
115
+ flowchart TD
116
+ Grid["grid<br/>cols × rows"] --> Enc["cells.encoding<br/>object-rows-v1"]
117
+ Enc --> Rows["cells.rows[]<br/>row-major"]
118
+ Rows --> Row["rows[y]"]
119
+ Row --> Cell["rows[y][x]"]
120
+ Cell --> Coord["cell.x = x<br/>cell.y = y"]
121
+ Grid -. "rows.length = rows" .-> Rows
122
+ Grid -. "each row length = cols" .-> Row
123
+ Grid -. "all coordinates are present" .-> Cell
124
+ ```
125
+
126
+ Coordinates are zero-based. Each cell's `x` and `y` must match its position in
127
+ `rows[y][x]`. Consumers must reject a mismatch instead of reordering or repairing
128
+ cells.
129
+
130
+ | Cell field | Required | Schema-level shape |
131
+ | -------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
132
+ | `x`, `y` | Yes | Nonnegative safe-range integers; decoder requires them to match array positions. |
133
+ | `character` | Yes | String constrained by `minLength: 1` and `maxLength: 64`; decoder rejects unpaired surrogates and counts Unicode scalar values against its configured limit. |
134
+ | `foreground`, `background` | Yes | Each is a six/eight digit hex color or an RGBA object. |
135
+ | `transform` | Yes | Object with `invert`, `flipX`, `flipY`, and `rotation`. |
136
+
137
+ An RGBA color object requires integer `r`, `g`, `b`, and `a` channels. Each
138
+ channel is between `0` and `255`. The schema permits additional properties in
139
+ these objects, as it does for the other ordinary objects in the format.
140
+
141
+ The decoder checks row sizes, coordinates, and canvas dimensions against the
142
+ grid. The schema alone does not check these relationships.
143
+
144
+ ## Cell, color, and transform structure
145
+
146
+ Each cell requires coordinates, a glyph string, foreground and background
147
+ colors, and all four transform values.
148
+
149
+ ```mermaid
150
+ flowchart TD
151
+ Cell["Cell"] --> X["x: zero-based column"]
152
+ Cell --> Y["y: zero-based row"]
153
+ Cell --> Character["character: nonempty Unicode string"]
154
+ Cell --> Foreground["foreground"]
155
+ Cell --> Background["background"]
156
+ Cell --> Transform["transform"]
157
+
158
+ Foreground --> Color["Color"]
159
+ Background --> Color
160
+ Color --> Hex6["#RRGGBB<br/>alpha defaults to 255"]
161
+ Color --> Hex8["#RRGGBBAA"]
162
+ Color --> RGBA["RGBA object"]
163
+ RGBA --> Channels["r, g, b, a<br/>integers 0–255"]
164
+
165
+ Transform --> Invert["invert: boolean"]
166
+ Transform --> FlipX["flipX: boolean"]
167
+ Transform --> FlipY["flipY: boolean"]
168
+ Transform --> Rotation["rotation: finite degrees<br/>canonical range [0, 360)"]
169
+ ```
170
+
171
+ The decoder converts colors to RGBA objects and normalizes cell rotation to
172
+ `[0, 360)`. It preserves Unicode strings without normalization. A glyph can
173
+ contain multiple scalar values, such as a combining sequence or joined emoji;
174
+ unpaired surrogates are invalid. The decoder also applies a scalar-count limit.
175
+
176
+ Cell alpha and layer opacity are separate. A cell is visually empty only when
177
+ both foreground and background alpha are zero. A space glyph with a visible
178
+ background remains meaningful. Inversion cannot generally be implemented by
179
+ swapping colors when alpha is present.
180
+
181
+ ## Metadata
182
+
183
+ Metadata is optional at the root. If the `metadata` object exists, it requires
184
+ both `createdAt` and `generator`. The generator requires a nonempty `name` and
185
+ `version`.
186
+
187
+ ```mermaid
188
+ flowchart LR
189
+ Root["Document"] -. "optional" .-> Metadata["metadata"]
190
+ Metadata --> CreatedAt["createdAt<br/>date-time string"]
191
+ Metadata --> Generator["generator"]
192
+ Generator --> Name["name: nonempty string"]
193
+ Generator --> Version["version: nonempty string"]
194
+ ```
195
+
196
+ `createdAt` uses the format's timestamp profile: uppercase `T`, seconds,
197
+ timezone, optional fractional seconds, and no leap seconds. Validators must
198
+ enable date-time format assertions to validate the `date-time` format keyword.
199
+ Metadata is informational. Consumers must not use it to choose executable
200
+ behavior, fetch resources, or bypass validation.
201
+
202
+ ## Validation and decoding boundaries
203
+
204
+ The schema describes structure and local constraints. The specification defines
205
+ field meaning and relationships. The decoder checks documents, applies limits,
206
+ and migrates supported older formats to canonical v3 output.
207
+
208
+ ```mermaid
209
+ flowchart LR
210
+ Input["v2 or supported legacy JSON"] --> Decoder["Decoder<br/>validation, limits,<br/>migration, normalization"]
211
+ Decoder --> Canonical["Canonical v3 document<br/>with migration warning"]
212
+ Decoder --> Error["Failure result"]
213
+ Schema["JSON Schema<br/>structure and local constraints"] -. "v2 structure" .-> Input
214
+ Spec["Format specification<br/>meaning and relationships"] -. "v2 rules" .-> Decoder
215
+ ```
216
+
217
+ A schema-valid document can still fail decoding. For example, the schema does
218
+ not check row sizes against the grid or enforce a caller's total cell budget.
219
+ See the [consumer guide](consumer-guide.md#use-json-schema) for validator setup
220
+ and the [v2 specification](specification-2.0.md#safety-limits) for decoder limits.
221
+
222
+ ## Scope and compatibility
223
+
224
+ Version 2 stores editable cell snapshots and layer stacks. It does not store
225
+ font identity or font assets, editor project state, authored animation,
226
+ post-processing, or final rendered pixels. Importers apply their own font,
227
+ feature-support, and placement policies.
228
+
229
+ The current decoder migrates v2 documents and supported `textmode.layer` files
230
+ to canonical v3 layers. It reports a migration warning and can be configured to
231
+ reject legacy `textmode.layer` input. The v2 JSON Schema describes the v2 wire
232
+ shapes; it is not a schema for the legacy formats or current v3 output.
233
+
234
+ For the exact normative rules, including unknown properties, timestamp
235
+ requirements, blend-mode behavior, and safety guidance, see the
236
+ [Format 2.0 specification](specification-2.0.md).
@@ -0,0 +1,117 @@
1
+ # Visual guide to document format 3.0
2
+
3
+ These diagrams show the structure of `textmode.document@3.0.0`. For the complete
4
+ rules, see the [specification](specification-3.0.md) and [JSON Schema](../schemas/3.0/schema.json).
5
+ The [consumer guide](consumer-guide.md) includes complete example documents.
6
+
7
+ ## One shape for any layer count
8
+
9
+ ```mermaid
10
+ flowchart TD
11
+ Document["Document object"] --> Format["format = textmode.document"]
12
+ Document --> Version["formatVersion = 3.0.0"]
13
+ Document --> Layers["layers[]: at least one; bottom to top"]
14
+ Document -. optional .-> Metadata["metadata: createdAt + generator"]
15
+ Layers --> Layer["Same complete layer structure for every entry"]
16
+ Layer --> Identity["id: informational string"]
17
+ Layer --> Presentation["visible, opacity, blendMode<br/>offsetX, offsetY, rotationZ"]
18
+ Layer --> Grid["grid: cols, rows, cellWidth, cellHeight"]
19
+ Layer --> Cells["cells: encoding + dense rows"]
20
+ ```
21
+
22
+ All shown fields are required except metadata. A document with one layer has one
23
+ array entry. Root `target`, `layer`, `grid`, and `canvas` fields are rejected.
24
+ Other unknown fields are accepted and omitted from canonical output. IDs need not
25
+ be unique. The format does not define a font, viewport, or animation.
26
+
27
+ ## Grids and storage
28
+
29
+ ```mermaid
30
+ flowchart TD
31
+ Grid["Layer grid: positive safe integers"] --> Extent["Derived pixel extent<br/>cols × cellWidth; rows × cellHeight"]
32
+ Grid --> Encoding["cells.encoding = object-rows-v1"]
33
+ Encoding --> Rows["cells.rows: grid.rows rows"]
34
+ Rows --> Row["rows[y]: grid.cols cells"]
35
+ Row --> Cell["rows[y][x]"]
36
+ Cell --> Coordinates["cell.x = x; cell.y = y<br/>zero-based, redundant checks"]
37
+ Grid -. "decoder checks safe products and array agreement" .-> Coordinates
38
+ ```
39
+
40
+ Every grid position is stored, including transparent cells. Layers can have
41
+ different dimensions and cell sizes. The decoder checks row sizes and coordinates
42
+ against each layer's grid.
43
+
44
+ ## Layer presentation
45
+
46
+ ```mermaid
47
+ flowchart LR
48
+ Layer["Layer"] --> Visible["visible: boolean"]
49
+ Layer --> Opacity["opacity: 0 to 1"]
50
+ Layer --> Blend["blendMode: nonempty name"]
51
+ Layer --> Offset["offsetX / offsetY: pixels<br/>positive right / down"]
52
+ Layer --> Rotation["rotationZ: finite degrees<br/>clockwise about center in textmode.js"]
53
+ Opacity --> Alpha["Multiplies layer appearance<br/>separate from cell alpha"]
54
+ Blend --> Support["Unknown names preserved<br/>consumer checks support"]
55
+ ```
56
+
57
+ Hidden layers keep their cells. Layer rotation is preserved without canonical
58
+ normalization. Stack order is bottom first. Every presentation field is explicit;
59
+ JSON Schema defaults are not used to supply missing values.
60
+
61
+ ## Complete cell payload
62
+
63
+ ```mermaid
64
+ flowchart TD
65
+ Cell["Cell: all fields required"] --> XY["x / y: nonnegative safe integers"]
66
+ Cell --> Glyph["character: nonempty Unicode glyph string"]
67
+ Cell --> FG["foreground"]
68
+ Cell --> BG["background"]
69
+ FG --> Color{"Color representation"}
70
+ BG --> Color
71
+ Color --> RGB["#RRGGBB: alpha 255"]
72
+ Color --> RGBAHex["#RRGGBBAA"]
73
+ Color --> Channels["Object: integer r, g, b, a<br/>each 0 to 255"]
74
+ Cell --> Transform["transform"]
75
+ Transform --> Booleans["invert / flipX / flipY: booleans"]
76
+ Transform --> Rotation["rotation: finite degrees<br/>canonical [0, 360)"]
77
+ ```
78
+
79
+ Canonical colors use RGBA objects. Unicode is preserved without normalization;
80
+ unpaired surrogates fail. The schema's minLength/maxLength are 1/64; decoder
81
+ scalar-count budgets are configurable. A visible background on a space matters.
82
+ Only cells whose foreground and background alpha are both zero are visually
83
+ empty. Inversion cannot generally be replaced by swapping colors.
84
+
85
+ ## Metadata
86
+
87
+ ```mermaid
88
+ flowchart LR
89
+ Document["Document"] -. optional .-> Metadata["metadata"]
90
+ Metadata --> Date["createdAt: real date-time<br/>uppercase T + timezone + seconds"]
91
+ Metadata --> Generator["generator"]
92
+ Generator --> Name["name: nonempty producer string"]
93
+ Generator --> Version["version: nonempty producer string"]
94
+ ```
95
+
96
+ All children are required when metadata is present. Fractional seconds are
97
+ optional; leap seconds are unsupported. Enable date-time format assertions in
98
+ validators. Metadata is informational and never authorizes code or resource loads.
99
+
100
+ ## Readers and canonical output
101
+
102
+ ```mermaid
103
+ flowchart TD
104
+ V3["v3: unified layers"] --> Decoder["Decode known fields<br/>enforce budgets + cross-field rules"]
105
+ V2["v2: selected or all<br/>original validation including canvas"] --> Migration["Convert structure<br/>neutral presentation for selected snapshots"]
106
+ V1["Legacy textmode.layer v1<br/>when acceptLegacy is true"] --> Migration
107
+ Migration --> Decoder
108
+ Decoder --> Success["Success: canonical v3 layers<br/>RGBA colors; normalized cell rotation"]
109
+ Decoder --> Failure["Failure: code, message, optional path"]
110
+ Success --> Warnings["Migration warnings for older input"]
111
+ ```
112
+
113
+ The decoder checks documents directly; a separate schema validation pass is
114
+ optional. It omits unknown fields from canonical output. Applications must check
115
+ input size before parsing and apply their own font, feature, and placement rules.
116
+ See the [specification](specification-3.0.md#validation-budgets-and-compatibility)
117
+ for default limits and the [v2 visual guide](schema-2.0.md) for older shapes.