@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.
- package/README.md +9 -8
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +1 -1
- package/dist/decode-shapes.d.ts +5 -3
- package/dist/decode-shapes.js +41 -13
- package/dist/decode.js +8 -4
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/types/canonical.d.ts +3 -11
- package/dist/types/decode.d.ts +2 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/legacy.d.ts +4 -3
- package/dist/types/v2.d.ts +34 -0
- package/dist/types/v2.js +1 -0
- package/dist/types/wire.d.ts +2 -34
- package/docs/consumer-guide.md +99 -66
- package/docs/schema-2.0.md +236 -0
- package/docs/schema-3.0.md +117 -0
- package/docs/specification-2.0.md +30 -34
- package/docs/specification-3.0.md +134 -0
- package/package.json +9 -4
- package/schemas/3.0/schema.d.ts +3 -0
- package/schemas/3.0/schema.json +267 -0
- package/docs/maintenance.md +0 -56
package/docs/consumer-guide.md
CHANGED
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
## Choose a decoder
|
|
4
4
|
|
|
5
|
-
Use `parseTextmodeDocumentJSON(text, options)` when you
|
|
6
|
-
textmode document.
|
|
7
|
-
|
|
8
|
-
`decodeTextmodeDocument(value, options)`. A filename or MIME type
|
|
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
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
39
|
-
and
|
|
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
|
|
52
|
-
|
|
53
|
-
`false` to
|
|
54
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
61
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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/
|
|
75
|
+
import schema from '@textmode/document/schema/3.0' with { type: 'json' };
|
|
75
76
|
```
|
|
76
77
|
|
|
77
|
-
Bundlers may use
|
|
78
|
-
the same package subpath
|
|
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/
|
|
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.
|
|
93
|
-
structure and local constraints
|
|
94
|
-
coordinates,
|
|
95
|
-
|
|
96
|
-
|
|
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
|
|
99
|
+
## Complete single-layer document
|
|
100
100
|
|
|
101
|
-
A
|
|
102
|
-
|
|
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": "
|
|
108
|
-
"
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
"
|
|
115
|
-
"
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
135
|
-
|
|
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": "
|
|
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": {
|
|
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": {
|
|
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": {
|
|
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": {
|
|
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.
|