simple_image

Image Editing for Eiffel: Load, Filter, Mask, Compose, Save

v0.1.0 MIT

Overview

simple_image is image editing in Eiffel: load, filter, mask, compose, save. It was built to make video thumbnails and reel covers without Python. Pixels live in C memory, every operation is an Eiffel feature with contracts over a C kernel, and drawing goes through simple_cairo.

Files go through simple_stb: PNG, JPEG, BMP, TGA, GIF, PSD and HDR in; PNG and JPEG out. The AI helpers (person cutout) are in simple_image_ai.

Part of the Simple Eiffel ecosystem.

Quick Start

Installation

  1. Set the environment variable:
    export SIMPLE_EIFFEL=/d/prod
  2. Add to your ECF file:
    <library name="simple_image" location="$SIMPLE_EIFFEL/simple_image/simple_image.ecf"/>

An Outlined Title with a Soft Shadow

local
    images: SIMPLE_IMAGE
do
    create images.make
    background := images.load ("frame.png")           -- detachable: check it
    background.enhance_brightness (0.5)
    background.vignette (0.6)

    shadow_canvas := images.new_mask_canvas (background.width, background.height)
    shadow_canvas.context.select_font ("Impact", shadow_canvas.context.Slant_normal, shadow_canvas.context.Weight_normal)
        .set_font_size (130).move_to (60, 660).text_path ("TITLE").fill.do_nothing
    shadow := shadow_canvas.to_mask
    shadow.grow (3)
    shadow.blur (6)

    canvas := images.canvas_from (background)
    canvas.fill_through (shadow, 0xC0000000, 6, 6)
    canvas.context.select_font ("Impact", ...).set_font_size (130).move_to (60, 660).text_path ("TITLE").do_nothing
    canvas.context.set_color_rgb (0, 0, 0).set_line_width (14).set_line_join (canvas.context.Join_round)
        .stroke_preserve.set_color_rgb (1, 0.83, 0.3).fill.do_nothing
    result := canvas.to_image
    if result.save_jpg ("thumbnail.jpg", 92) then ... end
end

Colors are NATURAL_32 words 0xAARRGGBB with straight alpha. Paths must be plain ASCII, because simple_stb opens files with fopen.

Key Features

Straight RGBA Images

IMG_IMAGE holds non-premultiplied RGBA, 8 bits per channel, in C memory.

Image Editing

Blur, Lanczos-3 resize, crop, brightness, contrast, color, vignette, flatten, alpha.

Masks

IMG_MASK is one 8-bit channel: blur, grow, shrink, threshold, invert, flood fill, resize, crop.

Keep the Largest Piece

keep_largest_piece removes specks from a cutout and keeps the soft edge.

Canvas Drawing

IMG_CANVAS places layers, paints a color through a mask, and draws text and shapes on simple_cairo.

Mask Canvases

A mask canvas turns whatever is drawn on it into a mask, for text shadows and glows.

API Reference

SIMPLE_IMAGE (Facade)

Image editing in Eiffel: load, filter, mask, compose, save.

Initialization

make

Ready to load and create images.

Loading

load (a_path: READABLE_STRING_GENERAL): detachable IMG_IMAGE

The image in the file as RGBA, or Void (see last_error).

load_mask (a_path: READABLE_STRING_GENERAL): detachable IMG_MASK

The file read as one gray channel, or Void (see last_error).

Creation

new_image (a_width, a_height: INTEGER): IMG_IMAGE

A transparent image.

new_mask (a_width, a_height: INTEGER): IMG_MASK

An empty mask.

new_canvas (a_width, a_height: INTEGER): IMG_CANVAS

A transparent color canvas.

new_mask_canvas (a_width, a_height: INTEGER): IMG_CANVAS

An empty mask canvas: what is drawn on it becomes a mask.

canvas_from (a_image: IMG_IMAGE): IMG_CANVAS

A color canvas holding a copy of the image.

gray_of (a_image: IMG_IMAGE): IMG_MASK

A mask of the image's gray levels (alpha ignored).

Status

last_error: STRING

Why the last load failed (empty when it did not).

has_error: BOOLEAN

Did the last load fail?

version: STRING

