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
- Set the environment variable:
export SIMPLE_EIFFEL=/d/prod - 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
Ready to load and create images.
Loading
The image in the file as RGBA, or Void (see last_error).
The file read as one gray channel, or Void (see last_error).
Creation
A transparent image.
An empty mask.
A transparent color canvas.
An empty mask canvas: what is drawn on it becomes a mask.
A color canvas holding a copy of the image.
A mask of the image's gray levels (alpha ignored).
Status
Why the last load failed (empty when it did not).
Did the last load fail?
IMG_IMAGE
An image: straight (non-premultiplied) RGBA, 8 bits per channel, in C memory.
Initialization
Transparent black image.
Image filled with a color (0xAARRGGBB).
Access
Address of the first pixel (R, G, B, A bytes). Valid until destroy.
Color at the position as 0xAARRGGBB, straight alpha.
A new mask holding this image's alpha channel.
Status
Is every pixel fully opaque (alpha 255)?
Does the rectangle lie wholly inside the image?
Pixels
Set the color at the position.
Set every pixel to the color.
Use the mask as the alpha channel. Color under alpha 0 is kept.
Filters
Three box passes of the radius (close to a gaussian of sigma about the radius); premultiplied in float in the kernel.
0 gives black, 1 leaves the image (PIL ImageEnhance.Brightness).
0 gives flat gray, 1 leaves the image (PIL ImageEnhance.Contrast).
0 gives grayscale, 1 leaves the image (PIL ImageEnhance.Color).
Darken toward the corners: a corner keeps 1 - strength of its brightness.
Composite onto an opaque color (0x__RRGGBB); the result is opaque.
New images
A new image of the given rectangle.
Scaled with Lanczos-3, premultiplied in float so soft edges keep their color.
Files
Write as PNG (alpha kept); True on success.
Write as JPEG at quality 1-100; requires an opaque image, so flatten first.
Duplication and removal
Make this image a separate copy of the other's pixels.
Same size and same pixels?
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
Empty (all 0) mask.
Mask with every value set to a_value.
From float32 values, stretched so the lowest becomes 0 and the highest 255.
Access
Address of the first value (rows packed, one byte each).
Value at the position.
Number of values >= a_level.
Size of the piece kept by the last keep_largest_piece.
Status
Does the rectangle lie wholly inside the mask?
Values
Set the value at the position.
Set every value to a_value.
Filters
Soften with three box passes of the radius.
Widen the mask by a_amount pixels (largest value over a square).
Narrow the mask by a_amount pixels (smallest value over a square).
Make values >= a_level 255 and the rest 0.
Replace each value v with 255 - v.
Set the 4-connected region of equal values at the position to a_value.
Keep only the largest 4-connected piece of values >= a_level (with its soft edge, up to 2 pixels out).
New masks
A new mask of the given rectangle.
Scaled with Lanczos-3.
Files, duplication and removal
Write as a grayscale PNG; True on success.
Make this mask a separate copy of the other's values.
Same size and same values?
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
Transparent color canvas.
Empty mask canvas (A8): whatever is drawn becomes coverage.
Color canvas holding a copy of the image.
Access
Is this an A8 mask canvas (rather than ARGB32 color)?
For text, shapes and paths (simple_cairo).
Drawing
Paint the image over the canvas with its top-left corner at the position.
Paint the image scaled (Lanczos-3) at the position.
Paint a color through the mask: halos, shadows, glows and text shadows.
Paint the whole canvas with the color over what is there.
Results and removal
A new image holding the canvas (straight alpha); color canvases only.
A new mask holding the canvas's coverage; mask canvases only.
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.
| Decision | Evidence |
|---|---|
| Pixels stay straight RGBA in C memory, not in a cairo surface | Premultiplied 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 loop | A straight blur turns a red edge dark (R 90 instead of 255) |
| Glows and shadows are a mask plus a color fill | Within 0.2 levels of a float premultiplied blur |
| Per-pixel loops in C, contracts on the Eiffel features | Eiffel loops with contracts on ran 3.6-4.4 times slower |
| Our own Lanczos-3, not cairo's scaler | cairo'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
- simple_stb 1.1.0 or later (images free themselves; the stb implementation is static, so clients link)
- simple_cairo 1.4.0 or later (
text_path) - EiffelBase