Permalink
Switch branches/tags
Nothing to show
Find file Copy path
191 lines (140 sloc) 14.1 KB

Color managing canvas contents

Use Case Description

  • Contents displayed through a canvas element should be color managed in order to minimize differences in appearance across browsers and display devices. Improving color fidelity matters a lot for artistic uses (e.g. photo and paint apps) and for e-commerce (product presentation).
  • Canvases should be able to take advantage of the full color gamut and dynamic range of the display device.
  • Some applications prefer that compositing, filtering and interpolation calculations be performed in a physically based linear color space.

Current Limitations

  • The color space of canvases is undefined in the current specification.
  • The bit-depth of canvases is currently fixed to 8 bits per component, which is below the capabilities of some monitors. Monitors with higher contrast ratios require more bits per component to avoid banding.

Current Usage and Workarounds

The lack of color space interoperability is hard to work around. With some browser implementations that color correct images drawn to canvases by applying the display profile, apps that want to use canvases for color corrected image processing are stuck doing convoluted workarounds, such as:

  • reverse-engineer the display profile by drawing test pattern images to the canvas and inspecting the color corrected result via getImageData
  • bypass CanvasRenderingContext2D.drawImage() and use image decoders implemented in JavaScript to extract raw image data that was not tainted by the browser's color correction behavior.''

An aspect of current implementations that is interoperable is that colors match between CSS/HTML and canvases:

  • A color value used as a canvas drawing style will have the same appearance as if the same color value were used as a CSS style
  • An image resource drawn to a canvas element will have the same appearance as if it were displayed as the replaced content of an HTML element or used as a CSS style value.

This color matching behavior needs to be preserved to avoid breaking pre-existing content.

Some implementations convert images drawn to canvases to the sRGB color space. This has the advantage of making the color correction behavior device independent, but it clamps the gamuts of the rendered content to the sRGB gamut, which is significantly narrower than the gamuts of some current consumer devices.

