@quario/layout 0.4.0 → 0.5.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 CHANGED
@@ -1,15 +1,69 @@
1
- # Changelog
2
-
3
- All notable changes to @quario/layout are documented in this file.
4
-
5
- The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
- and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
-
8
- ## [Unreleased]
9
-
10
- ## [0.4.0] - 2026-09-07
11
-
12
- ### Added
1
+ # @quario/layout
2
+
3
+ ## 0.5.0
4
+
5
+ ### Minor Changes
6
+
7
+ - **An image's box is now as wide as the container it sits in.** A
8
+ `background` or `border*` on an image item hugged the picture and now spans
9
+ the content width — or the slot's share inside a split — with `fit` sizing
10
+ the picture and `align` placing it inside, the way a text item's box has
11
+ always been drawn. The picture itself does not move. Breaking: a report that
12
+ relied on a border hugging a logo now draws that border across the full
13
+ width, and no declaration asks for the old shape.
14
+ - **A page of the display list no longer carries `width` and `height`.** Read
15
+ the list's own `width` and `height` instead: they are the paper every page of
16
+ the report is drawn on, and every page's pair was a copy of them. A report has
17
+ one page size for the whole render — that has always been so, and page size is
18
+ a target option rather than something a document declares — so a page is
19
+ identified and positioned, and asked nothing about its size. A page now
20
+ carries `{ number, total, ops, boxes }`.
21
+
22
+ If you consume the list yourself, the fix is one substitution:
23
+
24
+ ```js
25
+ canvas.width = list.width * 2; // was list.pages[i].width * 2
26
+ canvas.height = list.height * 2; // was list.pages[i].height * 2
27
+ ```
28
+
29
+ `paint()` is unchanged in shape — it still takes one page — and it now fills
30
+ the white over the whole canvas rather than over the page's own rectangle.
31
+ **Size the canvas to the page before you call it**, as the sizing example above
32
+ does: the ops are in the page's own coordinates, so a canvas short of the paper
33
+ clips them whatever the fill covers. That was always so; it is now written down
34
+ in the README and on `paint()` itself.
35
+
36
+ - **`Layout.pages` is typed as a non-empty list.** A render always lays out at
37
+ least one page — the first page opens whether or not anything renders on it —
38
+ so `pages[0]` no longer needs a check for a list that cannot exist. Runtime
39
+ behaviour is unchanged; this only states in the type what was already true.
40
+ - **`Paper`** — the page box a report is drawn on, `{ width, height }` in points,
41
+ and what a display list's own `width` and `height` are. `Layout` extends it, so
42
+ `list.width` is unchanged and the pair now has a name to refer to.
43
+
44
+ ### Patch Changes
45
+
46
+ - **A page's right and bottom edge could be left unpainted.** Where a caller
47
+ rounds the backing store up to a whole device pixel — A4 at 100% asks for
48
+ 793.71 and takes 794 — `paint()` filled its white over the page's exact
49
+ rectangle and left the fraction past it transparent. On a canvas with white
50
+ behind it that is invisible; on one without, a hairline of whatever is behind
51
+ the canvas showed along two edges. The fill now covers the whole canvas.
52
+ - **Every published README says where the documentation is.** Each package now
53
+ carries a Documentation section pointing at the reference, at the report schema
54
+ that normatively specifies what a report may declare, and at the package's own
55
+ API. The paragraphs that used to end on an unstated contract — the event
56
+ stream's field semantics, the style vocabulary, page columns, the Content
57
+ Security Policy a fragment with images needs, the formula mangling, and each
58
+ target's own contract — link the page that states it. Every link is an absolute
59
+ URL, so it resolves from the npm package page as readily as from an installed
60
+ copy.
61
+ - Updated dependencies
62
+ - quario@0.8.0
63
+
64
+ ## 0.4.0
65
+
66
+ ### Minor Changes
13
67
 