IMG_IMAGE

An image: straight (non-premultiplied) RGBA, 8 bits per channel, in C memory.

Initialization

make (a_width, a_height: INTEGER)

Transparent black image.

make_filled (a_width, a_height: INTEGER; a_color: NATURAL_32)

Image filled with a color (0xAARRGGBB).

Access

width: INTEGER
height: INTEGER
stride: INTEGER
pixel_count: INTEGER
pixels: POINTER

Address of the first pixel (R, G, B, A bytes). Valid until destroy.

pixel (a_x, a_y: INTEGER): NATURAL_32

Color at the position as 0xAARRGGBB, straight alpha.

alpha: IMG_MASK

A new mask holding this image's alpha channel.

Status

is_usable: BOOLEAN
is_opaque: BOOLEAN

Is every pixel fully opaque (alpha 255)?

is_inside (a_x, a_y, a_width, a_height: INTEGER): BOOLEAN

Does the rectangle lie wholly inside the image?

Pixels

set_pixel (a_x, a_y: INTEGER; a_color: NATURAL_32)

Set the color at the position.

fill (a_color: NATURAL_32)

Set every pixel to the color.

set_alpha (a_mask: IMG_MASK)

Use the mask as the alpha channel. Color under alpha 0 is kept.

Filters

blur (a_radius: INTEGER)

Three box passes of the radius (close to a gaussian of sigma about the radius); premultiplied in float in the kernel.

enhance_brightness (a_factor: REAL_64)

0 gives black, 1 leaves the image (PIL ImageEnhance.Brightness).

enhance_contrast (a_factor: REAL_64)

0 gives flat gray, 1 leaves the image (PIL ImageEnhance.Contrast).

enhance_color (a_factor: REAL_64)

0 gives grayscale, 1 leaves the image (PIL ImageEnhance.Color).

vignette (a_strength: REAL_64)

Darken toward the corners: a corner keeps 1 - strength of its brightness.

flatten_onto (a_background: NATURAL_32)

Composite onto an opaque color (0x__RRGGBB); the result is opaque.

New images

cropped (a_x, a_y, a_width, a_height: INTEGER): IMG_IMAGE

A new image of the given rectangle.

resized (a_width, a_height: INTEGER): IMG_IMAGE

Scaled with Lanczos-3, premultiplied in float so soft edges keep their color.

Files

save_png (a_path: READABLE_STRING_GENERAL): BOOLEAN

Write as PNG (alpha kept); True on success.

save_jpg (a_path: READABLE_STRING_GENERAL; a_quality: INTEGER): BOOLEAN

Write as JPEG at quality 1-100; requires an opaque image, so flatten first.

Duplication and removal

copy (a_other: like Current)

Make this image a separate copy of the other's pixels.

is_equal (a_other: like Current): BOOLEAN

Same size and same pixels?

destroy

Free the pixels now (the garbage collector frees them otherwise).

IMG_MASK

A mask: one 8-bit channel (0 = none, 255 = full), in C memory. Carries cutouts, halos, shadows and outlines.

Initialization

make (a_width, a_height: INTEGER)

Empty (all 0) mask.

make_filled (a_width, a_height: INTEGER; a_value: NATURAL_8)

Mask with every value set to a_value.

make_from_floats (a_floats: POINTER; a_width, a_height: INTEGER)

From float32 values, stretched so the lowest becomes 0 and the highest 255.

Access

width: INTEGER
height: INTEGER
pixel_count: INTEGER
values: POINTER

Address of the first value (rows packed, one byte each).

value (a_x, a_y: INTEGER): NATURAL_8

Value at the position.

count_at_least (a_level: INTEGER): INTEGER

Number of values >= a_level.

last_piece_size: INTEGER

Size of the piece kept by the last keep_largest_piece.

Status

is_usable: BOOLEAN
is_inside (a_x, a_y, a_width, a_height: INTEGER): BOOLEAN

Does the rectangle lie wholly inside the mask?

Values

set_value (a_x, a_y: INTEGER; a_value: NATURAL_8)

Set the value at the position.

fill (a_value: NATURAL_8)

Set every value to a_value.

Filters

blur (a_radius: INTEGER)

