Images¶
<img> bakes a PNG into the blob as a texture. ps2ui-layout reads the file's header for its size and records the resolved path. ps2ui-bake decodes the pixels and packs them into ui.uib. The console opens no files.
What it is¶
One <img> becomes one textured quad. The source is a PNG, and nothing else is read at build time.
The baker writes the quad's texture in one of two GS formats. PSMCT32 spends 32 bits per texel and keeps every colour the file had. PSMT8 spends 8 bits per texel and reads its colours from a 256-entry CLUT. The palettize attribute asks for PSMT8. Both formats are described in .uib.
The channel6 games screen draws six covers, every one of them palettized:

The markup is games.html. The art comes from make_assets.py, which draws flat shapes with no antialiasing so each cover stays inside 256 colours.
Minimal example¶
Keep the art in a folder beside the document that names it.
Compile it:
$ ps2ui-layout ui.html ui.css --fonts fonts/fonts.json -o ui.json
ps2ui-layout: 2 paint commands, 0 focusables -> ui.json
The file is 160x96 and the sheet states one axis. The other follows from the aspect ratio:
$ python3 -c "import json;c=[c for c in json.load(open('ui.json'))['commands'] if c['op']=='image'][0];print(c['w'], c['h'])"
96 58
Reference table¶
| attribute or flag | effect |
|---|---|
src="<path>" |
The PNG to bake. A relative path resolves against the directory of the HTML file, not the working directory. An absolute path is used as written. |
palettize |
Bakes this image as PSMT8 with its own 256-entry CLUT. Refused on a streamed slot. |
data-tex-slot="<name>" |
Reserves a slot the app fills at runtime. Excludes both src and palettize. Listed in HTML. |
width, height in CSS |
Set the laid-out size. CSS has the accepted units. |
--palettize-images |
A ps2ui-bake flag. Bakes every image in the run as PSMT8. |
Behaviour¶
Sizing¶
An <img> is a replaced element. What the sheet states decides which of three cases applies.
| CSS on the element | laid-out size |
|---|---|
neither width nor height |
the file's own pixels, plus padding and border |
| one of the two | the stated axis, and the other from the intrinsic aspect ratio, rounded half up |
| both | exactly what the sheet states |
Flex never stretches an image. A replaced element under align-items: stretch keeps its aspect ratio instead of distorting to fill the line. Give it both axes to fill a row.
Pre-scaling and sharing¶
The baker resizes every image to its laid-out size before it packs texels. The texture is the size of the quad, and the GS does no scaling at draw time. Author art at the size it is drawn, and the resample never runs.
The texture cache is keyed by path, width, height and palettize state. Two elements naming one file at one size share a texture. The same file at two sizes is two textures:
$ ps2ui-bake ui.json -o ui.uib --fonts fonts/fonts.json
...
tex[ 0] PSMCT32 64x48 baked 12288 B payload -> 16384 B in pages
tex[ 1] PSMCT32 32x24 baked 3072 B payload -> 8192 B in pages
...
ps2ui-bake: 1 screen(s), 3 records, 2 textures (15 KiB baked), 0 CLUTs -> ui.uib
Three <img> elements on one file, two of them at 64x48 and one at 32x24, produced two textures.
Palettize¶
palettize on one element and --palettize-images over the whole run both select PSMT8. They disagree about an already-indexed PNG.
| source PNG | with palettize on the element |
with --palettize-images alone |
|---|---|---|
| any non-indexed mode | quantized to at most 256 colours, then baked PSMT8 | the same |
| indexed, file size equals laid-out size | baked verbatim, keeping its own indices and its own palette | baked verbatim |
| indexed, file size differs from laid-out size | build error, and no blob is written | warning, then requantized |
The attribute is a claim about one asset, so resizing it silently would destroy the thing that was asked for. The flag is a request for VRAM across the run, and its author made no claim about any one file.
A palette shorter than 256 entries is padded with zeros. The runtime permutes all 256 entries when it uploads a CLUT, so a short one would walk off the end of the table. Every palettized image carries a CLUT of its own; palettized images do not share one.
Thirteen cases cover this in TestImages:
$ cd packages/baker && python3 -m unittest discover -s tests -k TestImages
...
Ran 13 tests in 0.079s
OK
What it costs¶
PSMT8 costs a quarter of PSMCT32 per texel. The two bakes below run over one IR. It is the channel6 games screen compiled with the palettize attribute removed, so the flag is the only difference:
$ ps2ui-bake games-plain.json -o off.uib --fonts fonts/fonts.json
...
tex[ 6] PSMCT32 96x58 baked 22272 B payload -> 32768 B in pages
...
textures 352256 B of 753664 B budget (46%)
$ ps2ui-bake games-plain.json -o on.uib --fonts fonts/fonts.json --palettize-images
...
tex[ 6] PSMT8 96x58 baked 5568 B payload -> 8192 B in pages
...
textures 253952 B of 753664 B budget (33%)
One 96x58 cover fell from 22272 B of texels to 5568 B. Each palettized image then adds 1024 B of CLUT payload. The in pages column is the budget model, which charges whole 8 KiB pages and is deliberately pessimistic. VRAM budget has that model and the rest of the breakdown.
Streamed slots¶
Art the app supplies at runtime is a streamed slot rather than a baked image. Write <img data-tex-slot="cover"> with an explicit width and height in CSS, and no src. The blob carries the geometry, the name and a PSMCT32 reservation, and no texels. Streaming art has the runtime call and the host-side conversion.
Limits and errors¶
Any format other than PNG is refused at compile time, with the resolved path in the message:
$ ps2ui-layout jpg.html ui.css --fonts fonts/fonts.json -o jpg.json
error: image: "/tmp/claude-0/-home-user-OPHTML/6b0c72b8-d98f-5f58-b749-f9808bb620d6/scratchpad/authoring/images/mini/assets/cover.jpg" is not a PNG — only PNG is supported at build time; convert other formats before compiling
The first seven messages below are compile errors from ps2ui-layout. The last three come from ps2ui-bake.
| message | cause |
|---|---|
image: cannot read "<path>": ENOENT |
The src names a file that is not there. |
image: "<path>" is not a PNG — only PNG is supported at build time; convert other formats before compiling |
Any other format, JPEG included. |
image: "<path>": malformed PNG (IHDR not first chunk) |
The chunk after the 8-byte signature is not IHDR. |
image: "<path>": zero-sized PNG |
The IHDR width or height is 0. |
layout: <img> on line <n> has no src attribute (or data-tex-slot, ...) |
An <img> carrying neither. |
layout: <img data-tex-slot="<name>"> needs an explicit width and height ... |
A streamed slot with either axis unset. A slot has no file to measure. |
layout: <img> on line <n>: palettize is not supported on a streamed slot ... |
palettize together with data-tex-slot. |
image: <path> is an indexed PNG at <w>x<h> but is laid out at <w>x<h>. ... |
palettize on an indexed PNG at any other size. |
warning: <path> is an indexed PNG at <w>x<h> laid out at <w>x<h>; --palettize-images requantized it, ... |
Warning. The same mismatch under the flag alone. |
image: cannot decode <path>: <reason> |
Pillow refused a file that passed the header check at compile time. |
Two silences are worth writing down. An image whose laid-out width or height computes to 0 emits no texture and no draw record, and nothing says so. The compile stage reads the IHDR only. A PNG with a valid header and a corrupt body passes ps2ui-layout and fails in ps2ui-bake.
Related pages¶
| page | why |
|---|---|
| CSS | the units width and height accept, and the flex rules around them |
| VRAM budget | what a texture costs and what the bake refuses |
| .uib | how a texture and its CLUT sit in the blob |
| ps2ui-bake | --palettize-images and the rest of the transcript |
| Streaming art | filling a data-tex-slot from the app |