Image

repository·main·Indexed 19 days ago

https://github.com/elixir-image/image

A high-performance, memory-efficient image processing library for Elixir built on top of libvips via Vix. It provides a functional API for resizing, cropping, color management, text rendering, and metadata manipulation. Features include CSS Color 5 support, dominant color extraction, video frame extraction via FFmpeg (through the :xav dependency), and Low Quality Image Placeholder (LQIP) generation for CSS.

Tokens
13K
Snippets
52
Records
64
Agent score
66%

What's inside Image

  1. Choosing a method for `Image.dominant_color/2`

    main

    The Image.dominant_color/2 function supports two different methods for extracting dominant colors, each with different performance and quality characteristics:

    1. :histogram (Default)

      • How it works: Uses a coarse 3D RGB histogram via vips_hist_find_ndim to return the centers of the most populated bins.
      • Performance: Extremely fast (two to three orders of magnitude faster than :imagequant). It scales well with image size and its cost barely increases with the number of colors requested (top_n).
      • Best for: Hot paths, bulk processing, or simple questions like "what is the overall dominant color?"
      • Output: Colors are quantized to the centers of a fixed 3D grid.
    2. :imagequant

      • How it works: Routes through vips_gifsave_buffer to run libimagequant, creating a quantized Global Color Table (GCT). The GCT is then parsed into RGB tuples ordered by perceptual importance.
      • Performance: Significantly slower. The cost scales with both pixel count and the number of colors requested (top_n).
      • Best for: High-quality color extraction where palette quality matters more than latency (e.g., building UI swatches or accents from photographs).
      • Output: Perceptually representative colors.

    Optimizing :imagequant with :effort

    You can control the quantization quality/speed using the :effort option. On larger images, dropping from the default effort: 7 to effort: 3 can cut runtime by approximately 5× with minimal perceived quality loss.

    # Example usage (conceptual)
    Image.dominant_color(image, :histogram, top_n: 5)
    Image.dominant_color(image, :imagequant, top_n: 5, effort: 3)
  2. Security considerations for image processing

    main

    When using :image and libvips, keep the following security practices in mind:

    • Process Crashes: Since libvips is written in C, a malicious input could potentially crash the BEAM. However, libvips has a significantly smaller attack surface compared to ImageMagick.
    • Untrusted Loaders: The :image application defaults to VIPS_BLOCK_UNTRUSTED=TRUE to block dangerous format loaders.
    • Metadata Sanitization: When displaying user-supplied images on web pages, always sanitize EXIF/XMP metadata to prevent embedded HTML/XSS attacks.
    • Resource Exhaustion: Image processing is CPU-intensive. In multi-tenant environments, use VIPS_CONCURRENCY to limit thread usage and prevent CPU starvation.
  3. Basic image operations: Open, Transform, and Write

    main

    You can open an image from a file, apply transformations like thumbnail, and write the result to disk. Use the ! variants (e.g., Image.write!) to raise errors instead of returning {:ok, value} or {:error, %Image.Error{}}.

    {:ok, image} = Image.open("photo.jpg")
    {:ok, thumb} = Image.thumbnail(image, 256)
    :ok = Image.write(thumb, "thumb.jpg", quality: 85)
  4. Install the Image library

    main

    Add :image to your mix.exs dependencies. By default, libvips is bundled via :vix, so no system-wide installation is required for standard usage. This makes it ideal for environments like Livebook or Heroku.

    def deps do
      [
        {:image, "~> 0.71"}
      ]
    end
  5. Implement LQIP CSS placeholders in HTML

    main

    Once you have generated the hex value using Image.Lqip.Css.encode/1, you must pass it to your HTML element so the CSS can unpack it.

    Set the value as an inline style using the --lqip variable. This is currently the safest method for browser compatibility.

    <img src="photo.jpg" style="--lqip: #a1b2c3d4" />

    Option 2: Data Attribute

    You can use a data-lqip attribute, which is accessed by the stylesheet using the attr() function. Note that browser support for attr() with type casting is limited.

    <img src="photo.jpg" data-lqip="#a1b2c3d4" />
  6. Optimize `:imagequant` performance with `:effort`

    main

    When using the :imagequant method with Image.dominant_color/2, you can use the :effort option to balance quantization quality against CPU cost.

    • Low Effort (1-3): Very fast; effort 1 and 3 are often indistinguishable in timing. Use this for a significant speed boost with minimal quality loss.
    • Medium Effort (5): A middle ground in terms of timing and quality.
    • High Effort (7-10): Default is 7. Increasing to 10 roughly doubles the effort compared to 7. Use this when maximum perceptual accuracy is required.

    Rule of thumb: If you need :imagequant but require better performance, try effort: 3.

    # Note: The exact API signature for the effort option should be verified in the Image module documentation.
    Image.dominant_color(image, :imagequant, top_n: 8, effort: 3)
  7. Install FFmpeg for video frame extraction

    main

    To use Image.Video (frame extraction, seeking, etc.), you must install FFmpeg 6.x or 7.x on your system. This feature is enabled via the :xav optional dependency.

    Note: Windows is not currently supported by Xav.

    # macOS
    brew install ffmpeg
    
    # Debian / Ubuntu
    apt install libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libavdevice-dev
  8. Use the LQIP CSS stylesheet

    main

    To render the placeholders, include the following CSS in your stylesheet. This CSS is a verbatim copy of the reference implementation and uses relative color syntax to unpack the 32-bit hex value into three distinct colors and apply them as radial gradients.

    [data-lqip] {
      --lqip-c: attr(data-lqip type(<color>), white);
    }
    
    [style*="--lqip:"] {
      --lqip-c: var(--lqip);
    }
    
    [style*="--lqip"],
    [data-lqip] {
      --lqip-c0: color(
        from var(--lqip-c) srgb calc(round(down, r * 255 / pow(2, 4)) / 15)
          calc(mod(round(down, r * 255), pow(2, 4)) / 15)
          calc(round(down, g * 255 / pow(2, 5)) / 7) / 1
      );
    
      --lqip-c1: color(
        from var(--lqip-c) srgb calc(mod(round(down, g * 255 / 2), pow(2, 4)) / 15)
          calc(
            (
                (mod(round(down, g * 255), 2) * pow(2, 3)) +
                  (round(down, b * 255 / pow(2, 5)))
              ) /
              15
          )
          calc(mod(round(down, b * 255 / pow(2, 2)), pow(2, 3)) / 7) / 1
      );
    
      --lqip-c2: color(
        from var(--lqip-c) srgb
          calc(
            (
                ((mod(round(down, b * 255), pow(2, 2)) * 2)) +
                  round(down, alpha * 255 / pow(2, 7))
              ) /
              7
          )
          calc(mod(round(down, alpha * 255 / pow(2, 3)), pow(2, 4)) / 15)
          calc(mod(round(down, alpha * 255), pow(2, 3)) / 7) / 1
      );
    
      background:
        radial-gradient(
          150% 75% at 80% 100%,
          var(--lqip-c2),
          rgb(from var(--lqip-c2) r g b / 98%) 10%,
          rgb(from var(--lqip-c2) r g b / 92%) 20%,
          rgb(from var(--lqip-c2) r g b / 82%) 30%,
          rgb(from var(--lqip-c2) r g b / 68%) 40%,
          rgb(from var(--lqip-c2) r g b / 32%) 60%,
          rgb(from var(--lqip-c2) r g b / 18%) 70%,
          rgb(from var(--lqip-c2) r g b / 8%) 80%,
          rgb(from var(--lqip-c2) r g b / 2%) 90%,
          transparent
        ),
        radial-gradient(
          100% 75% at 40% 50%,
          var(--lqip-c1),
          rgb(from var(--lqip-c1) r g b / 98%) 10%,
          rgb(from var(--lqip-c1) r g b / 92%) 20%,
          rgb(from var(--lqip-c1) r g b / 82%) 30%,
          rgb(from var(--lqip-c1) r g b / 68%) 40%,
          rgb(from var(--lqip-c1) r g b / 32%) 60%,
          rgb(from var(--lqip-c1) r g b / 18%) 70%,
          rgb(from var(--lqip-c1) r g b / 8%) 80%,
          rgb(from var(--lqip-c1) r g b / 2%) 90%,
          transparent
        ),
        var(--lqip-c0);
    }```
    
  9. Configure platform-provided libvips

    main

    If you need additional format support (like HEIF or JPEG XL), you can use your system's libvips instead of the bundled version.

    1. Install libvips via your package manager (e.g., brew install libvips or apt install libvips-dev).
    2. Set the environment variable VIX_COMPILATION_MODE=PLATFORM_PROVIDED_LIBVIPS at both compile time and runtime.
  10. Handle Image errors with pattern matching

    main

    Fallible functions return {:ok, value} or {:error, %Image.Error{}}. The error struct contains :reason, :operation, :path, :value, and a :message. You can pattern match on the :reason atom to handle specific failures like missing files or unsupported formats.

    case Image.open(path) do
      {:ok, image} -> use_image(image)
      {:error, %Image.Error{reason: :enoent}} -> not_found(path)
      {:error, %Image.Error{reason: :unsupported_format}} -> wrong_format(path)
      {:error, %Image.Error{} = error} -> raise error
    end
  11. Introspect social media image sizes with Image.Social

    main

    The Image.Social module provides tools to discover standard image dimensions for various social media platforms. You can retrieve the full map of sizes, a list of supported platforms, or specific image usages (like :profile or :story) for a given platform.

    Supported platforms include:

    • :facebook
    • :twitter
    • :linkedin
    • :pinterest
    • :instagram
    • :tumblr
    • :youtube
    • :snapchat
    • :tiktok
    # Get all sizes
    sizes = Image.Social.media_sizes()
    
    # Get specific size for a platform
    get_in(sizes, [:youtube, :thumbnail]) # => "1280x720"
    
    # Check supported platforms
    :twitter in Image.Social.known_platforms()
  12. Supported blend modes for image compositing

    main

    When using Image.compose/3, you can specify a blend mode to determine how two images are combined. The Image.BlendMode module provides a set of Elixir-friendly atoms that map to underlying libvips modes.

    Available Modes

    • :over: (Default) Shows the top image over the bottom image (like two semi-transparent slides).
    • :clear: Removes the first image where the second is drawn.
    • :source: Draws the second image as if nothing were below it.
    • :in: The first image is removed; the second is only drawn where the first was.
    • :out: The second is drawn only where the first isn't.
    • :atop: Leaves the first image mostly intact, but mixes both in the overlapping area.
    • :dest: Leaves the first image untouched; the second is discarded.
    • :dest_over: Swaps the arguments of :over.
    • :dest_in: Swaps the arguments of :in.
    • :dest_out: Swaps the arguments of :out.
    • :dest_atop: Swaps the arguments of :atop.
    • :xor: A difference-style operator.
    • :add: Adds the two images together.
    • :saturate: Uses the darker of the two.
    • :multiply: At least as dark as the darker of the two inputs.
    • :screen: At least as light as the lighter of the inputs.
    • :overlay: Multiplies or screens colors depending on lightness.
    • :darken: The darker of each component.
    • :lighten: The lighter of each component.
    • :colour_dodge: Brightens the first by a factor of the second.
    • :colour_burn: Darkens the first by a factor of the second.
    • :hard_light: Multiplies or screens depending on lightness.
    • :soft_light: Darkens or lightens depending on lightness.
    • :difference: The difference between the two.
    • :exclusion: Similar to :difference, but with lower contrast.