Announcing CrImage - Pure Crystal Image Processing Library

I’m excited to share CrImage, a comprehensive image processing library written entirely in Crystal with zero external dependencies.

What is it?

A complete image toolkit that handles everything from basic format conversion to advanced effects, text rendering, and even QR code generation — all in pure Crystal.

Features

Formats: PNG, JPEG, GIF (animated!), BMP, TIFF, WebP, ICO

Drawing: Lines, circles, polygons, Bézier curves, gradients, anti-aliased rendering, chart helpers

Transforms: Resize (nearest/bilinear/bicubic/lanczos), rotate, crop, flip

Filters & Effects: Blur, sharpen, edge detection, sepia, vignette, temperature adjustment

Text Rendering: Built-in TrueType/OpenType/WOFF font engine with kerning and ligatures — no FreeType needed

Utilities: QR codes, blurhash, smart crop, watermarks, thumbnails, EXIF reading, image comparison (SSIM/PSNR)

Security: Decompression bomb protection built-in

Quick taste

require "crimage"

img = CrImage.read("photo.jpg")
    .resize(800, 600, method: :lanczos)
    .brightness(10)
    .sepia

CrImage::PNG.write("vintage.png", img)

# Generate a QR code
qr = CrImage.qr_code("https://crystal-lang.org")
CrImage::PNG.write("qr.png", qr)

24 Likes

Wow, this looks very useful. And it’s a huge project! :heart_eyes:
Doing all of this with zero dependencies requires lots of homemade implementations.
Congrats on writing such a big codebase :tada:

Thanks @straight-shoota , I had been working on it on and off since past 2+ years.

This is awesome, a few days ago I was wondering if there was a good image library without fooling around with image magick. Will for sure keep this in mind whenever I get that next project with image manipulation.

2 Likes

Awesome! I expect I will be using this in half a dozen projects in the next week :slight_smile:

2 Likes

This looks positively amazing. I’m much rather using this than depending on Cairo to render text.

Some assorted thoughts:

API: The API is based on using files and/or IO as the source and target. In many cases that is great and what you want, but there are some cases where it is not so great - for example when either the input and/or output needs to be in the form of a buffer. Think integrating this with either a video stream from the camera (working on frames), or if you are painting directly something that is to be shown to the screen. Then you get a buffer - either empty or with data, and needs to either fill it or transform it, and then either let the buffer be used as is or export it somehow.

It isn’t mentioned in the guide, but I found a constructor that sortof helps by looking at the code, but it is a bit more involved than would be ideal (well, even more optimal would be a no-allocation path that allows hot-switching the buffer you are working with, but that is more a nice to have). In practice this also add a dependency on matching the binary layout of the formats to match what GPUs and external APIs use (as far as possible. I think most usage is consistent between different places of usage, but who knows if some place use different endians or whatever).

Pixelbuffers: We are getting to a point where there is a whole bunch of different libraries that interact with images in one way or another (either modifying them, or being a source or target), and it is getting to a point where it could make sense extract a common API for working with them in a way that make interopability easy and nonconfusing wrt color spaces (and preferably: typesafe - now any user will have to cast our respective pointers and essentially pray that the binary layer match). This of course doesn’t need to be done by you (or me - I’m also guilty of adding to the mess), but perhaps it would be a good idea if someone at some point made a common layer that dealt with this in easy and performant ways.

Text rendering: Amazing to have, and a pain to implement. Two things that are not mentioned explicitly in the guides are system fonts and built in fonts, so I assume they are not there. Sometimes you just want to print something and either doesn’t care or want to conform with whatever the rest of the system is using. Access to system fonts may be outside the domain of the library to solve (Are there any cross platform solutions to this? Is it even possible without eg linking FontConfig?), but would be very amazing to have.

But yeah, I’ll definitely use this library. Looks great and solves a bunch of problems I have that were previously unsolved or not as nice!

1 Like

API / Buffer access:
CrImage does support direct buffer access:

# Create from existing buffer

img = CrImage::RGBA.new(pixel_bytes, stride, CrImage.rect(0, 0, width, height))

# Access raw pixel buffer

raw = img.pix  # Returns Bytes

Layout is row-major, RGBA order. For GPU interop you’d need to match expected format. A zero-allocation hot-swap API for video/realtime is a good idea - noted for future.

Pixelbuffers / Common API:

Agreed. A shared `Pixelbuffer` shard with type-safe color space handling would be valuable. Happy to collaborate if someone wants to lead that.

Text rendering / System fonts:

