Dart Image Library

repository·main·Indexed 23 days ago

https://github.com/brendan-duncan/image

A comprehensive Dart library for loading, saving, and manipulating images across CLI, Flutter, and Web environments. It supports a wide range of formats including BMP, CUR, EXR, GIF, ICO, JPG, PNG, PSD, PVR, TGA, TIFF, and WebP. The library features a Command API for batching operations like resizing, filtering, and drawing, with support for asynchronous execution on isolate threads to prevent UI locking. It also provides specialized tools for handling animated images and pixel-level manipulation.

Tokens
44.8K
Snippets
93
Records
274
Agent score
78%

What's inside image

  1. Overview of the Dart Image Library

    main
    The Dart Image Library is a comprehensive toolset for loading, saving, and manipulating images across various file formats. It is designed for cross-platform compatibility, supporting both dart:io and dart:html. This allows the library to be used in command-line interfaces (CLI), Flutter applications, and web-based environments.
  2. Handle pixels in palette-based images

    main

    In images using a palette, the standard .r, .g, .b, and .a properties return the actual color values from the palette rather than the raw index.

    To work with the palette index directly, use the .index property. Setting .index will update the pixel's position in the palette, while setting .r will also update the index value.

  3. How masking works with drawing functions

    main

    Most drawing functions in the Dart Image Library support an optional mask parameter. A mask is an Image that controls the blending of the drawing operation per pixel.

    • Where the mask channel has full intensity, the drawing has full effect.
    • Where the mask channel is 0, the drawing has no effect.
    • Intermediate values blend the drawing with the original image.

    You can specify which channel of the mask to use via the maskChannel parameter (defaults to Channel.luminance).

  4. Accessing animation properties and frames

    main

    Animated image formats (like GIF, PNG, or WebP) are represented by an Image object containing a frames list. For non-animated images, this list contains a single element. You can inspect the animation properties and iterate through individual frames using the following properties:

    • hasAnimation: Returns true if the image contains more than one frame.
    • loopCount: The repeat count of the animation (0 indicates infinite looping).
    • frameType: Defines how frames are interpreted (e.g., animation, pages, or a sequence of images).
    • frames: A list of Frame objects.

    Each Frame object provides:

    • frameIndex: The zero-based index of the frame in the list.
    • frameDuration: The duration of the frame in milliseconds.
    bool hasAnim = image.hasAnimation; // True if the image has more than 1 frame.
    int loopCount = image.loopCount; // The repeat count for the animated image, 0 means repeat forever.
    FrameType type = image.frameType; // How frames can be interpreted, as animation, pages, or just a sequence of images.
    for (final frame in image.frames) { // Iterate over the frames of the image.
      final frameIndex = frame.frameIndex; // The index of the frame in the frame list.
      final duration = frmae.frameDuration; // The duration of the frame, in milliseconds.
    }
  5. How the Command API and async execution work

    main

    The Command API allows you to batch a sequence of image operations (decoding, filtering, transforming, etc.) and execute them all at once. Instead of performing operations immediately, calling methods on a Command object records sub-commands to be executed later. This is useful for performance optimization and batching multiple operations on a single image.

    To run the recorded commands, you must call either execute() or executeThread().

    • execute(): Runs the commands in the main thread. It is an async method because it may involve file I/O.
    • executeThread(): Runs the commands in a separate Isolate thread (on supported platforms like mobile/desktop, but not web). This prevents heavy image processing from locking up the main UI thread, though there is a small overhead for copying image data back to the main thread.
    final cmd = Command()
      ..decodePngFile('image.png')
      ..sepia(amount: 0.5)
      ..vignette()
      ..writeToFile('processedImage.png');
    
    // Nothing has actually been performed yet.
    
    const useIsolate = true;
    if (useIsolate) {
      await cmd.executeThread(); // Executes in a separate Isolate thread.
    } else {
      await cmd.execute(); // Executes in the main thread.
    }
  6. Understanding High Dynamic Range (HDR) images in the Dart Image Library

    main

    The Dart Image Library supports High Dynamic Range (HDR) images. Unlike standard Low Dynamic Range (LDR) images (such as 8-bit RGBA) which cap color intensity at 1.0 (mapping [0, 255] to [0, 1]), HDR images use floating-point pixel values. This allows them to store color intensities greater than 1.0.

    Note that HDR capabilities are dependent on the file format used. Only specific formats support these values.

  7. Use Decoder classes for advanced image metadata

    main

    When you need more information than a standard Image object provides (like width, height, number of frames, or format-specific metadata), use Decoder classes directly. You can use startDecode to retrieve DecodeInfo without fully decoding the pixel data, which is more efficient for inspecting metadata.

    final decoder = PngDecoder();
    
    // Validate and decode
    bool isValid = decoder.isValidFile(fileBytes);
    Image? image = decoder.decode(fileBytes);
    
    // Efficiently get metadata
    DecodeInfo? info = decoder.startDecode(fileBytes);
    if (info != null) {
      int width = info.width;
      int height = info.height;
      int numFrames = info.numFrames;
      
      // Access format-specific info via casting
      if (info is PngInfo) {
        double? gamma = info.gamma;
        int bits = info.bits;
      }
    }
    
    // Handle animation
    int totalFrames = decoder.numFrames;
    Image? frame0 = decoder.decodeFrame(0);
    final decoder = PngDecoder();
    // Returns true if the file is a valid PNG image.
    decoder.isValidFile(fileBytes);
    // Decodes the PNG image, returning null if the file is not a PNG.
    Image? image = decoder.decode(fileBytes);
    
    // startDecode will decode just the information from the image file without decoding the image data. 
    DecodeInfo? info = decoder.startDecode(fileBytes);
    if (info != null) {
      int width = info.width; // The width of the PNG image.
      int height = info.height; // The height of the PNG image.
      int numFrames = info.numFrames; // The number of frames, if it's an animated image, otherwise 1.
      final pngInfo = info as PngInfo; // The actual class of the info, in the case of PngDecoder.
      double? gamma = pngInfo.gamma; // The display gamma of the PNG
      int bits = pngInfo.bits; // How many bits per pixel for the PNG image data.
    }
    int numFrames = decoder.numFrames; // How many frames can be decoded.
    Image? frame0 = decoder.decodeFrame(0); // Decode the 1st frame if it's animated, otherwise the image itself.
  8. Use the Command API for asynchronous image processing

    main

    The Command API allows you to define a sequence of image operations (like decoding, resizing, blurring, and writing) using a fluent interface. You can execute these commands in the main thread using .execute() or in a separate Isolate thread using .executeThread() to prevent blocking the main event loop.

    import 'package:image/image.dart' as img;
    void main() async {
      // The Command API lets you define sequences of image commands to execute, and supports executing
      // in a separate Isolate thread.
      await (img.Command()
      // Decode the PNG image file
      ..decodeImageFile('test.png')
      // Resize the image to a width of 120 and a height that maintains the aspect ratio
      ..copyResize(width: 120)
      // Apply a blur to the image
      ..gaussianBlur(radius: 5)
      // Save the resized image to a PNG image file
      ..writeToFile('thumbnail.png'))
      // executeThread will run the commands in an Isolate thread
      .executeThread();
    }
  9. Understand Image data formats and dynamic range

    main

    The ImageData class supports various pixel formats categorized by dynamic range:

    Low Dynamic Range (LDR):

    • uint1: 1-bit per channel [0, 1]
    • uint2: 2-bit per channel [0, 3]
    • uint4: 4-bit per channel [0, 15]
    • uint8: 8-bit per channel [0, 255]

    High Dynamic Range (HDR):

    • uint16, uint32: Unsigned integers
    • int8, int16, int32: Signed integers
    • float16, float32, float64: Floating-point values

    Note on Bit Packing: Formats with less than 1 byte per channel (uint1, uint2, uint4) are stored in packed bits with a row stride. Bit packing is applied per pixel row, and unused bits at the end of a row are used as padding.

  10. How masking works in filter functions

    main

    Most drawing and filter functions in the Dart Image Library support a mask parameter. A mask is an image used to control the blending of the filter on a per-pixel basis.

    • Full Intensity: Where the mask channel is at full intensity, the filter has its maximum effect.
    • Zero Intensity: Where the mask channel is 0, the filter has no effect.
    • Blending: Values between 0 and full intensity will blend the filter effect with the original image.

    You can specify which channel of the mask (e.g., Channel.red, Channel.blue) or its luminance (Channel.luminance) to use for the blending value via the maskChannel parameter.

  11. Create and use bitmap fonts for text rendering

    main

    The Dart Image Library has limited support for text rendering via bitmap fonts. To use custom fonts, you must first convert a .ttf file into a .fnt zip format using an external tool like snowb.org.

    Workflow:

    1. Select a TTF: Choose a specific style from your font source (e.g., Roboto-Black.ttf from Google Fonts' /static folder).
    2. Convert: Use a tool to convert the .ttf to a .fnt zip.
      • Tip: Set the font color to white during conversion; this allows you to draw the text in any color using the library's drawing functions.
    3. Import: Use img.BitmapFont.fromZip() to load the font from the resulting zip file bytes.
    import 'package:image/image.dart' as img;
    import 'dart:io';
    
    void main() async {
      final fontZipFile = await File('font.zip').readAsBytes();
      final font = img.BitmapFont.fromZip(fontZipFile);
      final image = img.Image(width: 320, height: 200);
      img.drawString(image, 'Hello', font: font, x: 10, y: 100);
      await img.encodePngFile('testFont.png', image);
    }