crimage

Version, currently main branch1 version
  • main branchlatestJun 6, 2026

github.com/naqvis/crimage

A comprehensive 2D image processing library for Crystal with support for multiple formats (PNG, JPEG, GIF, BMP, TIFF, WebP, ICO), drawing primitives, text rendering with TrueType fonts, color transformations, filters, and effects. Includes SIMD-accelerated operations for performance-critical tasks.

34 stars
9 dependents
License: MIT

Installation

# Add this to your shard.yml
dependencies:
  crimage:
    github: naqvis/crimage
    branch: main

main is a branch, not a release, so this tracks it rather than pinning a version.

Then run:

shards install

shard.yml

Crystal
~> 1.0
License
MIT
Author
Ali Naqvi

Dependencies

This version declares no dependencies.

README

CrImage

A comprehensive pure-Crystal image processing library with no external dependencies.

Why CrImage?

A few highlights:

  • Complete image toolkit: formats, drawing, text, filters, effects — all in one shard
  • Built-in TrueType font engine (no FreeType dependency)
  • Animated GIF creation and multi-resolution ICO support
  • QR code generation, blurhash encoding, smart cropping
  • Decompression bomb protection for untrusted images

See Features at a Glance for the full list.

Installation

Add to your shard.yml:

dependencies:
  crimage:
    github: naqvis/crimage

Then run shards install.

Quick Start

require "crimage"

# Read any supported format (auto-detected)
img = CrImage.read("photo.jpg")

# Basic operations
resized = img.resize(800, 600, method: :lanczos)
thumbnail = img.thumb(200)  # 200x200 square

# Apply filters
result = img.brightness(20).contrast(1.2).blur(2)

# Save in any format
CrImage::PNG.write("output.png", result)
CrImage::WEBP.write("output.webp", result)

Features at a Glance

CategoryCapabilities
FormatsPNG, JPEG, GIF (animated), BMP, TIFF, WebP (VP8L), ICO
ColorRGBA, NRGBA, Gray, CMYK, YCbCr, HSV, HSL, LAB, Paletted
TransformResize (nearest/bilinear/bicubic/lanczos), rotate, crop, flip
FiltersBlur, sharpen, edge detection (Sobel/Prewitt/Roberts/Scharr)
EffectsSepia, emboss, vignette, temperature, grayscale, invert
AnalysisHistogram, CLAHE, SSIM, PSNR, perceptual hash, palette extraction
DrawingLines, circles, polygons, gradients, Bézier, patterns, chart helpers
TextTrueType/OpenType/WOFF fonts, multi-line, alignment, effects
UtilitiesThumbnails, watermarks, sprites, QR codes, blurhash, smart crop

Common Tasks

# Fit within bounds (preserves aspect ratio)
fitted = img.fit(800, 600)

# Fill exact dimensions (crops excess)
filled = img.fill(800, 600)

# Square thumbnail
avatar = img.thumb(200)

# High-quality resize
hq = img.resize(1920, 1080, method: :lanczos)
# Basic adjustments
brighter = img.brightness(30)      # -255 to 255
contrasted = img.contrast(1.5)     # 0.0 to 2.0
grayscale = img.grayscale
inverted = img.invert

# Effects
vintage = img.sepia
warm = img.temperature(30)         # positive = warmer
cool = img.temperature(-30)        # negative = cooler
# Blur
blurred = img.blur(radius: 3)
gaussian = img.blur_gaussian(radius: 5, sigma: 2.0)

# Sharpen
sharp = img.sharpen(amount: 1.5)

# Edge detection
edges = img.sobel
edges = img.prewitt
edges = img.detect_edges(threshold: 50)
img = CrImage.rgba(400, 400, CrImage::Color::WHITE)

# Shapes
img.draw_line(0, 0, 400, 400, color: CrImage::Color::RED)
img.draw_circle(200, 200, 50, color: CrImage::Color::BLUE, fill: true)
img.draw_rect(50, 50, 100, 80, stroke: CrImage::Color::BLACK)

# Thick anti-aliased lines
style = CrImage::Draw::LineStyle.new(CrImage::Color::RED, thickness: 5, anti_alias: true)
CrImage::Draw.line(img, CrImage::Point.new(50, 50), CrImage::Point.new(350, 350), style)

# Ring slice (for donut charts)
ring_style = CrImage::Draw::RingStyle.new(CrImage::Color::BLUE, fill: true)
CrImage::Draw.ring_slice(img, CrImage::Point.new(200, 200), 40, 80, 0.0, Math::PI, ring_style)

# Clipping regions
img.with_clip(50, 50, 200, 200) do |clipped|
  # Drawing here is restricted to the clip region
  clipped.draw_circle(100, 100, 150, color: CrImage::Color::GREEN, fill: true)
end