By design - CrImage requires explicit font paths. System font discovery needs fontconfig (Linux), CoreText (macOS), or DirectWrite (Windows) which would break the zero-dependency goal. Could be a separate shard that provides font paths to CrImage.

Thanks for the detailed feedback!

CrImage::RGBA.new(pixel_bytes, stride, CrImage.rect(0, 0, width, height))

Right, that is the one I found. A little unwieldy, as the only things really needed is `pixel_bytes`, `width` and `height` (and the knowledge of what the byte size of RGBA is, to calculate the stride). But perhaps all that can be implicit in a pixelbuffer :thinking:

Right, but does R have bit 0-7 from the left or from the right? There are conventions in this space

For GPU interop you’d need to match expected format.

and to avoid needless order conversions when interacting with the outer world, it may be worth choosing the same as what both other projects(FFMPEG, GPUs etc) and serialized file representation uses.

Pixelbuffers

Me too. Too many projects, too little time :frowning:

> A little unwieldy

Fair point. A convenience constructor like `CrImage::RGBA.from_buffer(bytes, width, height)` that calculates stride internally would be cleaner. Noted.

> does R have bit 0-7 from the left or from the right?

CrImage uses RGBA order: `pix[0]=R, pix[1]=G, pix[2]=B, pix[3]=A`. Standard left-to-right byte order.

> avoid needless order conversions

You’re right - BMP writer does RGB→BGR conversion. CrImage chose RGBA because it matches PNG, WebP, and most image formats. BMP/Windows APIs use BGRA which is the outlier. For GPU work (OpenGL/Vulkan), RGBA is typically the native format. FFMPEG supports both via pixel format flags.

The tradeoff was: optimize for file format compatibility (RGBA) vs Windows/BMP compatibility (BGRA). Went with RGBA since it’s more universal.

6 posts were split to a new topic: Using Software Engineering agents

First: tartrazine uses this to generate images with highlighted code

3 Likes

Gosh this is cool! I hope I’d be able to check it out when I get to rewrite the GUI part of my project (can it do 60FPS, at least in theory?). Even though right now my thing is engineered around vector graphics, I’m willing to change that, seeing the state of things in that land! Freetype stuff looks useful, it’d be nice if there was a HarfBuzz wrapper/API as well. The last time I tried to call its C API it gave me some weird answers so I decided I don’t need text shaping after all. Rounded rectangles with different border widths and radii were very hard for me, too, and I need both for my thing; even better is also different border color for different sides – that I wasn’t able to achieve, ever; one seems to need a proper vector thing for that, like Skia, with intersection, union, and others on arbitrary paths. And calling Skia from Crystal is pretty much impossible. It’s nice there are people like you who like this stuff. At least I hope you do, because otherwise it’s miserable, very miserable. I’ve been there, and it’s a tough fight every time… I like immutable data structures and weird symbolic cellular automata stuff, not fighting one-pixel rasterization artifacts on the border of a rounded rect in one of the gazillion edge cases !!! :slight_smile:

> Can it do 60 FPS, at least in theory? …

60 FPS: Theoretically yes, but GIF format limits you. Frame delays use centiseconds (1/100th second), so true 60 FPS (16.67ms) isn’t possible with integer values. You can get 50 FPS (delay: 2) or 100 FPS (delay: 1), though browsers often cap GIF frame rates anyway. For actual 60 FPS, export individual frames and use video formats or canvas animations.

HarfBuzz wrapper…

No integration because CrImage design goal is pure Crystal with zero external dependencies.

Variable border widths and radii…

Built-in border utilities only support uniform width/color. BUT you can create variable borders manually using drawing primitives - draw individual rectangles or lines for each side with different widths/colors. The Polaroid example shows this (different side vs bottom border widths).

GUI use

This is an image processing library, not a GUI framework. Zero windowing/events. You’d use it within GTK/Qt for image generation only.

Bottom line: Won’t solve your border edge cases. Still need Skia-level vector ops for that stuff.
Stick with the cellular automata - way more interesting!

1 Like

(不装了,直接用中文回复!)虽然我在图片生成方面的知识几乎可以说 一片空白, 但是我仍旧认为这是最近一年来我见过的社区最有用、最强大的 shard!

感谢 naqvis 大佬!

1 Like

哈哈,感谢老郑的夸奖 :grinning_face_with_smiling_eyes: 能被觉得“有用”就是最大的肯定了,有任何使用反馈或想法都欢迎继续交流。

1 Like

I tried running the specs on Windows, and all of them pass except for the ones with hardcoded /tmp/ paths (they should use File.tempname instead).

1 Like

Thanks, I might have missed some there. Will fix them