module

Hwaro::Content::Processors::ImageProcessor

Constants

ANCHORS = {"center", "top", "bottom", "left", "right", "top_left", "top_right", "bottom_left", "bottom_right"}
IMAGE_EXTENSIONS = {".jpg", ".jpeg", ".png", ".bmp"}

Only formats that stb can both read AND write back correctly.

MAX_PIXELS = 64000000_i64

Maximum pixel count to prevent excessive memory allocation (64 megapixels)

PNG_SIGNATURE = Bytes[137, 80, 78, 71, 13, 10, 26, 10]
RESIZE_OPS = {"fit", "fill", "crop"}

resize_image(op=…) operations and crop anchors.

SVG_HEIGHT_RE = /\sheight\s*=\s*["']\s*(\d+(?:\.\d+)?)\s*(?:px\s*)?["']/i
SVG_TAG_RE = /<svg\b[^>]*>/i
SVG_VIEWBOX_RE = /\sviewBox\s*=\s*["']\s*-?[\d.]+[\s,]+-?[\d.]+[\s,]+(\d+(?:\.\d+)?)[\s,]+(\d+(?:\.\d+)?)\s*["']/i
SVG_WIDTH_RE = /\swidth\s*=\s*["']\s*(\d+(?:\.\d+)?)\s*(?:px\s*)?["']/i

(?:px\s*)? rather than (?:px)?\s*: the two \s* runs must not be able to split the same whitespace, or a long run backtracks quadratically into PCRE's match limit.

Instance methods

dimensions(path : String) : Tuple(Int32, Int32) | Nil

Read the intrinsic {width, height} of an image from its file header without decoding pixel data (pure Crystal, no stb round-trip). Supports the formats image? accepts (PNG/JPEG/BMP) plus GIF, WebP and SVG (read-only formats, measured for content <img> sizes); returns nil for anything unreadable so callers can fall back to other heuristics. Used by the warm-build variant-reuse check: inferring the source width from the largest on-disk variant silently ignored newly configured LARGER widths (the variant set "matched" after clamping to the stale inferred width).

Source
dominant_color(pixels : Pointer(UInt8), w : Int32, h : Int32, channels : Int32) : String

Compute dominant color as a hex string (e.g., "#a3b2c1"). Channels: 1=gray, 2=gray+alpha, 3=RGB, 4=RGBA

Pixels are weighted by alpha: a transparent pixel's color bytes are invisible (usually black), so averaging them in darkened every logo on a transparent background. A fully transparent image has no color and gets the white its LQIP is flattened onto.

Source
generate_lqip_with_color(pixels : Pointer(UInt8), src_w : Int32, src_h : Int32, channels : Int32, lqip_width : Int32 = 32, quality : Int32 = 20) : Tuple(String, String) | Nil

Generate LQIP data URI and dominant color from the thumbnail in one pass. Returns {data_uri, dominant_color_hex} or nil on failure.

Source
image?(path : String) : Bool

Check if a file is a resizable image based on extension

Source
orientation(path : String) : Int32

EXIF Orientation (1-8) of the picture a decoder returns for path: 1 (upright, or no tag) for everything but a JPEG that carries one.

Source
reorient(pixels : Pointer(UInt8), w : Int32, h : Int32, channels : Int32, orientation : Int32) : Tuple(Pointer(UInt8), Int32, Int32) | Nil

Turn decoded pixels (w×h, channels interleaved) upright for EXIF orientation 2-8, into a freshly allocated buffer, and free the input. stb decodes the stored pixels and the encoders write no EXIF, so a variant cut from them would show a phone photo on its side. Orientation 1 (and anything unknown) hands the input back. Returns {pixels, width, height}; nil if the new buffer cannot be allocated (the input is then untouched).

Source
resize_and_lqip(source : String, dest_dir : String, widths : Array(Int32), quality : Int32 = 85, lqip_width : Int32 = 32, lqip_quality : Int32 = 20, authored : Proc(String, Bool) | Nil = nil, extra : Hash(String, Hash(Int32, String)) | Nil = nil) : Tuple(Hash(Int32, String), String | Nil, String)

Combined resize + LQIP in a single decode pass. Returns {width_map, lqip_data_uri_or_nil, dominant_color_hex}

authored (when given) is asked about each <name>_<w>w.<ext> this would write: true means a file the site itself publishes under that name, which is left alone — that width is simply not generated.

extra names further output formats by its keys ("webp"); each width is then also written as <name>_<w>w.<format> from the same resized pixels, and the format's width => path map is filled in.

Source
transform(source : String, dest : String, width : Int32, height : Int32, op : String, anchor : String, quality : Int32 = 85) : Tuple(Int32, Int32) | Nil

One fill / crop variant of source written to dest. fill: scale to cover width×height, then crop to exactly that box around anchor (may upscale — the box is the contract). crop: no scaling; cut a width×height region at anchor, clamped to the source size. Returns the written {width, height}, or nil when the source cannot be decoded or the variant cannot be written.

Source