14
68
  - **Per-run typography, decoration and highlights.** A styled run resolves its
15
69
  own face, size and colour, and a line takes the largest size among its runs.
@@ -17,31 +71,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
17
71
  that run's colour — where they used to stroke the whole line in the first
18
72
  piece's colour. A run's `background` paints a highlight rectangle behind its
19
73
  text; a cell's own background is still painted once, at cell scope.
20
-
21
74
  - **`checkFonts` is public.** It checks the shape of a `fonts` mapping without
22
75
  loading a parser, and takes an optional name to prefix a failure with, the
23
76
  way `pageBox` does — so a surface validating its own `fonts` property can
24
77
  report the mistake against that property rather than against
25
78
  `options.fonts`.
26
-
27
- ### Fixed
28
-
29
79
  - **An outline `Mark` carries only what it declares.** Every mark shipped an
30
80
  extra `titled` boolean — bookkeeping for whether a header had claimed the
31
81
  entry — which was never part of the `Mark` interface and which nothing
32
82
  reads. It is the open group instance's own state now, and no longer travels
33
83
  on the object a consumer receives.
34
84
 
35
- ## [0.3.0] - 2026-09-07
85
+ ## 0.3.0
36
86
 
37
- ### Added
87
+ ### Minor Changes
38
88
 
39
89
  - The text join reads a cell's own `currency` code, ahead of the instance's
40
90
  default, so every target built on this package presents a per-cell
41
91
  denomination.
42
-
43
- ### Changed
44
-
45
92
  - **A group instance that renders nothing no longer takes up space.** A group
46
93
  whose header and footer items all resolve `visible: false`, with nothing
47
94
  visible under it either, used to open the same half-line gap as any other
@@ -52,7 +99,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
52
99
  its rows stay in the aggregates. A group that declares `break: "page"` or
53
100
  `reset: "page"` still starts its page either way. Every document with such a
54
101
  group renders slightly shorter than it did.
55
-
56
102
  - **Numbers presented through `format` now show a fixed two fraction digits,
57
103
  matching every other target** — `1,000.00` where `1,000` was rendered,
58
104
  `21.00%` where `21%` was, and a currency's own minor units in place of a
@@ -60,15 +106,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
60
106
  a formatted cell is now up to three characters wider than it was: a line
61
107
  that just fitted can wrap, which can move a page break in a PDF, the viewer,
62
108
  or an editor preview. Nothing else about wrapping changed.
63
-
64
- ### Fixed
65
-
66
109
  - **An image failure now names the item that asked for the bytes.** A file too
67
110
  short to carry a size failed saying only that the size could not be read,
68
111
  naming no item, so a report with two pictures gave no way to tell which one
69
112
  was bad. The message is now prefixed with the item's `source` path, as every
70
113
  other render error is.
71
-
72
114
  - **An image the browser cannot decode no longer costs the whole page.** A
73
115
  PNG or JPEG whose pixel data is corrupt past the size in its header lays out
74
116
  like any other — the size is all that is read of it — and used to throw out
@@ -77,9 +119,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77
119
  drawn as nothing and the page is drawn around it, so what a bad image costs
78
120
  is the image.
79
121
 
80
- ## [0.2.0] - 2026-09-05
122
+ ## 0.2.0
81
123
 
82
- ### Added
124
+ ### Minor Changes
83
125
 
84
126
  - **A spanning cell is drawn as one box** across the columns it covers,
85
127
  starting where the first of them starts. It takes no part in allocating their
@@ -87,21 +129,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
87
129
  empty table still shows its geometry. A spanning row too tall for the page it
88
130
  is on slices like any other, each slice following the geometry of the page or
89
131
  page-column strip it lands in.
90
-
91
132
  - **`valign`** places a cell's lines, or a slot's lines or picture, in the slack
92
133
  its row or split leaves over its own content: middle halves it, bottom takes
93
134
  it. The box does not move, and a picture is not scaled. A row too tall for
