Images
How are images represented?
At the lowest level, images are represented as a
Uint8List(i.e., an opaque list of unsigned bytes). These bytes can be expressed in any number of image formats, and must be decoded to a common representation by a codec.instantiateImageCodecaccepts a list of bytes and returns the appropriate codec from the engine already bound to the provided image. This function accepts an optional width and height; if these do not match the image’s intrinsic size, the image is scaled accordingly. If only one dimension is provided, the other dimension remains the intrinsic dimension.PaintingBinding.instantiateImageCodecprovides a thin wrapper around this function with the intention of eventually supporting additional processing.Codec represents the application of a codec on a pre-specified image array. Codecs process both single frames and animated images. Once the
Codecis retrieved viainstantiateImageCodec, the decodedFrameInfo(which contains the image) may be requested viaCodec.nextFrame; this may be invoked repeatedly for animations, and will automatically wrap to the first frame. TheCodecmust be disposed when no longer needed (the image data remains valid).DecoderCallbackprovides a layer of indirection between image decoding (via theCodecreturned byinstantiateImageCodec) and any additional decoding necessary for an image (e.g., resizing). It is primarily used withImageProviderto encapsulate decoding-specific implementation details.FrameInfocorresponds to a single frame in an animated image (single images are considered one-frame animations). Duration, if application, is exposed viaFrameInfo.duration. Otherwise, the decodedImagemay be read asFrameInfo.image.Image is an opaque handle to decoded image pixels managed by the engine, with a width and a height. The decoded bytes can be obtained via
Image.toByteDatawhich accepts anImageByteFormatspecifying (e.g.,ImageByteFormat.rawRgba,ImageByteFormat.png). However, the raw bytes are often not required as theImagehandle is sufficient to paint images to the screen.ImageInfoassociates anImagewith a pixel density (i.e.,ImageInfo.scale). Scale describes the number of image pixels per one side of a logical pixel (e.g., a scale of2.0implies that each 1x1 logical pixel corresponds to 2x2 image pixels; that is, a 100x100 pixel image would be painted into a 50x50 logical pixel region and therefore have twice the resolution depending on the display).
What are the building blocks for managing image data?
The image framework must account for a variety of cases that complicate image handling. Some images are obtained asynchronously; others are arranged into image sets so than an optimal variant can be selected at runtime (e.g., for the current resolution). Others correspond to animations which update at regular intervals. Any of these images may be cached to avoid unnecessary loading.
ImageStreamprovides a consistent handle to a potentially evolving image resource; changes may be due to loading, animation, or explicit mutation. Changes are driven by a singleImageStreamCompleter, which notifies theImageStreamwhenever concrete image data is available or changes (viaImageInfo). TheImageStreamforwards notifications to one or more listeners (i.e.,ImageStreamListenerinstances), which may be invoked multiple times as the image loads or mutates. EachImageStreamis associated with a key that can be used to determine whether twoImageStreaminstances are backed by the same completer [?].ImageStreamListenerencapsulates a set of callbacks for responding to image events. If the image is being loaded (e.g., via the network), anImageChunkListeneris invoked with anImageChunkEventdescribing overall progress. If an image has become available, anImageListeneris invoked with the finalImageInfo(including a flag indicating whether the image was loaded synchronously). Last, if the image has failed to load, anImageErrorListeneris invoked.The chunk listener is only called when an image must be loaded (e.g., via
NetworkImage). It may also be called after theImageListenerif the image is an animation (i.e., another frame is being fetched).The
ImageListenermay be invoked multiple times if the associated image is an animation (i.e., once per frame).ImageStreamListenersare compared on the basis of the contained callbacks.
ImageStreamCompletermanages image loading for anImageStreamfrom an asynchronous source (typically aCodec). A list ofImageStreamListenerinstances are notified whenever image data becomes available (i.e., the completer “completes”), either in part (viaImageStreamListener.onImageChunk) or in whole (viaImageStreamListener.onImage). Listeners may be invoked multiple times (e.g., as chunks are loaded or with multiple animation frames). The completer notifies listeners when an image becomes available (viaImageStreamCompleter.setImage). Adding listeners after the image has been loaded will trigger synchronous notifications; this is how theImageCacheavoids refetching images unnecessarily.The corresponding
Imagemust be resolved to anImageInfo(i.e., by incorporating scale); the scale is often provided explicitly.OneFrameImageStreamCompleterhandles one-frame (i.e., single) images. The correspondingImageInfois provided as a future; when this future resolves,OneFrameImageStreamCompleter.setImageis invoked, notifying listeners.MultiFrameImageStreamCompleterhandles multi-frame images (e.g., animations or engine frames), completing once per animation frame as long as there are listeners. If the image is only associated with a single frame, that frame is emitted immediately. An optional stream ofImageChunkEventsallows loading status to be conveyed to the attached listeners. Note that adding a new listener will attempt to decode the next frame; this is safe, if inefficient, asCodec.getNextFrameautomatically cycles.The next frame is eagerly decoded by the codec (via
Codec.getNextFrame). Once available, a non-repeating callback is scheduled to emit the frame after the corresponding duration has lapsed (viaFrameInfo.duration); the first frame is emitted immediately. If there are additional frames (viaCodec.frameCount), or the animation cycles (viaCodec.repetitionCount), this process is repeated. Frames are emitted viaMultiFrameImageStreamCompleter.setImage, notifying all subscribed listeners.In this way, the next frame is decoded eagerly but only emitted during the first application frame after the duration has lapsed. If at any point there are no listeners, the process is paused; no frames are decoded or emitted until a listener is added.
A singleton
ImageCacheis created by thePaintingBindingduring initialization (viaPaintingBinding.createImageCache). The cache maps keys toImageStreamCompleters, retaining only the most recently used entries. Once a maximum number of entries or bytes is reached, the least recently accessed entries are evicted. Note that any images actively retained by the application (e.g.,Image,ImageInfo,ImageStream, etc.) cannot be invalidated by this cache; the cache is only useful when locating anImageStreamCompleterfor a given key. If a completer is found, and the image has already been loaded, the listener is notified with the image synchronously.ImageCache.putIfAbsentserves as the main interface to the cache. If a key is found, the correspondingImageStreamCompleteris returned. Otherwise, the completer is built using the provided closure. In both cases, the timestamp is updated.Because images are loaded asynchronously, the cache policy can only be enforced once the image loads. Thus, the cache maintains two maps:
ImageCache._pendingImagesandImageCache._cache. On a cache miss, the newly built completer is added to the pending map and assigned anImageStreamListener; when the listener is notified, the final image size is calculated, the listener removed, and the cache policy applied. The completer is then moved to the cache map.If an image fails to load, it does not contribute to cache size but it does consume an entry. If an image is too large for the cache, the cache is expanded to accommodate the image with some headroom.
ImageConfigurationdescribes the operating environment so that the best image can be selected from a set of alternatives (i.e., a double resolution image for a retina display); this is the primary input toImageProvider. A configuration can be extracted from the element tree viacreateLocalImageConfiguration.ImageProvideridentifies an image without committing to a specific asset. This allows the best variant to be selected according to the currentImageConfiguration. Any images managed viaImageProviderare passed through the globalImageCache.ImageProvider.obtainKeyproduces a key that uniquely identifies a specific image (including scale) given anImageConfigurationand the provider’s settings.ImageProvider.loadbuilds anImageStreamCompleterfor a given key. The completer begins fetching the image immediately and decodes the resulting bytes via theDecoderCallback.ImageProvider.resolvewraps both methods to (1) obtain a key (viaImageProvider.obtainKey), (2) query the cache using the key, and (3) if no completer is found, create anImageStreamCompleter(viaImageProvider.load) and update the cache.
precacheImageprovides a convenient wrapper aroundImageProviderso that a given image can be added to theImageCache. So long as the same key is used for subsequent accesses, the image will be available immediately (provided that it has fully loaded).
How are images provided and painted?
ImageProviderfederates access to images, selecting the best image given the current environment (i.e.,ImageConfiguration). The provider computes a key that uniquely identifies the asset to be loaded; this creates or retrieves anImageStreamCompleterfrom the cache. Various provider subclasses overrideImageProvider.loadto customize how the completer is configured; most useSynchronousFutureto try to provide the image without needing to wait for the next frame. TheImageStreamCompleteris constructed with a future resolving to a bound codec (i.e., associated with raw image bytes). These bytes may be obtained in a variety of ways: from the network, from memory, from anAssetBundle, etc. The completer accepts an optional stream ofImageChunkEventsso that any listeners are notified as the image loads. Once the raw image has been read into memory, an appropriate codec is provided by the engine (via aDecoderCallback, which generally delegates toPaintingBinding.instantiateImageCodec). This codec is used to decode frames (potentially multiple times for animated images). As frames are decoded, listeners (e.g., an image widget) are notified with the finalizedImageInfo(which includes decoded bytes and scale data). These bytes may be painted directly viapaintImage.
What image providers are available?
FileImageprovides images from the file system. As its own key,FileImageoverrides the equality operator to compare the target file name and scale. AMultiFrameImageStreamCompleteris configured with the provided scale, and aCodecinstantiated using bytes loaded from the file (viaFile.readAsBytes). The completer will only notify listeners when the image is fully loaded.MemoryImageprovides images directly from an immutable array of bytes. As its own key,MemoryImageoverrides the equality operator to compare scale as well as the actual bytes. AMultiFrameImageStreamCompleteris configured with the provided scale, and aCodecinstantiated using the provided bytes. The completer will only notify listeners when the image is fully loaded.NetworkImagedefines a thin interface to support different means of providing images from the network; it relies on instances of itself for a key.io.NetworkImageimplements this interface usingDart’s standardHttpClientto retrieve images. As its own key,io.NetworkImageoverrides the equality operator to compare the targetURLand scale. AMultiFrameImageStreamCompleteris configured with the provided scale, and aCodecinstantiated using the consolidated bytes produced byHttpClient.getUrl. Unlike the other providers,io.NetworkImagewill report loading status to its listeners via a stream ofImageChunkEvents. This relies on the “Content-Length” header being correctly reported by the remote server.
AssetBundleImageProviderprovides images from anAssetBundleusingAssetBundleImageKey. The key is comprised of a specific asset bundle, asset key, and image scale. AMultiFrameImageStreamCompleteris configured with the provided scale, and aCodecinstantiated using bytes loaded from the bundle (viaAssetBundle.load). The completer will only notify listeners when the image is fully loaded.ExactAssetImageis a subclass that allows the bundle, asset, and image scale to be set explicitly, rather than read from anImageConfiguration.AssetImageis a subclass that resolves to the most appropriate asset given a set of alternatives and the current runtime environment. Primarily, this subclass selects assets optimized for the device’s pixel ratio using a simple naming convention. Assets are organized into logical directories within a given parent. Directories are named “Nx/”, where N is corresponds to the image’s intended scale; the default asset (with 1:1 scaling) is rooted within the parent itself. The variant that most closely matches the current pixel ratio is selected.The main difference from the superclass is method by which keys are produced; all other functionality (e.g.,
AssetImage.load,AssetImage.resolve) is inherited.A
JSON-encoded asset manifest is produced from the pubspec file during building. This manifest is parsed to locate variants of each asset according to the scheme described above; from this list, the variant nearest the current pixel ratio is identified. A key is produced using this asset’s scale (which may not match the device’s pixel ratio), its fully qualified name, and the bundle that was used. The completer is configured by the superclass.The equality operator is overridden such that only the unresolved asset name and bundle are consulted; scale (and the best fitting asset name) are excluded from the comparison.
ResizeImagewraps anotherImageProviderto support size-aware caching. Ordinarily, images are decoded using their intrinsic dimensions (viainstantiateImageCodec); consequently, the version of the image stored in theImageCachecorresponds to the full size image. This is inefficient for images that are displayed at a different size.ResizeImageaddresses this by augmenting the underlying key with the requested dimensions; it also applies aDecoderCallbackthat forwards these dimensions viainstantiateImageCodec.The first time an image is provided, it is loaded using the underlying provider (via
ImageProvider.load, which doesn’t update the cache). The resultingImageStreamCompleteris cached using theResizeImage’s key (i.e.,_SizeAwareCacheKey).Subsequent accesses will hit the cache, which returns an image with the corresponding dimensions. Usages with different dimensions will result in additional entries being added to the cache.
What are the building blocks for image rendering?
There are several auxiliary classes allowing image rendering to be customized.
BlendModespecifies how pixels from source and destination images are combined during compositing (e.g.,BlendMode.multiply,BlendMode.overlay,BlendMode.difference).ColorFilterspecifies a function combining two colors into an output color; this function is applied before any blending.ImageFilterprovides a handle to an image filter applied during rendering (e.g., Gaussian blur, scaling transforms).FilterQualityallows the quality/performance of said filter to be broadly customized.Canvas exposes the lowest level API for painting images into layers. The principal methods include
Canvas.drawImage, which paints an image at a particular offset,Canvas.drawImageRect, which copies pixels from a source rectangle to a destination rectangle,Canvas.drawAtlas, which does the same for a variety of rectangles using a “sprite atlas,” andCanvas.drawImageNine, which slices an image into a non-uniform 3x3 grid, scaling the cardinal and center boxes to fill a destination rectangle (the corners are copied directly). Each of these methods accept aPaintinstance to be used when compositing the image (e.g., allowing aBlendModeto be specified); each also calls directly into the engine to perform any actual painting.paintImagewraps the canvas API to provide an imperative API for painting images in a variety of styles. It adds support for applying a box fit (e.g.,BoxFit.coverto ensure the image covers the destination) and repeated painting (e.g.,ImageRepeat.repeatto tile an image to cover the destination), managing layers as necessary.
How are images integrated with the render tree?
Image encapsulates a variety of widgets, providing a high level interface to the image rendering machinery. This widget configures an
ImageProvider(selected based on the named constructor, e.g.,Image.network,Image.asset,Image.memory) which it resolves to obtain anImageStream. Whenever this stream emits anImageInfoinstance, the widget is rebuilt and repainted. Conversely, if the widget is reconfigured, theImageProvideris re-resolved, and the process repeated. From this flow,Imageextracts the necessary data to fully configure aRawImagewidget, which manages the actualRenderImageIf a cache width or cache height are provided, the underlying
ImageProvideris wrapped in aResizeImage(viaImage._resizeIfNeeded). This ensures that the image is decoded and cached using the provided dimensions, potentially limiting the amount of memory used.Image adds support for image chrome (e.g., a loading indicator) and semantic annotations.
If animations are disabled by
TickerMode,Imagepauses rendering of any new animation frames provided by theImageStreamfor consistency.The
ImageConfigurationpassed toImageProvideris retrieved from the widget environment viacreateLocalImageConfiguration.
RawImageis aLeafRenderObjectWidgetwrapping aRenderImageand all necessary configuration data (e.g., theui.Image, scale, dimensions, blend mode).RenderImageis aRenderBoxleaf node that paints a single image; as such, it relies on the widget system to repaint whenever the associatedImageStreamemits a new frame. Painting is performed bypaintImageusing a destination rectangle sized by layout and positioned at the current offset. Alignment, box fit, and repetition determines how the image fills the available space.There are two types of dimensions considered during layout: the image’s intrinsic dimensions (e.g., the number of bytes comprising the image divided by scale) and the requested dimensions (e.g., the value of width and height specified by the caller).
During layout, the incoming constraints are applied to the requested dimensions (via
RenderImage._sizeForConstraints): first, the requested dimensions are clamped to the constraints. Next, the result is adjusted to match the image’s intrinsic aspect ratio while remaining as large as possible. If there is no image associated with the render object, the smallest possible size is selected.The intrinsic dimension methods apply the same logic. However, instead of using the incoming constraints, one dimension is fixed (i.e., corresponding to method’s parameter) whereas the other is left unconstrained.
Last updated
Was this helpful?