OptionalbitBits a channel "avif" codes its pixels at, defaulting to whatever the
canvas has to give.
AV1 codes 8, 10 and 12 and AVIF carries all three. Unasked, an eight-bit canvas is written at 10 -- AV1's transforms work above the input depth anyway, and the headroom keeps quantisation from banding a gradient that eight bits would step through -- and a float canvas at 12.
The reason to name one is reach. 8 and 10 at 4:4:4 are AV1's High profile; 12 is Professional, which fewer decoders implement. So a float canvas whose file has to open anywhere asks for 10, while 8 is both the smallest file and the one depth that reaches the encoder as the bytes the canvas already holds.
Naming one for any other format is a TypeError: their depths are the
ones colorType already names. JPEG, WebP, GIF, ICO and BMP have
no deeper form at all -- eight bits a channel is what those formats are.
OptionalchromaHow "avif" samples chroma, defaulting to "4:4:4".
Full chroma is the opposite of what most AVIF encoders default to, and
it is deliberate: this library draws canvases. Measured on flat UI with
text, "4:2:0" came out 22 dB worse -- 50.07 against 27.96 -- and
produced a larger file, because the artefacts it introduces cost bits of
their own. Saturated colour against a light ground is exactly what
halving chroma in both axes destroys.
On photographs the usual trade holds and is worth taking: the same
measurement put "4:2:0" 30% smaller for 7 dB. So a canvas exporting a
photograph should ask for it, and one exporting a chart should not.
"4:2:2" is the middle and rarely the best of the three -- on UI it was
indistinguishable from "4:4:4" while saving nothing, and on
photographs "4:2:0" was both smaller and no worse.
Naming one for any other format is a TypeError. JPEG has a subsampling
switch of its own in downsample, which is a boolean because
JPEG offers the one alternative.
OptionalcolorColor space the exported image is converted to, defaulting to the canvas's own.
A display-p3 canvas therefore writes a Display P3 file with the
matching ICC profile embedded, without being asked. Naming a space here
converts; it cannot recover. A colour outside the canvas's gamut was
already clipped when it was drawn, so exporting an srgb canvas as
"display-p3" writes a P3 file holding sRGB colours.
OptionalcolorPixel format the export is handed back in, defaulting to the canvas's own.
This is what makes toBuffer("raw", {colorType: "RGBAF32"}) differ from
the canvas it came from, and it is also where the encoded formats read
their own depth: a float type writes a sixteen-bit "png", "apng" or
"tiff", and "RGBA8888" on a float canvas writes eight. "avif" is
the exception and has ExportOptions.bitDepth of its own.
Compositing still follows the canvas: a readback format has no business choosing the precision a page is drawn at.
OptionaldensityNumber of pixels per grid ‘point’ (defaults to 1)
OptionaldownsampleOptionally use 4:2:0 chroma subsampling (JPEG only)
OptionalfpsFrames per second for the animated formats -- "webp", "gif",
"apng" and "avif". One page is one frame, so this is the rate the
pages play at.
Defaults to 30. GIF stores hundredths of a second, so its frame times round to the nearest 10ms.
Naming one for any other format is a TypeError, not something quietly
dropped -- and that includes "tiff", "ico" and "pdf", which gather
every page without any of them having a duration.
OptionalframePer-frame durations in milliseconds, overriding fps.
Must have one entry per page, which is what makes re-encoding an
animation possible: the delays an Image reports can be handed
straight back. A list of any other length is a RangeError rather than
a silent retiming, and a list given to a format that cannot animate is
a TypeError.
OptionalloopHow many times an animation plays. 0 -- the default -- repeats it
forever, which is how both GIF and APNG spell it. Naming one for a
format that cannot animate is a TypeError.
1 plays it once, which APNG states outright and GIF cannot: GIF's loop
count lives in a block whose zero already means "forever", so no number
means "once" and the convention is to omit the block. A GIF asking for a
single play therefore declares nothing, and a decoder may report either
answer depending on when it is asked. Every other count is stated
plainly by both.
OptionallosslessWhether "avif" is coded with no loss at all, defaulting to false.
Off by default and deliberately: AVIF is reached for because it is small, and a lossless one is several times the size of a lossy one and often larger than the PNG it would replace. Every encoder in the ecosystem defaults to lossy for the same reason.
This is lossless in red, green and blue, not merely in what the encoder was handed. That needs two things beyond the flag, both of which this sets: full chroma, and the identity matrix, where the three coded planes are green, blue and red rather than a luma and two colour differences. Without the second, the picture is rounded by the conversion before quantisation runs and the file faithfully preserves data that was already lossy.
So naming a chromaSampling other than "4:4:4" alongside this is
a TypeError rather than being silently overridden -- subsampled
identity planes would discard literal red and blue samples.
quality is ignored when this is set, and is not promoted to
lossless at 1.0: that means the finest quantizer, which is
near-lossless but still filtered, and changing what it meant would change
every file this library has already written.
Naming it for any other format is a TypeError.
OptionalmatteBackground color to draw beneath transparent parts of the canvas
OptionalmsaaNumber of samples used for antialiasing each pixel. 0 and 1 both
mean one sample a pixel -- no multisampling.
OptionaloutlineOptionally convert text to bézier paths (SVG only)
OptionalpageWhich page to export, numbered from 1.
Left out, a single-page format encodes the current page -- the most
recently added one, which is what canvas.getContext() hands back --
and a format that gathers pages takes all of them. A negative number
counts from the end, so -1 is that same current page named explicitly.
A number past either end is a RangeError that names the page asked
for, not the index it resolved to.
OptionalpageWhich pages to gather, numbered from 1 and inclusive at both ends.
Left out, a format that gathers pages takes all of them. Negative
numbers count from the end, as RenderOptions.page does, so
[2, -1] is everything after the first page.
This is what separates an intro from the loop that follows it: two calls
over one canvas, each with its own loop, rather than one file that has
to compromise between them.
const intro = await canvas.toBuffer('webp', { fps: 30, pageRange: [1, 20], loop: 1 })
const cycle = await canvas.toBuffer('webp', { fps: 30, pageRange: [21, 60], loop: 0 })
It serves the paged documents as well -- { pageRange: [12, 18] } pulls
one chapter out of a long PDF, and a filename template such as
frame-{}.png writes only the frames named.
Naming both this and page is a TypeError: they answer the same
question differently. A bound past either end is a RangeError naming
the page asked for, an end before its start is a RangeError, and a
range given to a format that encodes a single page -- with no filename
template to write a sequence -- is a TypeError.
OptionalqualityQuality for lossy encodings like JPEG & WEBP (0.0–1.0)
🧪 Not in the HTML Canvas standard.