meo-canvas - v8.1.3
    Preparing search index...

    Interface SaveOptions

    🧪 Not in the HTML Canvas standard.

    interface SaveOptions {
        bitDepth?: 8 | 10 | 12;
        chromaSampling?: "4:4:4" | "4:2:2" | "4:2:0";
        colorSpace?: ColorSpace;
        colorType?: ColorType;
        density?: number;
        downsample?: boolean;
        format?: ExportFormat;
        fps?: number;
        frameDelays?: number[];
        loop?: number;
        lossless?: boolean;
        matte?: string;
        msaa?: number | boolean;
        outline?: boolean;
        page?: number;
        pageRange?: [number, number];
        quality?: number;
    }

    Hierarchy (View Summary)

    Index
    bitDepth?: 8 | 10 | 12

    Bits 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.

    chromaSampling?: "4:4:4" | "4:2:2" | "4:2:0"

    How "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.

    colorSpace?: ColorSpace

    Color 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.

    colorType?: ColorType

    Pixel 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.

    density?: number

    Number of pixels per grid ‘point’ (defaults to 1)

    downsample?: boolean

    Optionally use 4:2:0 chroma subsampling (JPEG only)

    format?: ExportFormat

    Image format to use (either as a file extension or a mime-type string)

    fps?: number

    Frames 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.

    frameDelays?: number[]

    Per-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.

    loop?: number

    How 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.

    lossless?: boolean

    Whether "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.

    matte?: string

    Background color to draw beneath transparent parts of the canvas

    msaa?: number | boolean

    Number of samples used for antialiasing each pixel. 0 and 1 both mean one sample a pixel -- no multisampling.

    outline?: boolean

    Optionally convert text to bézier paths (SVG only)

    page?: number

    Which 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.

    pageRange?: [number, number]

    Which 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.

    quality?: number

    Quality for lossy encodings like JPEG & WEBP (0.0–1.0)