94
135
  any page, sliced across pages, paints from the top.
95
-
96
- ### Changed
97
-
98
136
  - **A row's box is drawn by the row's cells.** A box declared on a table row
99
137
  used to be one rect across the summed column widths; it is now each covered
100
138
  cell's own, so a row's `borderBottom` still reads as one continuous edge
101
139
  while a row's `borderLeft` becomes an edge on every cell rather than one at
102
140
  the row's outer left. A row's border also occupies height now, as a cell's
103
141
  always has, so a bordered row is taller by its border's width.
104
-
105
142
  - **Measuring TrueType families now needs `fontkit`, not `@pdf-lib/fontkit`.**
106
143
  Install `fontkit` instead; nothing else about `options.fonts` changes. The
107
144
  old package's bundle crashed with a bare `ReferenceError` on any OpenType
@@ -110,9 +147,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
110
147
  besides. Those faces now measure where they used to throw. Which scripts a
111
148
  face supports remains the font's and the parser's to answer, not this
112
149
  package's.
113
-
114
- ### Fixed
115
-
116
150
  - **A group header no longer strands above a split.** A split cannot be broken
117
151
  across a page, so a header that introduces one has to keep the whole of it
118
152
  company — but the keep-together test measured a split by its first two lines
@@ -127,9 +161,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
127
161
  always has before content no page can hold: it degrades to plain paginated
128
162
  flow and repeats nothing.
129
163
 
130
- ## [0.1.0] - 2026-09-03
164
+ ## 0.1.0
131
165
 
132
- ### Added
166
+ ### Minor Changes
133
167
 
134
168
  - **The paged display list.** `layout({ page, fonts })` is a render target
135
169
  resolving every page of the report — ops in points from the top-left, one
@@ -137,12 +171,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
137
171
  marks, and on an unlicensed render the marking. The algorithm is the PDF
138
172
  target's typesetter, with its home moved here so a preview can paint the
139
173
  same pages the document has.
140
-
141
174
  - **One measurer.** Base-14 families measure against `@pdf-lib/standard-fonts`'
142
175
  AFM metrics over WinAnsi, TrueType families against `@pdf-lib/fontkit`'s
143
176
  shaping of the same bytes the PDF embeds, so a preview breaks its lines
144
177
  where the document breaks them.
145
-
146
178
  - **A box survives a page break.** An item or table row no page can hold
147
179
  whole is drawn as slices, and a slice carries the box sides the break left
148
180
  it: the top belongs to the first slice, the bottom to the last, and left and
@@ -151,7 +183,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
151
183
  short one draws. Each slice reserves the bottom padding it may yet owe, so
152
184
  that edge stays above the bottom margin, and each carries a hit box, so an
153
185
  item that breaks is selectable on every page it reaches.
154
-
155
186
  - **`paint` and `hit`.** A Canvas 2D painter for one page, and the hit-test
156
187
  that maps a point on a page back to the schema node drawn there.
157
188
 
@@ -170,13 +201,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
170
201
 
171
202
  - **`pageBox`.** The page-size table and its validation, in one home for the
172
203
  PDF target, the viewer and the editor.
173
-
174
204
  - **`PX_PER_POINT`.** How big a point is on screen — 96 dpi over PostScript's
175
205
  72 — so every surface that shows a page shows it at the same size, and a
176
206
  zoom is a factor on top of it.
177
-
178
- ### Changed
179
-
180
207
  - **A split slot's box now fills the split's height.** Before, a slot's
181
208
  background and border were exactly as tall as that slot's own content and
182
209
  padding asked for, so any slot shorter than the tallest one drew a box that
@@ -189,13 +216,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
189
216
  does, and a slot's hit box follows its painted one. A report that relied on
190
217
  the short box — a slot background used as a chip beside taller content —
191
218
  now draws it full height; give that content a narrower slot of its own.
192
-
193
219
  - **A date string under `format: "date"` now presents as a date.** The text