Soften with three box passes of the radius.

grow (a_amount: INTEGER)

Widen the mask by a_amount pixels (largest value over a square).

shrink (a_amount: INTEGER)

Narrow the mask by a_amount pixels (smallest value over a square).

threshold (a_level: INTEGER)

Make values >= a_level 255 and the rest 0.

invert

Replace each value v with 255 - v.

flood_fill (a_x, a_y: INTEGER; a_value: NATURAL_8)

Set the 4-connected region of equal values at the position to a_value.

keep_largest_piece (a_level: INTEGER)

Keep only the largest 4-connected piece of values >= a_level (with its soft edge, up to 2 pixels out).

New masks

cropped (a_x, a_y, a_width, a_height: INTEGER): IMG_MASK

A new mask of the given rectangle.

resized (a_width, a_height: INTEGER): IMG_MASK

Scaled with Lanczos-3.

Files, duplication and removal

save_png (a_path: READABLE_STRING_GENERAL): BOOLEAN

Write as a grayscale PNG; True on success.

copy (a_other: like Current)

Make this mask a separate copy of the other's values.

is_equal (a_other: like Current): BOOLEAN

Same size and same values?

destroy

Free the values now.

IMG_CANVAS

A drawing surface on simple_cairo, reached by copying: cairo is a seam, not the store. A color canvas holds ARGB32; a mask canvas holds A8.

Initialization

make (a_width, a_height: INTEGER)

Transparent color canvas.

make_mask (a_width, a_height: INTEGER)

Empty mask canvas (A8): whatever is drawn becomes coverage.

make_from (a_image: IMG_IMAGE)

Color canvas holding a copy of the image.

Access

width: INTEGER
height: INTEGER
is_mask: BOOLEAN

Is this an A8 mask canvas (rather than ARGB32 color)?

context: CAIRO_CONTEXT

For text, shapes and paths (simple_cairo).

is_usable: BOOLEAN

Drawing

place (a_image: IMG_IMAGE; a_x, a_y: REAL_64)

Paint the image over the canvas with its top-left corner at the position.

place_sized (a_image: IMG_IMAGE; a_x, a_y: REAL_64; a_width, a_height: INTEGER)

Paint the image scaled (Lanczos-3) at the position.

fill_through (a_mask: IMG_MASK; a_color: NATURAL_32; a_x, a_y: REAL_64)

Paint a color through the mask: halos, shadows, glows and text shadows.

fill (a_color: NATURAL_32)

Paint the whole canvas with the color over what is there.

Results and removal

to_image: IMG_IMAGE

A new image holding the canvas (straight alpha); color canvases only.

to_mask: IMG_MASK

A new mask holding the canvas's coverage; mask canvases only.

destroy

Release the cairo context and surface.

IMG_KERNELS

The per-pixel kernels: inline-C calls into Clib/simple_image.h, inherited (non-conforming) by IMG_IMAGE, IMG_MASK and IMG_CANVAS. All its features are implementation-only; there is no public API.

Design Notes

The design came from an /eiffel.debate cycle, recorded in .eiffel-workflow/debate/. Each decision was settled by measurement on 2026-10-11.

DecisionEvidence
Pixels stay straight RGBA in C memory, not in a cairo surfacePremultiplied 8-bit storage loses a mean of 30 levels of color at alpha 1-10
Every kernel that mixes soft-alpha pixels premultiplies in float inside the loopA straight blur turns a red edge dark (R 90 instead of 255)
Glows and shadows are a mask plus a color fillWithin 0.2 levels of a float premultiplied blur
Per-pixel loops in C, contracts on the Eiffel featuresEiffel loops with contracts on ran 3.6-4.4 times slower
Our own Lanczos-3, not cairo's scalercairo's best filter is off by up to 61 levels on mask edges; ours by 4 against PIL

Testing

/d/prod/ec.sh test -config simple_image.ecf -target simple_image_tests
PATH="/d/prod/simple_cairo:$PATH" ./EIFGENs/simple_image_tests/F_code/simple_image.exe   # from the library root

27 tests. They write to data/output/. The soft-edge tests were checked by breaking the kernel rule on purpose: a straight-color blur and resize fail them.

Dependencies