@quario/layout 0.4.0 → 0.6.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/CHANGELOG.md +117 -49
- package/README.md +39 -24
- package/lib/canvas.js +4 -1
- package/lib/fonts.js +7 -3
- package/lib/index.d.ts +33 -10
- package/lib/index.js +15 -3
- package/lib/layout.js +25 -22
- package/lib/page.js +2 -1
- package/lib/paint.js +18 -9
- package/lib/style.js +1 -0
- package/package.json +3 -3
- package/lib/image.js +0 -58
package/CHANGELOG.md
CHANGED
|
@@ -1,15 +1,114 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
1
|
+
# @quario/layout
|
|
2
|
+
|
|
3
|
+
## 0.6.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- **Every target factory now refuses an option it does not understand.** An unknown key, a key with
|
|
8
|
+
a value of the wrong type, or an `options` that is not an object throws a `TypeError` at the
|
|
9
|
+
factory call, naming the option path — `options.meta.title`, `options.page.size`. `@quario/docx`
|
|
10
|
+
already behaved this way. `@quario/pdf`, `@quario/html`, `@quario/xlsx` and `@quario/layout` read
|
|
11
|
+
the keys they knew and ignored the rest, so a typo cost you the option you meant to set with no
|
|
12
|
+
signal at any point. `csv()` takes no options, and it refuses an argument rather than discarding
|
|
13
|
+
one.
|
|
14
|
+
|
|
15
|
+
The check itself is one thing, so the engine now exports it: `hostOptions` closes a key set and
|
|
16
|
+
`hostMeta` validates the `{ title, author, subject }` contract the document targets share. A
|
|
17
|
+
fourth document property is one edit rather than three.
|
|
18
|
+
|
|
19
|
+
**What this changes for you.** One options object spread across several targets stops working if
|
|
20
|
+
any target does not know one of its keys — `const o = { page, fonts, meta }` passed to `pdf(o)`,
|
|
21
|
+
`html(o)` and `xlsx(o)` is three different key sets. An object holding only what its consumers
|
|
22
|
+
share is unaffected, so `{ page, fonts }` into both `pdf()` and `layout()` still works.
|
|
23
|
+
TypeScript does not warn about this: excess-property checking fires on an object literal and not
|
|
24
|
+
on a variable, so a bag held in a `const` compiles clean and throws when you call the factory.
|
|
25
|
+
|
|
26
|
+
Nullish is absence everywhere, at both levels, so `{ meta: config.meta ?? null }` and
|
|
27
|
+
`{ meta: { title: config.title } }` over a config that carries neither are both fine.
|
|
28
|
+
`html({ paths })` now takes `true` or `false` rather than anything truthy, so `paths: "no"` throws
|
|
29
|
+
instead of turning path stamping on. A `fonts` mapping given as an array is refused by
|
|
30
|
+
`@quario/html` and `@quario/layout` rather than read as families named `0`, `1`, `2`.
|
|
31
|
+
|
|
32
|
+
Every host-option failure is now a `TypeError` rather than a plain `Error`. A `catch` that tests
|
|
33
|
+
`instanceof Error` is unaffected. One that compares the constructor is not.
|
|
34
|
+
|
|
35
|
+
`@quario/docx` is relaxed in three places, each of them now accepting what it refused: `meta: null`
|
|
36
|
+
and `page: null` are absence rather than errors, a property written as `meta: { title: undefined }`
|
|
37
|
+
is a property you did not write rather than one of the wrong type, and an inherited enumerable key
|
|
38
|
+
is no longer reported as an option you wrote.
|
|
39
|
+
|
|
40
|
+
`@quario/pdf`, `@quario/xlsx` and `@quario/docx` now copy `meta` at the factory call. Mutating the
|
|
41
|
+
object you passed no longer changes what a configured target writes.
|
|
42
|
+
|
|
43
|
+
### Patch Changes
|
|
44
|
+
|
|
45
|
+
- Updated dependencies
|
|
46
|
+
- quario@0.9.0
|
|
47
|
+
|
|
48
|
+
## 0.5.0
|
|
49
|
+
|
|
50
|
+
### Minor Changes
|
|
51
|
+
|
|
52
|
+
- **An image's box is now as wide as the container it sits in.** A
|
|
53
|
+
`background` or `border*` on an image item hugged the picture and now spans
|
|
54
|
+
the content width — or the slot's share inside a split — with `fit` sizing
|
|
55
|
+
the picture and `align` placing it inside, the way a text item's box has
|
|
56
|
+
always been drawn. The picture itself does not move. Breaking: a report that
|
|
57
|
+
relied on a border hugging a logo now draws that border across the full
|
|
58
|
+
width, and no declaration asks for the old shape.
|
|
59
|
+
- **A page of the display list no longer carries `width` and `height`.** Read
|
|
60
|
+
the list's own `width` and `height` instead: they are the paper every page of
|
|
61
|
+
the report is drawn on, and every page's pair was a copy of them. A report has
|
|
62
|
+
one page size for the whole render — that has always been so, and page size is
|
|
63
|
+
a target option rather than something a document declares — so a page is
|
|
64
|
+
identified and positioned, and asked nothing about its size. A page now
|
|
65
|
+
carries `{ number, total, ops, boxes }`.
|
|
66
|
+
|
|
67
|
+
If you consume the list yourself, the fix is one substitution:
|
|
68
|
+
|
|
69
|
+
```js
|
|
70
|
+
canvas.width = list.width * 2; // was list.pages[i].width * 2
|
|
71
|
+
canvas.height = list.height * 2; // was list.pages[i].height * 2
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
`paint()` is unchanged in shape — it still takes one page — and it now fills
|
|
75
|
+
the white over the whole canvas rather than over the page's own rectangle.
|
|
76
|
+
**Size the canvas to the page before you call it**, as the sizing example above
|
|
77
|
+
does: the ops are in the page's own coordinates, so a canvas short of the paper
|
|
78
|
+
clips them whatever the fill covers. That was always so; it is now written down
|
|
79
|
+
in the README and on `paint()` itself.
|
|
80
|
+
|
|
81
|
+
- **`Layout.pages` is typed as a non-empty list.** A render always lays out at
|
|
82
|
+
least one page — the first page opens whether or not anything renders on it —
|
|
83
|
+
so `pages[0]` no longer needs a check for a list that cannot exist. Runtime
|
|
84
|
+
behaviour is unchanged; this only states in the type what was already true.
|
|
85
|
+
- **`Paper`** — the page box a report is drawn on, `{ width, height }` in points,
|
|
86
|
+
and what a display list's own `width` and `height` are. `Layout` extends it, so
|
|
87
|
+
`list.width` is unchanged and the pair now has a name to refer to.
|
|
88
|
+
|
|
89
|
+
### Patch Changes
|
|
90
|
+
|
|
91
|
+
- **A page's right and bottom edge could be left unpainted.** Where a caller
|
|
92
|
+
rounds the backing store up to a whole device pixel — A4 at 100% asks for
|
|
93
|
+
793.71 and takes 794 — `paint()` filled its white over the page's exact
|
|
94
|
+
rectangle and left the fraction past it transparent. On a canvas with white
|
|
95
|
+
behind it that is invisible; on one without, a hairline of whatever is behind
|
|
96
|
+
the canvas showed along two edges. The fill now covers the whole canvas.
|
|
97
|
+
- **Every published README says where the documentation is.** Each package now
|
|
98
|
+
carries a Documentation section pointing at the reference, at the report schema
|
|
99
|
+
that normatively specifies what a report may declare, and at the package's own
|
|
100
|
+
API. The paragraphs that used to end on an unstated contract — the event
|
|
101
|
+
stream's field semantics, the style vocabulary, page columns, the Content
|
|
102
|
+
Security Policy a fragment with images needs, the formula mangling, and each
|
|
103
|
+
target's own contract — link the page that states it. Every link is an absolute
|
|
104
|
+
URL, so it resolves from the npm package page as readily as from an installed
|
|
105
|
+
copy.
|
|
106
|
+
- Updated dependencies
|
|
107
|
+
- quario@0.8.0
|
|
108
|
+
|
|
109
|
+
## 0.4.0
|
|
110
|
+
|
|
111
|
+
### Minor Changes
|
|
13
112
|
|
|
14
113
|
- **Per-run typography, decoration and highlights.** A styled run resolves its
|
|
15
114
|
own face, size and colour, and a line takes the largest size among its runs.
|
|
@@ -17,31 +116,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
17
116
|
that run's colour — where they used to stroke the whole line in the first
|
|
18
117
|
piece's colour. A run's `background` paints a highlight rectangle behind its
|
|
19
118
|
text; a cell's own background is still painted once, at cell scope.
|
|
20
|
-
|
|
21
119
|
- **`checkFonts` is public.** It checks the shape of a `fonts` mapping without
|
|
22
120
|
loading a parser, and takes an optional name to prefix a failure with, the
|
|
23
121
|
way `pageBox` does — so a surface validating its own `fonts` property can
|
|
24
122
|
report the mistake against that property rather than against
|
|
25
123
|
`options.fonts`.
|
|
26
|
-
|
|
27
|
-
### Fixed
|
|
28
|
-
|
|
29
124
|
- **An outline `Mark` carries only what it declares.** Every mark shipped an
|
|
30
125
|
extra `titled` boolean — bookkeeping for whether a header had claimed the
|
|
31
126
|
entry — which was never part of the `Mark` interface and which nothing
|
|
32
127
|
reads. It is the open group instance's own state now, and no longer travels
|
|
33
128
|
on the object a consumer receives.
|
|
34
129
|
|
|
35
|
-
##
|
|
130
|
+
## 0.3.0
|
|
36
131
|
|
|
37
|
-
###
|
|
132
|
+
### Minor Changes
|
|
38
133
|
|
|
39
134
|
- The text join reads a cell's own `currency` code, ahead of the instance's
|
|
40
135
|
default, so every target built on this package presents a per-cell
|
|
41
136
|
denomination.
|
|
42
|
-
|
|
43
|
-
### Changed
|
|
44
|
-
|
|
45
137
|
- **A group instance that renders nothing no longer takes up space.** A group
|
|
46
138
|
whose header and footer items all resolve `visible: false`, with nothing
|
|
47
139
|
visible under it either, used to open the same half-line gap as any other
|
|
@@ -52,7 +144,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
52
144
|
its rows stay in the aggregates. A group that declares `break: "page"` or
|
|
53
145
|
`reset: "page"` still starts its page either way. Every document with such a
|
|
54
146
|
group renders slightly shorter than it did.
|
|
55
|
-
|
|
56
147
|
- **Numbers presented through `format` now show a fixed two fraction digits,
|
|
57
148
|
matching every other target** — `1,000.00` where `1,000` was rendered,
|
|
58
149
|
`21.00%` where `21%` was, and a currency's own minor units in place of a
|
|
@@ -60,15 +151,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
60
151
|
a formatted cell is now up to three characters wider than it was: a line
|
|
61
152
|
that just fitted can wrap, which can move a page break in a PDF, the viewer,
|
|
62
153
|
or an editor preview. Nothing else about wrapping changed.
|
|
63
|
-
|
|
64
|
-
### Fixed
|
|
65
|
-
|
|
66
154
|
- **An image failure now names the item that asked for the bytes.** A file too
|
|
67
155
|
short to carry a size failed saying only that the size could not be read,
|
|
68
156
|
naming no item, so a report with two pictures gave no way to tell which one
|
|
69
157
|
was bad. The message is now prefixed with the item's `source` path, as every
|
|
70
158
|
other render error is.
|
|
71
|
-
|
|
72
159
|
- **An image the browser cannot decode no longer costs the whole page.** A
|
|
73
160
|
PNG or JPEG whose pixel data is corrupt past the size in its header lays out
|
|
74
161
|
like any other — the size is all that is read of it — and used to throw out
|
|
@@ -77,9 +164,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
77
164
|
drawn as nothing and the page is drawn around it, so what a bad image costs
|
|
78
165
|
is the image.
|
|
79
166
|
|
|
80
|
-
##
|
|
167
|
+
## 0.2.0
|
|
81
168
|
|
|
82
|
-
###
|
|
169
|
+
### Minor Changes
|
|
83
170
|
|
|
84
171
|
- **A spanning cell is drawn as one box** across the columns it covers,
|
|
85
172
|
starting where the first of them starts. It takes no part in allocating their
|
|
@@ -87,21 +174,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
87
174
|
empty table still shows its geometry. A spanning row too tall for the page it
|
|
88
175
|
is on slices like any other, each slice following the geometry of the page or
|
|
89
176
|
page-column strip it lands in.
|
|
90
|
-
|
|
91
177
|
- **`valign`** places a cell's lines, or a slot's lines or picture, in the slack
|
|
92
178
|
its row or split leaves over its own content: middle halves it, bottom takes
|
|
93
179
|
it. The box does not move, and a picture is not scaled. A row too tall for
|
|
94
180
|
any page, sliced across pages, paints from the top.
|
|
95
|
-
|
|
96
|
-
### Changed
|
|
97
|
-
|
|
98
181
|
- **A row's box is drawn by the row's cells.** A box declared on a table row
|
|
99
182
|
used to be one rect across the summed column widths; it is now each covered
|
|
100
183
|
cell's own, so a row's `borderBottom` still reads as one continuous edge
|
|
101
184
|
while a row's `borderLeft` becomes an edge on every cell rather than one at
|
|
102
185
|
the row's outer left. A row's border also occupies height now, as a cell's
|
|
103
186
|
always has, so a bordered row is taller by its border's width.
|
|
104
|
-
|
|
105
187
|
- **Measuring TrueType families now needs `fontkit`, not `@pdf-lib/fontkit`.**
|
|
106
188
|
Install `fontkit` instead; nothing else about `options.fonts` changes. The
|
|
107
189
|
old package's bundle crashed with a bare `ReferenceError` on any OpenType
|
|
@@ -110,9 +192,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
110
192
|
besides. Those faces now measure where they used to throw. Which scripts a
|
|
111
193
|
face supports remains the font's and the parser's to answer, not this
|
|
112
194
|
package's.
|
|
113
|
-
|
|
114
|
-
### Fixed
|
|
115
|
-
|
|
116
195
|
- **A group header no longer strands above a split.** A split cannot be broken
|
|
117
196
|
across a page, so a header that introduces one has to keep the whole of it
|
|
118
197
|
company — but the keep-together test measured a split by its first two lines
|
|
@@ -127,9 +206,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
127
206
|
always has before content no page can hold: it degrades to plain paginated
|
|
128
207
|
flow and repeats nothing.
|
|
129
208
|
|
|
130
|
-
##
|
|
209
|
+
## 0.1.0
|
|
131
210
|
|
|
132
|
-
###
|
|
211
|
+
### Minor Changes
|
|
133
212
|
|
|
134
213
|
- **The paged display list.** `layout({ page, fonts })` is a render target
|
|
135
214
|
resolving every page of the report — ops in points from the top-left, one
|
|
@@ -137,12 +216,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
137
216
|
marks, and on an unlicensed render the marking. The algorithm is the PDF
|
|
138
217
|
target's typesetter, with its home moved here so a preview can paint the
|
|
139
218
|
same pages the document has.
|
|
140
|
-
|
|
141
219
|
- **One measurer.** Base-14 families measure against `@pdf-lib/standard-fonts`'
|
|
142
220
|
AFM metrics over WinAnsi, TrueType families against `@pdf-lib/fontkit`'s
|
|
143
221
|
shaping of the same bytes the PDF embeds, so a preview breaks its lines
|
|
144
222
|
where the document breaks them.
|
|
145
|
-
|
|
146
223
|
- **A box survives a page break.** An item or table row no page can hold
|
|
147
224
|
whole is drawn as slices, and a slice carries the box sides the break left
|
|
148
225
|
it: the top belongs to the first slice, the bottom to the last, and left and
|
|
@@ -151,7 +228,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
151
228
|
short one draws. Each slice reserves the bottom padding it may yet owe, so
|
|
152
229
|
that edge stays above the bottom margin, and each carries a hit box, so an
|
|
153
230
|
item that breaks is selectable on every page it reaches.
|
|
154
|
-
|
|
155
231
|
- **`paint` and `hit`.** A Canvas 2D painter for one page, and the hit-test
|
|
156
232
|
that maps a point on a page back to the schema node drawn there.
|
|
157
233
|
|
|
@@ -170,13 +246,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
170
246
|
|
|
171
247
|
- **`pageBox`.** The page-size table and its validation, in one home for the
|
|
172
248
|
PDF target, the viewer and the editor.
|
|
173
|
-
|
|
174
249
|
- **`PX_PER_POINT`.** How big a point is on screen — 96 dpi over PostScript's
|
|
175
250
|
72 — so every surface that shows a page shows it at the same size, and a
|
|
176
251
|
zoom is a factor on top of it.
|
|
177
|
-
|
|
178
|
-
### Changed
|
|
179
|
-
|
|
180
252
|
- **A split slot's box now fills the split's height.** Before, a slot's
|
|
181
253
|
background and border were exactly as tall as that slot's own content and
|
|
182
254
|
padding asked for, so any slot shorter than the tallest one drew a box that
|
|
@@ -189,13 +261,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
189
261
|
does, and a slot's hit box follows its painted one. A report that relied on
|
|
190
262
|
the short box — a slot background used as a chip beside taller content —
|
|
191
263
|
now draws it full height; give that content a narrower slot of its own.
|
|
192
|
-
|
|
193
264
|
- **A date string under `format: "date"` now presents as a date.** The text
|
|
194
265
|
join presents the kind through the engine's `format()` helper, which now
|
|
195
266
|
revives the two read forms. See the `quario` changelog for the forms.
|
|
196
|
-
|
|
197
|
-
### Fixed
|
|
198
|
-
|
|
199
267
|
- **A nested group's footer keeps the page columns.** Inside a page-column
|
|
200
268
|
region, the first footer of a group nested in it ended the region: the
|
|
201
269
|
strips collapsed and every band after it was laid out across the page, as
|
package/README.md
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
**The paged layout of a [quario](https://www.npmjs.com/package/quario) report, as a display
|
|
4
4
|
list.** `layout(options)` is a render target whose output is pages of boxes, text runs, rules
|
|
5
|
-
and images in points
|
|
6
|
-
so the preview breaks its pages exactly where the document does.
|
|
5
|
+
and images in points. The PDF target writes that layout, and the viewer and editor paint it on
|
|
6
|
+
screen, so the preview breaks its pages exactly where the document does.
|
|
7
7
|
|
|
8
8
|
You install this package directly only to consume the list yourself. `@quario/pdf`, `@quario/viewer`
|
|
9
9
|
and `@quario/editor` depend on it and run it for you.
|
|
@@ -38,11 +38,17 @@ list.pages[0].ops; // what to draw on the first page, in order
|
|
|
38
38
|
list.pages[0].boxes; // which schema node was drawn where
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
-
Every coordinate is in PostScript points from the page's top-left corner.
|
|
42
|
-
`
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
41
|
+
Every coordinate is in PostScript points from the page's top-left corner. `list.width` and
|
|
42
|
+
`list.height` are the paper every page is drawn on. A report has one paper, so a page does not
|
|
43
|
+
carry its own size.
|
|
44
|
+
|
|
45
|
+
A page carries:
|
|
46
|
+
|
|
47
|
+
- its `{ number, total }`,
|
|
48
|
+
- its ops: `rect`, `line`, `image`, and `text`. A `text` op holds the string to draw, the face it
|
|
49
|
+
uses, and, for a built-in family only, the per-character advances the measurer laid it out
|
|
50
|
+
with. An unlicensed render adds one `mark` op.
|
|
51
|
+
- its `boxes`, one per schema node drawn, named by the node's path.
|
|
46
52
|
|
|
47
53
|
## Options
|
|
48
54
|
|
|
@@ -51,7 +57,7 @@ named by the node's path.
|
|
|
51
57
|
| `page` | `{ size: "A4" \| "letter" \| [w, h], margin }` points | A4, 54pt |
|
|
52
58
|
| `fonts` | A font mapping: family name to TrueType bytes | none |
|
|
53
59
|
|
|
54
|
-
They are the pdf target's own `page` and `fonts
|
|
60
|
+
They are the pdf target's own `page` and `fonts`. Pass the same object to `layout()` and to
|
|
55
61
|
`pdf()` and the two agree, because the pdf target runs this layout and paints its list.
|
|
56
62
|
|
|
57
63
|
## Painting on a canvas
|
|
@@ -61,34 +67,43 @@ import { hit, paint } from "@quario/layout";
|
|
|
61
67
|
|
|
62
68
|
const canvas = document.querySelector("canvas");
|
|
63
69
|
const page = list.pages[0];
|
|
64
|
-
canvas.width =
|
|
65
|
-
canvas.height =
|
|
70
|
+
canvas.width = list.width * 2; // 2 device pixels per point
|
|
71
|
+
canvas.height = list.height * 2;
|
|
66
72
|
await paint(canvas.getContext("2d"), page, { scale: 2, fonts });
|
|
67
73
|
|
|
68
74
|
hit(page, x, y); // -> { path, x, y, w, h } or null
|
|
69
75
|
```
|
|
70
76
|
|
|
71
|
-
`paint` draws a white page and every op in order.
|
|
72
|
-
|
|
77
|
+
`paint` draws a white page and every op in order. Size the canvas to the page before you call it:
|
|
78
|
+
the ops are in the page's own coordinates, so a smaller canvas clips them, and `paint` fills the
|
|
79
|
+
white over the whole canvas. `paint` decodes images from the bytes on the list. No `img-src`
|
|
80
|
+
grant, no URLs. `hit` answers which schema node was drawn at a point: the smallest box
|
|
73
81
|
containing it.
|
|
74
82
|
|
|
75
|
-
Text is drawn in the face the document will use.
|
|
76
|
-
`FontFace` from your own bytes, and
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
measured advances
|
|
80
|
-
|
|
83
|
+
Text is drawn in the face the document will use. `paint` registers a family you pass as `fonts` as
|
|
84
|
+
a `FontFace` from your own bytes, and the browser then shapes its runs. That is the same shaping
|
|
85
|
+
the PDF gets from the same file, so ligatures, joined scripts and accents look on screen the way
|
|
86
|
+
they will on paper. `paint` draws text in the built-in families character by character at the
|
|
87
|
+
measured advances. The typeface a browser has for Helvetica, Times or Courier only stands in for
|
|
88
|
+
the one the PDF writes, and without that correction a line would drift as it ran.
|
|
81
89
|
|
|
82
90
|
Inside a single run of a family you supply, the preview is not promised to place glyphs exactly
|
|
83
|
-
where the PDF does
|
|
91
|
+
where the PDF does. Both read a glyph's own advance and neither applies the font's GPOS
|
|
84
92
|
positioning, so a browser may kern by a fraction more. Where a line breaks, how wide a column is,
|
|
85
93
|
and where a page ends are the layout's — one measurer, so preview and document agree.
|
|
86
94
|
|
|
87
|
-
`PX_PER_POINT` is how big a point is on screen — 96 dpi over PostScript's 72
|
|
88
|
-
a page at 100% sizes its canvas `
|
|
89
|
-
`PX_PER_POINT * devicePixelRatio`. Any zoom is a factor on top of that.
|
|
95
|
+
`PX_PER_POINT` is how big a point is on screen — 96 dpi over PostScript's 72. A surface that draws
|
|
96
|
+
a page at 100% therefore sizes its canvas `list.width * PX_PER_POINT` CSS pixels wide and paints
|
|
97
|
+
at `PX_PER_POINT * devicePixelRatio`. Any zoom is a factor on top of that.
|
|
98
|
+
|
|
99
|
+
## Documentation
|
|
100
|
+
|
|
101
|
+
[The quario documentation](https://getquario.com/docs/) is the reference.
|
|
102
|
+
The [report schema](https://getquario.com/docs/reference/report-schema/) is the normative
|
|
103
|
+
specification of what a report may declare, and
|
|
104
|
+
[`@quario/layout`](https://getquario.com/docs/reference/layout/) is this package's own API.
|
|
90
105
|
|
|
91
106
|
## License
|
|
92
107
|
|
|
93
|
-
quario is commercial software
|
|
94
|
-
unlicensed. See [LICENSE](https://getquario.com/license).
|
|
108
|
+
quario is commercial software. Evaluation is free and fully featured, and quario marks its output
|
|
109
|
+
as unlicensed. See [LICENSE](https://getquario.com/license).
|
package/lib/canvas.js
CHANGED
|
@@ -140,7 +140,10 @@ let listing = (box, render) => {
|
|
|
140
140
|
|
|
141
141
|
// Open a fresh page and put the cursor at its top.
|
|
142
142
|
let newPage = () => {
|
|
143
|
-
|
|
143
|
+
// No geometry: the paper is the list's, written once from this canvas's
|
|
144
|
+
// own frame, because the page box is fixed for the whole document (ADR
|
|
145
|
+
// 0069). A page carries what is per-page.
|
|
146
|
+
page = { ops: [], boxes: [] };
|
|
144
147
|
pages.push(page);
|
|
145
148
|
canvas.y = canvas.top;
|
|
146
149
|
canvas.fresh = true;
|
package/lib/fonts.js
CHANGED
|
@@ -165,8 +165,8 @@ let useFontkit = async () => {
|
|
|
165
165
|
let asFamily = (name, def, at) => {
|
|
166
166
|
let path = at + "." + name;
|
|
167
167
|
if (!def || typeof def !== "object")
|
|
168
|
-
throw
|
|
169
|
-
if (def.regular == null) throw
|
|
168
|
+
throw TypeError(path + ": expected { regular, bold?, italic?, boldItalic? }");
|
|
169
|
+
if (def.regular == null) throw TypeError(path + ".regular: required");
|
|
170
170
|
return path;
|
|
171
171
|
};
|
|
172
172
|
|
|
@@ -279,6 +279,10 @@ export async function loadFonts(custom) {
|
|
|
279
279
|
return { families };
|
|
280
280
|
}
|
|
281
281
|
|
|
282
|
+
// An array is a `typeof "object"` whose families would be named 0, 1, 2.
|
|
283
|
+
/** @type {(custom: any) => boolean} */
|
|
284
|
+
let record = (custom) => typeof custom === "object" && !Array.isArray(custom);
|
|
285
|
+
|
|
282
286
|
/**
|
|
283
287
|
* Check the shape of a host's font mapping without loading a parser.
|
|
284
288
|
*
|
|
@@ -295,7 +299,7 @@ export async function loadFonts(custom) {
|
|
|
295
299
|
*/
|
|
296
300
|
export let checkFonts = (custom, at = "options.fonts") => {
|
|
297
301
|
if (custom == null) return;
|
|
298
|
-
if (
|
|
302
|
+
if (!record(custom)) throw TypeError(at + ": expected a record of families");
|
|
299
303
|
for (let [name, def] of Object.entries(custom)) asFamily(name, def, at);
|
|
300
304
|
};
|
|
301
305
|
|
package/lib/index.d.ts
CHANGED
|
@@ -22,7 +22,11 @@ export interface LayoutFontFamily {
|
|
|
22
22
|
/** The font mapping: a declared `family` name to the bytes it measures against. */
|
|
23
23
|
export type LayoutFonts = Record<string, LayoutFontFamily>;
|
|
24
24
|
|
|
25
|
-
/**
|
|
25
|
+
/**
|
|
26
|
+
* Host controls, taken and validated at the factory call: an option this
|
|
27
|
+
* target does not understand, or one of the wrong type, throws a `TypeError`
|
|
28
|
+
* there rather than costing the host the option in silence.
|
|
29
|
+
*/
|
|
26
30
|
export interface LayoutOptions {
|
|
27
31
|
page?: LayoutPage;
|
|
28
32
|
/**
|
|
@@ -159,12 +163,13 @@ export interface Box {
|
|
|
159
163
|
* One page of the list. Coordinates are points from the page's top-left
|
|
160
164
|
* corner, `y` descending; `number` and `total` are what `page.number` and
|
|
161
165
|
* `page.total` read on it.
|
|
166
|
+
*
|
|
167
|
+
* A page carries what is per-page and nothing else: the `Paper` it is drawn on
|
|
168
|
+
* is the list's (ADR 0069).
|
|
162
169
|
*/
|
|
163
170
|
export interface Page {
|
|
164
171
|
number: number;
|
|
165
172
|
total: number;
|
|
166
|
-
width: number;
|
|
167
|
-
height: number;
|
|
168
173
|
ops: Op[];
|
|
169
174
|
boxes: Box[];
|
|
170
175
|
}
|
|
@@ -178,11 +183,23 @@ export interface Mark {
|
|
|
178
183
|
y: number;
|
|
179
184
|
}
|
|
180
185
|
|
|
181
|
-
/**
|
|
182
|
-
|
|
186
|
+
/**
|
|
187
|
+
* The [paper](../../../CONTEXT.md#paper) a report is drawn on, in points: one
|
|
188
|
+
* width and one height for a whole render. The page box is fixed once the
|
|
189
|
+
* render opens, so this is the list's and a page carries none of its own (ADR
|
|
190
|
+
* 0069) — a consumer sizing a page reads it here.
|
|
191
|
+
*/
|
|
192
|
+
export interface Paper {
|
|
183
193
|
width: number;
|
|
184
194
|
height: number;
|
|
185
|
-
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** The display list: every page of the document, laid out, on one `Paper`. */
|
|
198
|
+
export interface Layout extends Paper {
|
|
199
|
+
/** Every page, and never none: the band flow opens the body's first page
|
|
200
|
+
* whether or not anything renders on it. Typed as a non-empty tuple so a
|
|
201
|
+
* consumer reading `pages[0]` needs no check for a list that cannot exist. */
|
|
202
|
+
pages: [Page, ...Page[]];
|
|
186
203
|
/** The group tree, in document order — the PDF target's bookmarks. */
|
|
187
204
|
marks: Mark[];
|
|
188
205
|
}
|
|
@@ -206,11 +223,17 @@ export const PX_PER_POINT: number;
|
|
|
206
223
|
* order. `scale` is device pixels per point. `fonts` is the same record
|
|
207
224
|
* given to `layout()`, so a TrueType family draws in its own face.
|
|
208
225
|
*
|
|
226
|
+
* **Size the canvas to the page first** — `Layout.width` and `Layout.height`
|
|
227
|
+
* times `scale`, the paper every page of the list is drawn on. The ops are in
|
|
228
|
+
* the page's own coordinates, so a canvas short of that clips them, and the
|
|
229
|
+
* white is filled over the whole canvas rather than over a rectangle a page
|
|
230
|
+
* carries (ADR 0069).
|
|
231
|
+
*
|
|
209
232
|
* It awaits its faces and images before it draws, and it draws whatever
|
|
210
|
-
* happened in between: a canvas re-sized under a call still in flight
|
|
211
|
-
*
|
|
212
|
-
* repaints one canvas at changing scales owns that, by not letting
|
|
213
|
-
* superseded call reach a canvas still on screen.
|
|
233
|
+
* happened in between: a canvas re-sized under a call still in flight has its
|
|
234
|
+
* ops placed at that call's own `scale` on a canvas of the size it now has. A
|
|
235
|
+
* caller that repaints one canvas at changing scales owns that, by not letting
|
|
236
|
+
* a superseded call reach a canvas still on screen.
|
|
214
237
|
*/
|
|
215
238
|
export function paint(
|
|
216
239
|
ctx: CanvasRenderingContext2D,
|
package/lib/index.js
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* driver dispatches to is the band flow's, next door in `layout.js`; this
|
|
21
21
|
* file adds none. The package publishes `lib/` verbatim.
|
|
22
22
|
*/
|
|
23
|
-
import { breathe, walk } from "quario";
|
|
23
|
+
import { breathe, hostOptions, walk } from "quario";
|
|
24
24
|
import { listing, stamp } from "./canvas.js";
|
|
25
25
|
import { checkFonts, loadFonts } from "./fonts.js";
|
|
26
26
|
import { flow, furniture } from "./layout.js";
|
|
@@ -58,6 +58,19 @@ let markPages = async (canvas, marking) => {
|
|
|
58
58
|
await overPages(canvas, () => canvas.watermark(mark));
|
|
59
59
|
};
|
|
60
60
|
|
|
61
|
+
// This target's whole option contract, checked at the factory call: the key
|
|
62
|
+
// sets, then the geometry and the font mapping's shape. The `page` key set
|
|
63
|
+
// closes here rather than in `pageBox`, which the two surface elements also
|
|
64
|
+
// call -- a host configures those by assigning DOM properties, and nothing can
|
|
65
|
+
// refuse an unknown one (`docs/adr/0072`).
|
|
66
|
+
/** @type {(options: any) => void} */
|
|
67
|
+
let checkOptions = (options) => {
|
|
68
|
+
hostOptions(options, ["page", "fonts"], "options");
|
|
69
|
+
hostOptions(options?.page, ["size", "margin"], "options.page");
|
|
70
|
+
geometry(options?.page);
|
|
71
|
+
checkFonts(options?.fonts);
|
|
72
|
+
};
|
|
73
|
+
|
|
61
74
|
/**
|
|
62
75
|
* The layout target:
|
|
63
76
|
* `quario().report(schema).render(layout({ page }), data)` resolves the
|
|
@@ -70,8 +83,7 @@ let markPages = async (canvas, marking) => {
|
|
|
70
83
|
* The target (see SCHEMA.md, "Instances and targets").
|
|
71
84
|
*/
|
|
72
85
|
export function layout(options) {
|
|
73
|
-
|
|
74
|
-
checkFonts(options?.fonts);
|
|
86
|
+
checkOptions(options);
|
|
75
87
|
let custom = options?.fonts;
|
|
76
88
|
/** @type {(stream: any) => (data?: any) => Promise<Layout>} */
|
|
77
89
|
let compile = (stream) => async (data) => {
|
package/lib/layout.js
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* document's bands claim before anything is placed, and where they are drawn
|
|
22
22
|
* on each finished page once the count is known.
|
|
23
23
|
*/
|
|
24
|
-
import { display,
|
|
24
|
+
import { display, isReportBand, text } from "quario";
|
|
25
25
|
import { balance } from "./balance.js";
|
|
26
26
|
/** `Measured` is the measured table's own type, and the flow passes one
|
|
27
27
|
* through: the region buffer measures early and the replay hands it back.
|
|
@@ -35,7 +35,6 @@ import { balance } from "./balance.js";
|
|
|
35
35
|
import { gridOf, measureTable, sum, tableOf } from "./measure.js";
|
|
36
36
|
import { adopt, measuring } from "./canvas.js";
|
|
37
37
|
import { frame } from "./page.js";
|
|
38
|
-
import { intrinsic } from "./image.js";
|
|
39
38
|
import { CELL_PAD, NO_PAD, WHOLE, insetOf, isWhole, paintBox, sliceInset, unbox } from "./box.js";
|
|
40
39
|
import {
|
|
41
40
|
BAND,
|
|
@@ -122,19 +121,22 @@ let unsliceable = (block) => Boolean(block.picture || block.parts);
|
|
|
122
121
|
// image's own size in points, never wider than the content box; `width`
|
|
123
122
|
// scales to the content box either way. The ratio is kept in both, so a
|
|
124
123
|
// height is never anything but the width's consequence.
|
|
124
|
+
// An image pixel as a page point, at the conventional 96 dpi the schema names
|
|
125
|
+
// for `fit: "natural"`. Deliberately not `PX_PER_POINT`, which is the same
|
|
126
|
+
// number answering a different question -- how a point becomes a length on
|
|
127
|
+
// screen, where a zoom is a factor on top of it. An image's resolution and a
|
|
128
|
+
// viewer's scale share a convention today and are free to stop.
|
|
129
|
+
let PER_PX = 72 / 96;
|
|
130
|
+
|
|
125
131
|
/** @type {(event: any, avail: number) => Picture} */
|
|
126
132
|
let pictureOf = (event, avail) => {
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
//
|
|
130
|
-
//
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
} catch (cause) {
|
|
135
|
-
throw imageError(event.path, /** @type {Error} */ (cause).message, cause);
|
|
136
|
-
}
|
|
137
|
-
let { w, h } = sized;
|
|
133
|
+
// The event states the size in pixels; a page is measured in points. The
|
|
134
|
+
// conversion is the first step of the same calculation the rest of this
|
|
135
|
+
// function is, at the conventional 96 dpi the schema names, and there is no
|
|
136
|
+
// header to read: the engine read it once, for every target, and a file too
|
|
137
|
+
// short to state a size never reached a target at all (`docs/adr/0067`).
|
|
138
|
+
let w = event.width * PER_PX;
|
|
139
|
+
let h = event.height * PER_PX;
|
|
138
140
|
let width = event.fit === "width" ? avail : Math.min(w, avail);
|
|
139
141
|
return {
|
|
140
142
|
bytes: event.bytes,
|
|
@@ -195,23 +197,24 @@ let shrink = (block, room) => {
|
|
|
195
197
|
block.h = inner + block.inset.t + block.inset.b;
|
|
196
198
|
};
|
|
197
199
|
|
|
198
|
-
// Draw one block whole at the cursor.
|
|
199
|
-
//
|
|
200
|
-
//
|
|
201
|
-
//
|
|
200
|
+
// Draw one block whole at the cursor. The box is the width the block was
|
|
201
|
+
// given, exactly as `drawLines` paints one: an image's background and border
|
|
202
|
+
// span the content width, or the slot's share inside a split, and never the
|
|
203
|
+
// picture's own width. `fit` sizes the picture and `align` places it in the
|
|
204
|
+
// inner box; neither moves an edge. Text is placed line by line as each is
|
|
205
|
+
// drawn; a picture is one box, so where it sits is worked out once.
|
|
202
206
|
/** @type {(canvas: Canvas, block: Block, x: number, avail: number) => void} */
|
|
203
207
|
let drawPicture = (canvas, block, x, avail) => {
|
|
204
208
|
let inset = block.inset;
|
|
205
209
|
let { bytes, format, w, h } = /** @type {Picture} */ (block.picture);
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
canvas.box(block.path, at, canvas.y, outer, block.h);
|
|
210
|
+
paintBox(canvas, x, canvas.y, avail, block.h, block.style, block.bg);
|
|
211
|
+
canvas.box(block.path, x, canvas.y, avail, block.h);
|
|
212
|
+
let inner = Math.max(avail - inset.l - inset.r, 1);
|
|
210
213
|
canvas.picture(
|
|
211
214
|
block.path,
|
|
212
215
|
bytes,
|
|
213
216
|
format,
|
|
214
|
-
|
|
217
|
+
x + inset.l + shift(block.align, inner - w),
|
|
215
218
|
canvas.y - inset.t - block.drop - h,
|
|
216
219
|
w,
|
|
217
220
|
h,
|
package/lib/page.js
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
// The named sizes. `@quario/pdf` used to own this table, and the two element
|
|
17
17
|
// packages restated it; the layout package is where all three now read it.
|
|
18
|
+
// fallow-ignore-next-line code-duplication -- @quario/docx keeps a deliberate copy of these rules: a flow target runs no layout, and taking @quario/layout for a two-entry table would install the pagination engine with it. test/page-sizes.test.js holds the two in sync (ADR 0039).
|
|
18
19
|
let SIZES = /** @type {Record<string, [number, number]>} */ ({
|
|
19
20
|
A4: [595.28, 841.89],
|
|
20
21
|
letter: [612, 792],
|
|
@@ -22,7 +23,7 @@ let SIZES = /** @type {Record<string, [number, number]>} */ ({
|
|
|
22
23
|
|
|
23
24
|
/** @type {(msg: string) => never} */
|
|
24
25
|
let err = (msg) => {
|
|
25
|
-
throw
|
|
26
|
+
throw TypeError(msg);
|
|
26
27
|
};
|
|
27
28
|
|
|
28
29
|
/** @type {(value: number) => boolean} */
|
package/lib/paint.js
CHANGED
|
@@ -235,18 +235,20 @@ let decode = async (page) => {
|
|
|
235
235
|
/**
|
|
236
236
|
* Paint one page of the list onto a Canvas 2D context: a white page, then
|
|
237
237
|
* every op in order. `scale` is device pixels per point — the caller sized
|
|
238
|
-
* the canvas, so it knows
|
|
238
|
+
* the canvas, so it knows, and the canvas it sized is the page's own size,
|
|
239
|
+
* which is why the paper is filled over the whole store and no page geometry
|
|
240
|
+
* is asked for (ADR 0069). `fonts` is the host's font mapping, the same
|
|
239
241
|
* record given to `layout()`, so a TrueType family draws in its own face.
|
|
240
242
|
*
|
|
241
243
|
* It awaits its faces and images before it draws, and it draws whatever
|
|
242
|
-
* happened in between: a canvas re-sized under a call still in flight
|
|
243
|
-
*
|
|
244
|
-
* repaints one canvas at changing scales owns that, by not letting
|
|
245
|
-
* superseded call reach a canvas still on screen — which is what the
|
|
246
|
-
* stage retires a page for (docs/adr/0046).
|
|
244
|
+
* happened in between: a canvas re-sized under a call still in flight has its
|
|
245
|
+
* ops placed at that call's own `scale` on a store of the size it now has. A
|
|
246
|
+
* caller that repaints one canvas at changing scales owns that, by not letting
|
|
247
|
+
* a superseded call reach a canvas still on screen — which is what the
|
|
248
|
+
* viewer's stage retires a page for (docs/adr/0046).
|
|
247
249
|
*
|
|
248
250
|
* **An image the browser will not decode is drawn as nothing, and the page is
|
|
249
|
-
* drawn around it.** The engine vouched for the magic numbers and
|
|
251
|
+
* drawn around it.** The engine vouched for the magic numbers and
|
|
250
252
|
* read the size out of the header, so a file corrupt past that point is not
|
|
251
253
|
* known to be bad until here; costing the whole page for it — every other op
|
|
252
254
|
* and the marking with it — is a worse answer than costing the image. What is
|
|
@@ -263,9 +265,16 @@ export async function paint(ctx, page, { scale = 1, fonts } = {}) {
|
|
|
263
265
|
await loadFaces(fonts);
|
|
264
266
|
let bitmaps = await decode(page);
|
|
265
267
|
ctx.save();
|
|
266
|
-
|
|
268
|
+
// The paper, over the whole store rather than over the page's own box: a
|
|
269
|
+
// page carries no geometry to fill to (ADR 0069), and the caller has already
|
|
270
|
+
// sized the canvas to the page — the obligation the ops themselves put on it,
|
|
271
|
+
// since a short store clips them. In device pixels, before the scale goes on,
|
|
272
|
+
// so a store rounded up to a whole pixel is covered to its edge rather than
|
|
273
|
+
// left a sliver of whatever it held.
|
|
274
|
+
ctx.resetTransform();
|
|
267
275
|
ctx.fillStyle = "#fff";
|
|
268
|
-
ctx.fillRect(0, 0,
|
|
276
|
+
ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
|
277
|
+
ctx.setTransform(scale, 0, 0, scale, 0, 0);
|
|
269
278
|
for (let op of page.ops) paintOp(ctx, op, bitmaps);
|
|
270
279
|
ctx.restore();
|
|
271
280
|
}
|
package/lib/style.js
CHANGED
|
@@ -54,6 +54,7 @@ let vshift = (valign, extra) => (Object.hasOwn(VSHIFT, valign) ? VSHIFT[valign]
|
|
|
54
54
|
|
|
55
55
|
// A declared colour as a `Color`, or null when the value is not one.
|
|
56
56
|
/** @type {(value: any) => Color | null} */
|
|
57
|
+
// fallow-ignore-next-line code-duplication -- each target reads a declared hex into its own type; neither may import a sibling, and the engine's stream is all they share
|
|
57
58
|
let col = (value) => {
|
|
58
59
|
let match = typeof value === "string" && HEX.exec(value);
|
|
59
60
|
if (!match) return null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quario/layout",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "The paged display list for quario — the layout the PDF target writes and the viewer paints — in the makings, not yet released",
|
|
5
5
|
"homepage": "https://getquario.com",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE",
|
|
@@ -43,13 +43,13 @@
|
|
|
43
43
|
"@size-limit/preset-small-lib": "^13.0.3",
|
|
44
44
|
"@types/fontkit": "^2.0.9",
|
|
45
45
|
"fontkit": "^2.0.4",
|
|
46
|
-
"quario": "^0.
|
|
46
|
+
"quario": "^0.9.0",
|
|
47
47
|
"size-limit": "^13.0.3",
|
|
48
48
|
"typescript": "^7.0.2"
|
|
49
49
|
},
|
|
50
50
|
"peerDependencies": {
|
|
51
51
|
"fontkit": "^2.0.4",
|
|
52
|
-
"quario": "^0.
|
|
52
|
+
"quario": "^0.9.0"
|
|
53
53
|
},
|
|
54
54
|
"peerDependenciesMeta": {
|
|
55
55
|
"fontkit": {
|
package/lib/image.js
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* An image's own size, read from its header. Sizing is a fact about the bytes
|
|
3
|
-
* rather than anything a page can answer, so it sits here and not on the
|
|
4
|
-
* drawing surface: the band flow asks for it while measuring, where no
|
|
5
|
-
* document is in reach at all, and again while drawing, where one is.
|
|
6
|
-
*
|
|
7
|
-
* Read, never decoded — a PNG's IHDR and a JPEG's frame header carry the two
|
|
8
|
-
* numbers the layout needs, and nothing else here looks at a pixel.
|
|
9
|
-
*
|
|
10
|
-
* The XLSX target reads the same two headers for its own placement, in pixels.
|
|
11
|
-
* Whether that fact belongs on the engine's event instead — it already reads
|
|
12
|
-
* the magic numbers to name the format — is bead `quario-b0h`.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
// Points, at the conventional 96 dpi the schema names: 72/96 of a pixel.
|
|
16
|
-
let PER_PX = 72 / 96;
|
|
17
|
-
|
|
18
|
-
/** @type {(bytes: Uint8Array, at: number) => number} */
|
|
19
|
-
let word = (bytes, at) => (bytes[at] << 8) | bytes[at + 1];
|
|
20
|
-
|
|
21
|
-
/** @type {(code: number) => boolean} */
|
|
22
|
-
let inSof = (code) => code >= 0xc0 && code <= 0xcf;
|
|
23
|
-
|
|
24
|
-
/** @type {(code: number) => boolean} */
|
|
25
|
-
let notTable = (code) => code !== 0xc4 && code !== 0xc8 && code !== 0xcc;
|
|
26
|
-
|
|
27
|
-
// The frame header carries the dimensions, and it is the first SOFn marker:
|
|
28
|
-
// every code in C0..CF except the three in that range that are not frames --
|
|
29
|
-
// DHT, the JPG extension, and DAC.
|
|
30
|
-
/** @type {(code: number) => boolean} */
|
|
31
|
-
let isFrame = (code) => inSof(code) && notTable(code);
|
|
32
|
-
|
|
33
|
-
/** @type {(bytes: Uint8Array) => { w: number, h: number }} */
|
|
34
|
-
let jpegSize = (bytes) => {
|
|
35
|
-
for (let at = 2; at + 9 < bytes.length; at += 2 + word(bytes, at + 2)) {
|
|
36
|
-
if (bytes[at] !== 0xff) break;
|
|
37
|
-
if (isFrame(bytes[at + 1])) return { w: word(bytes, at + 7), h: word(bytes, at + 5) };
|
|
38
|
-
}
|
|
39
|
-
return { w: 0, h: 0 };
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* The image's intrinsic size in points. The format is the engine's sniff,
|
|
44
|
-
* riding on the event, so nothing here decides it a second time.
|
|
45
|
-
*
|
|
46
|
-
* @param {Uint8Array} bytes The image file.
|
|
47
|
-
* @param {string} format The engine's sniff: `"png"` or `"jpeg"`.
|
|
48
|
-
* @returns {{ w: number, h: number }} The size, in points.
|
|
49
|
-
*/
|
|
50
|
-
export let intrinsic = (bytes, format) => {
|
|
51
|
-
// A PNG carries the two numbers in its IHDR at a fixed offset.
|
|
52
|
-
let { w, h } = format === "png" ? { w: word(bytes, 18), h: word(bytes, 22) } : jpegSize(bytes);
|
|
53
|
-
// The engine vouched for the magic numbers, not for the rest of the file:
|
|
54
|
-
// a truncated header reaches here as a zero, and failing loudly beats
|
|
55
|
-
// drawing an image with no size (SCHEMA.md, "Image item").
|
|
56
|
-
if (!(w > 0 && h > 0)) throw Error("could not read the image's size from its bytes");
|
|
57
|
-
return { w: w * PER_PX, h: h * PER_PX };
|
|
58
|
-
};
|