194
220
  join presents the kind through the engine's `format()` helper, which now
195
221
  revives the two read forms. See the `quario` changelog for the forms.
196
-
197
- ### Fixed
198
-
199
222
  - **A nested group's footer keeps the page columns.** Inside a page-column
200
223
  region, the first footer of a group nested in it ended the region: the
201
224
  strips collapsed and every band after it was laid out across the page, as
package/README.md CHANGED
@@ -38,8 +38,9 @@ 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. A page carries its
42
- `{ number, total }`, its ops`rect`, `line`, `image`, `text` (the string to draw, the face
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, so a page does not carry its
43
+ own size. A page carries its `{ number, total }`, its ops — `rect`, `line`, `image`, `text` (the string to draw, the face
43
44
  it is set in, and — for a built-in family only — the per-character advances the measurer laid it
44
45
  out with) and, on an unlicensed render, one `mark` — and its `boxes`, one per schema node drawn,
45
46
  named by the node's path.
@@ -61,14 +62,16 @@ import { hit, paint } from "@quario/layout";
61
62
 
62
63
  const canvas = document.querySelector("canvas");
63
64
  const page = list.pages[0];
64
- canvas.width = page.width * 2; // 2 device pixels per point
65
- canvas.height = page.height * 2;
65
+ canvas.width = list.width * 2; // 2 device pixels per point
66
+ canvas.height = list.height * 2;
66
67
  await paint(canvas.getContext("2d"), page, { scale: 2, fonts });
67
68
 
68
69
  hit(page, x, y); // -> { path, x, y, w, h } or null
69
70
  ```
70
71
 
71
- `paint` draws a white page and every op in order. Images are decoded from the bytes on the list
72
+ `paint` draws a white page and every op in order. Size the canvas to the page before you call it:
73
+ the ops are in the page's own coordinates, so a smaller canvas clips them, and the white is filled
74
+ over the whole canvas. Images are decoded from the bytes on the list —
72
75
  no `img-src` grant, no URLs. `hit` answers which schema node was drawn at a point: the smallest box
73
76
  containing it.
74
77
 
@@ -85,9 +88,16 @@ positioning, so a browser may kern by a fraction more. Where a line breaks, how
85
88
  and where a page ends are the layout's — one measurer, so preview and document agree.
86
89
 
87
90
  `PX_PER_POINT` is how big a point is on screen — 96 dpi over PostScript's 72 — so a surface showing
88
- a page at 100% sizes its canvas `page.width * PX_PER_POINT` CSS pixels wide and paints at
91
+ a page at 100% sizes its canvas `list.width * PX_PER_POINT` CSS pixels wide and paints at
89
92
  `PX_PER_POINT * devicePixelRatio`. Any zoom is a factor on top of that.
90
93
 
94
+ ## Documentation
95
+
96
+ [The quario documentation](https://getquario.com/docs/) is the reference.
97
+ The [report schema](https://getquario.com/docs/reference/report-schema/) is the normative
98
+ specification of what a report may declare, and
99
+ [`@quario/layout`](https://getquario.com/docs/reference/layout/) is this package's own API.
100
+
91
101
  ## License
92
102
 
93
103
  quario is commercial software; evaluation is free and fully featured, with output marked as
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
- page = { width: canvas.width, height: canvas.height, ops: [], boxes: [] };
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/index.d.ts CHANGED
@@ -159,12 +159,13 @@ export interface Box {
159
159
  * One page of the list. Coordinates are points from the page's top-left
160
160
  * corner, `y` descending; `number` and `total` are what `page.number` and
161
161
  * `page.total` read on it.
162
+ *
163
+ * A page carries what is per-page and nothing else: the `Paper` it is drawn on
164
+ * is the list's (ADR 0069).
162
165
  */
163
166
  export interface Page {
164
167
  number: number;
165
168
  total: number;
166
- width: number;
167
- height: number;
168
169
  ops: Op[];
169
170
  boxes: Box[];
170
171
  }
@@ -178,11 +179,23 @@ export interface Mark {
178
179
  y: number;
179
180
  }
180
181
 
181
- /** The display list: every page of the document, laid out. */
182
- export interface Layout {
182
+ /**
183
+ * The [paper](../../../CONTEXT.md#paper) a report is drawn on, in points: one
184
+ * width and one height for a whole render. The page box is fixed once the
185
+ * render opens, so this is the list's and a page carries none of its own (ADR
186
+ * 0069) — a consumer sizing a page reads it here.
187
+ */
188
+ export interface Paper {
183
189
  width: number;
184
190
  height: number;
185
- pages: Page[];
191
+ }
192
+
193
+ /** The display list: every page of the document, laid out, on one `Paper`. */
194
+ export interface Layout extends Paper {
195
+ /** Every page, and never none: the band flow opens the body's first page
196
+ * whether or not anything renders on it. Typed as a non-empty tuple so a
197
+ * consumer reading `pages[0]` needs no check for a list that cannot exist. */
198
+ pages: [Page, ...Page[]];
186
199
  /** The group tree, in document order — the PDF target's bookmarks. */
187
200
  marks: Mark[];
188
201
  }
@@ -206,11 +219,17 @@ export const PX_PER_POINT: number;
206
219
  * order. `scale` is device pixels per point. `fonts` is the same record
207
220
  * given to `layout()`, so a TrueType family draws in its own face.
208
221
  *
222
+ * **Size the canvas to the page first** — `Layout.width` and `Layout.height`
223
+ * times `scale`, the paper every page of the list is drawn on. The ops are in
224
+ * the page's own coordinates, so a canvas short of that clips them, and the
225
+ * white is filled over the whole canvas rather than over a rectangle a page
226
+ * carries (ADR 0069).
227
+ *
209
228
  * 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 is
211
- * filled at that call's own `scale`, not the size it now has. A caller that
212
- * repaints one canvas at changing scales owns that, by not letting a
213
- * superseded call reach a canvas still on screen.
229
+ * happened in between: a canvas re-sized under a call still in flight has its
230
+ * ops placed at that call's own `scale` on a canvas of the size it now has. A
231
+ * caller that repaints one canvas at changing scales owns that, by not letting
232
+ * a superseded call reach a canvas still on screen.
214
233
  */
215
234
  export function paint(
216
235
  ctx: CanvasRenderingContext2D,
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, imageError, isReportBand, text } from "quario";
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
- // Located here rather than in `image.js`, which is handed bytes and a
128
- // format and has never heard of a schema: this is the innermost place that
129
- // knows both what went wrong and which item asked for it. SCHEMA.md calls
130
- // this failure a render error, and a render error names its node.
131
- let sized;
132
- try {
133
- sized = intrinsic(event.bytes, event.format);
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. A picture's background fills the
199
- // image's own box rather than the width of the flow -- there is no line for
200
- // it to sit behind. Text is placed line by line as each is drawn; a picture
201
- // is one box, so where it sits in the width it was given is worked out once.
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
- let outer = w + inset.l + inset.r;
207
- let at = x + shift(block.align, avail - outer);
208
- paintBox(canvas, at, canvas.y, outer, block.h, block.style, block.bg);
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
- at + inset.l,
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],
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. `fonts` is the host's font mapping, the same
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 is
243
- * filled at that call's own `scale`, not the size it now has. A caller that
244
- * repaints one canvas at changing scales owns that, by not letting a
245
- * superseded call reach a canvas still on screen — which is what the viewer's
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 the layout
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
- ctx.setTransform(scale, 0, 0, scale, 0, 0);
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, page.width, page.height);
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.4.0",
3
+ "version": "0.5.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.7.0",
46
+ "quario": "^0.8.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.7.0"
52
+ "quario": "^0.8.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
- };