# Gradients
stops = [
  CrImage::Draw::ColorStop.new(0.0, CrImage::Color::RED),
  CrImage::Draw::ColorStop.new(1.0, CrImage::Color::BLUE)
]
gradient = CrImage::Draw::LinearGradient.new(
  CrImage::Point.new(0, 0),
  CrImage::Point.new(400, 0),
  stops
)
CrImage::Draw.fill_linear_gradient(img, img.bounds, gradient)
require "freetype"

font = FreeType::TrueType.load("path/to/font.ttf")
face = FreeType::TrueType.new_face(font, 48.0)

img = CrImage.rgba(400, 100, CrImage::Color::WHITE)
text_color = CrImage::Uniform.new(CrImage::Color::BLACK)
drawer = CrImage::Font::Drawer.new(img, text_color, face)

# Simple text
drawer.draw_text("Hello, World!", 20, 60)

# With effects
drawer.draw_text("Styled", 20, 60,
  shadow: true,
  outline: true,
  underline: true
)

# Easy text measurement
width = face.measure("Hello")
width, height = face.text_size("Hello")
line_height = face.line_height
frames = (0...10).map do |i|
  frame_img = CrImage.rgba(100, 100, CrImage::Color::WHITE)
  frame_img.draw_circle(10 + i * 8, 50, 10, color: CrImage::Color::RED, fill: true)
  CrImage::GIF::Frame.new(frame_img, delay: 10)  # 100ms per frame
end

animation = CrImage::GIF::Animation.new(frames, 100, 100, loop_count: 0)
CrImage::GIF.write_animation("animated.gif", animation)
# Simple
qr = CrImage.qr_code("https://example.com")
CrImage::PNG.write("qr.png", qr)

# With options
qr = CrImage.qr_code("Hello",
  size: 400,
  error_correction: :high,
  margin: 4
)
# Encode image to blurhash string
hash = img.to_blurhash  # => "LKO2?U%2Tw=w]~RBVZRi};RPxuwH"

# Decode to placeholder
placeholder = CrImage::Util::Blurhash.decode(hash, width: 32, height: 32)
original = CrImage.read("original.png")
modified = CrImage.read("modified.png")

mse = original.mse(modified)        # Mean Squared Error
psnr = original.psnr(modified)      # Peak Signal-to-Noise Ratio (dB)
ssim = original.ssim(modified)      # Structural Similarity (0-1)

# Visual diff for testing
diff = original.visual_diff(modified)
CrImage::PNG.write("diff.png", diff)

Font Support

The library includes a complete TrueType font engine:

FeatureStatus
TrueType (.ttf)✅ Full support
OpenType/CFF (.otf)✅ Full support
WOFF (.woff)✅ Auto-decompression
TrueType Collections (.ttc)✅ Multi-font files
Legacy kern table✅ Pair kerning
GPOS kerning✅ Modern pair positioning
GSUB ligatures✅ Standard ligatures (fi/fl/ffi/ffl)
Vertical text✅ Top-to-bottom layout
Variable fonts⚠️ Axis detection only
Complex scripts (Arabic, Indic)❌ Requires external shaper

Note: Text rendering uses left-to-right glyph placement. Complex scripts (Arabic, Hebrew, Thai, Indic) and advanced OpenType features (contextual forms, mark positioning) require an external shaping engine like HarfBuzz.

Architecture

CrImage::Image          # Base interface for all image types
├── CrImage::RGBA       # 8-bit RGBA (premultiplied alpha)
├── CrImage::NRGBA      # 8-bit RGBA (non-premultiplied)
├── CrImage::Gray       # 8-bit grayscale
├── CrImage::Alpha      # 8-bit alpha channel
├── CrImage::CMYK       # Print color model
├── CrImage::YCbCr      # JPEG color model
├── CrImage::Paletted   # Indexed color (GIF)
└── CrImage::Uniform    # Solid color fill

CrImage::Draw           # Drawing primitives
CrImage::Transform      # Resize, rotate, filters
CrImage::Util           # Thumbnails, watermarks, etc.
CrImage::Font           # Text rendering
FreeType::TrueType      # Font parsing

Documentation

GuideDescription
Quick Start10 common tasks with examples
Image FormatsSupported formats and adding custom ones
Drawing & TextPrimitives, gradients, fonts
Transforms & FiltersResize, rotate, blur, effects
Color ModelsRGBA, HSV, LAB conversions
UtilitiesThumbnails, watermarks, QR, blurhash
PerformanceOptimization and thread-safety
SecurityHandling untrusted images

Examples: See examples/ for 60+ working demos.

API Docs: Run crystal docs and open docs/index.html.

Development

crystal spec          # Run tests
crystal docs          # Generate API docs

Contributing

  1. Fork it (https://github.com/naqvis/crimage/fork)
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create a new Pull Request

License

MIT License. See LICENSE for details.

Contributors