Requests for this Feature

  • [https://github.com/whatwg/html/issues/299]

    Allow 2dcontexts to use deeper color buffers

  • [https://bugs.chromium.org/p/chromium/issues/detail?id=425935]

    Wrong color profile with 2D canvas

  • Engineers from the Google Photos, Maps and Sheets teams have expressed a desire for canvases to become color managed. Particularly for the use case of resizing an imaging, using a canvas, prior to uploading it to the server, to save bandwidth. The problem is that the images retrieved from a canvas are in an undefined color space and no color space information is encoded by toDataURL or toBlob.

Proposed Solution

  • Clearly define the color space of canvases. Most current browser implementations are either color-managed or are currently actively working on becoming color-managed. Therefore, it will be possible to specify that the default color space of canvases as 'srgb' instead of leaving it undefined in the spec (as is currently the case due to lack of interoperability).
  • Add a canvas context creation attribute to specify a color space.
  • Add a canvas context creation attribute to specify a numeric format for storing pixel values.
  • Color space and numeric format parameters are also extended to image storage interfaces that interact with canvases, such as CanvasPattern, ImageData and ImageBitmap.

Processing Model

The colorSpace canvas creation parameter

IDL:

enum CanvasColorSpace {
  "srgb", // default
  "linear-srgb",
};

enum CanvasPixelFormat {
  "8-8-8-8", // default
  "float16",
};

partial dictionary CanvasRenderingContext2DSettings {
  CanvasColorSpace colorSpace = "srgb";
  CanvasPixelFormat pixelFormat = "8-8-8-8";
};

partial interface CanvasRenderingContext2D {
  CanvasRenderingContext2DSettings getContextAttributes();
};

Example:

canvas.getContext('2d', { colorSpace: "linear-srgb", pixelFormat: "float16"});

Common properties of all color spaces

  • All input colors (e.g, fillStyle or strokeStyle, and gradient stops) follow the same interpretation as CSS color literals, regardless of canvas color space.
  • Images with no color profile, when drawn to the canvas, are assumed to already be in sRGB color space.
  • Unless otherwise explicitly specified by the user, toDataURL/toBlob will produce resources in sRGB color space. If the encoding format supports colorspace tagging or embedded color profiles, the resource will be tagged as being in sRGB color space.
  • Values written to WebGL drawing buffers (e.g, values written to gl_FragColor or the clear color) are defined to be in the output canvas color space.

The "srgb" color space

  • This color space matches the existing canvas behavior.
  • Guarantees that color values used as fillStyle or strokeStyle exactly match the appearance of the same color value when it is used in CSS.
  • On implementations that do not color-manage CSS colors, the canvas "srgb" color space must not be color-managed either, in order to preserve color-matching between CSS and canvas-rendered content. This situation shall be referred to as the "legacy behavior".
  • All content drawn into the canvas must be color corrected to sRGB. Exception: User agents that implement the legacy behavior must apply color correction steps that match the color correction that is applied to image resources that are displayed via CSS.
  • Displayed canvases must be color corrected for the display if a display color profile is available. This color correction happens downstream at the compositing stage, and has no script-visible side-effects.
  • toDataURL/toBlob produce resources tagged as being in the sRGB color space, if the encoding format supports colorspace tagging or embedded color profiles. Exception: User agents that implement the legacy behavior must not encode any color space metadata.
  • Images with no color profile, when drawn to the canvas, are assumed to already be in the canvas's color space, and require no color transformation.

The "rec2020" color space

  • Support is optional.
  • Requires that the pixelFormat attribute be "float16".
  • This color space exists to enable wide color gamut and high dynamic range applications that use physically correct linear color operations (e.g, blending and interpolation)
  • Color space definition
    • This color space uses the same primaries as the sRGB color space.
    • This color space has a linear transfer function that supports arbitrary pixel values, in particular, values outside of the range [0, 1].
  • Note that gradients will be physically uniform, and thus will not match gradients in an "srgb" canvas
  • Note that pixel values written to WebGL draw buffers are now in a space with a linear transfer function, and will not necessarily match those of an "srgb" canvas.

The pixelFormat context creation attribute

The pixelFormat attributes specifies the numeric types to be used for storing pixel values.

  • Support for "8-8-8-8" is mandatory. All other formats ar optional.
  • When an unsupported format is requested, the format shall fall back to "8-8-8-8".
  • With formats that use integer numeric types for the color channels, the canvas pixel buffer shall store non-linear color values, encoded using the transfer curves prescribed by the specification of the current color space.
  • With formats that use floating-point numeric types for the color channels, the canvas pixel buffer shall store linear (i.e unencoded) color values.
  • Float values outside of [0,1] range can be used to represent colors outside of the chosen color gamut. This allows float pixel formats to represent all possible colors and brightness levels. How values outside of [0,1] are displayed depends on the capabilites of the device and output display. Some implementations may simply clamp these values to [0,1]. If the device and display are capable, a pixel value of (2,2,2) should be twice as bright as (1,1,1).

Selecting the best color space match for the user agent's display device

var colorSpace = window.matchMedia("(color-gamut: rec2020)").matches ? "rec2020" : 
    (window.matchMedia("(color-gamut: p3)").matches ? "p3" : "srgb");

Selecting the best pixelFormat for the user agent's display device

Selection should be based on the best color space match (see above). For srgb, at least 8 bits per component is recommended; for p3, 10 bits; and for rec2020, 12 bits. The float16 format is suitable for any colorspace. There may soon be a proposal to add a way of detecting HDR displays, possibly something like "window.screen.isHDR()" (TBD), which would be a good hint to use the float16 format.

Non-standard color spaces

For future consideration: support could be added for color space defined using the CSS @color-profile rule.

Compositing the canvas element

Canvas contents are composited in accordance with the canvas element's style (e.g. CSS compositing and blending rules). The necessary compositing operations must be performed in an intermediate colorspace, the compositing space, that is implementation specific. The compositing space must have sufficient precision and a sufficiently wide gamut to guarantee no undue loss of precision or gamut clipping in bringing the canvas's contents to the display.

Feature detection

2D rendering contexts are to expose a new getContextAttributes() method, that works much like the method of the same name on WebGLRenderingContext. The method returns the "actual context attributes" which represents the settings that were successfully applied at context creation time. The settings attribute reflects the result of running the algorithm for coercing the settings argument for 2D contexts, as well as the result of any fallbacks that may have happened as a result of options not being supported by the UA.

Web apps may infer that a user agent that does not implement getContextAttributes() does not support the colorSpace and pixelFormat attributes.

Note: An alternative approach that was considered was to augment the probablySupportsContext() API by making it check the second argument. That approach is difficult to consolidate with how dictionary arguments are meant to work, where unsupported entries are just ignored.

ImageBitmap

TODO(ccameron): Review this ImageBitmap objects are augmented to have an internal color space attribute of type CanvasColorSpace and an internal pixelFormat attribute of type CanvasPixelFormat. The colorSpaceConversion creation attribute also accepts enum values that correspond to CanvasColorSpace values. Specifying a CanvasColorSpace value results in a conversion of the image to the specified color space.

ImageData

TODO(ccameron): Review this

IDL

enum ImageDataStorageType {
  "uint8", // default
  "uint16",
  "float32",
};

typedef (Uint8ClampedArray or Uint16Array or Float32Array) ImageDataArray;

dictionary ImageDataColorSettings {
  CanvasColorSpace colorSpace = "srgb";
  ImageDataStorageType storageType = "uint8";
};

[Constructor(unsigned long sw, unsigned long sh, optional ImageDataColorSettings imageDataColorSettings),
 Constructor(ImageDataArray data, unsigned long sw, optional unsigned long sh, optional ImageDataColorSettings imageDataColorSettings),
 Exposed=(Window,Worker)]
interface ImageData {
  readonly attribute unsigned long width;
  readonly attribute unsigned long height;
  readonly attribute ImageDataArray data;
  
  ImageDataColorSettings getColorSettings();
};
  • When using the constructor that takes an ImageDataArray parameter, the "storageType" setting is ignored.
  • createImageData() and getImageData() produce an ImageData object with the same color space as the source canvas, using an ImageDataArray of a type that is appropriate for the pixelFormat of the source canvas (smallest possible numeric size that guarantees no loss of precision).
  • If the canvas color space is not "srgb" or the pixel format is not "8-8-8-8", the data returned by getImageData() is in linear color space.
  • putImageData() performs a color space conversion to the color space of the destination canvas.
  • Data returned by getImageData() or passed to putImageData() are assumed to be in linear color space.
  • If the image data color space is not "srgb" or the storage format is not "uint8", the data passed to putImageData() is assumed to be in linear color space.

Limitations

  • toDataURL and toBlob are lossy, depending on the file format, when used on a canvas that has a pixelFormet other than 8-8-8-8. Possible future improvements could solve or mitigate this issue by adding more file formats or adding options to specify the resource color space.

Adoption

Lack of color management and color interoperability is a longstanding complaint about the canvas API. Authors of games and imaging apps are expected to be enthusiastic adopters.

Unresolved Issues

  • Should black level compensation be implied in "linear spaces", such that (0,0,0) always represents absolute black in linear color values?

  • Should we support custom color spaces based on ICC profiles? Would offer ultimate flexibility. Would be hard to make implementations as efficient as built-in color spaces, in particular for implement linearPixelMath for profiles that have arbitrary transfer curves.

  • Should float16 pixelFormat allow alpha values outside [0,1] range at any stage of the pipeline? What would they mean?

Proposal History

This propsal was originally incubated in the WHATWG github issue tracker and incorporates feedback that was provided in the following thread: https://github.com/whatwg/html/issues/299

This proposal was further discussed in the Khronos WebGL working group, with the participation of engineers from Apple, Google, Microsoft, Mozilla, Nvidia, and others.

The current venue for discussing this proposal is a W3C WICG Discourse thread