RFC: Standard Image Types in Stdlib

Please consider the recommendation not to include “crystal” in a shard name. (ref shards/docs/shard.yml.adoc at master · crystal-lang/shards · GitHub)

yeah, that’s just a repo name. shard name will be just image

1 Like

Thanks everyone for the feedback. Pushed code to crystal-image

We landed on a design that prioritizes safety and interoperability while exposing raw performance where needed:

  • Foundation: Immutable Point/Rect and standard Color structs.
  • Interface: A unified PixelBuffer(T) that supports generic algorithms, with stride awareness for optimization.
  • Performance: Concrete types (RGBA8, etc.) expose raw Bytes buffers and safe/unsafe access patterns.

PRs and further testing are welcome!

1 Like

Looking at the suggested Pixelbuffers I think I would have preferred them even simpler - that is having them without any rectangle abstraction except width and height - that is no nonzero start. To many use cases introducing Rects will just be a source of extra boilerplate and confusion.

Views into (pixel)buffers that share the same memory can just as well be a separate thing, for those who want that. They’d also be very easy to construct with a rect and a pixelbuffer.

(I’m up for doing a pr for separating them, but we should probably agree on the resolution first)

I understand the appeal of simplicity, but I think the Rectangle bounds design is the right choice here. Let me explain why:

1. The complexity is already hidden for simple cases

Users creating images never see or think about bounds:

img = Image.rgba(640, 480)
img[10, 20] = color  # Just works, no Rectangle in sight

The bounds abstraction only surfaces when you explicitly ask for it. There’s no extra boilerplate for the common case.

2. Separate view wrappers create worse complexity

If views are separate types, you fragment the ecosystem:

  • Functions need to accept both Image and ImageView
  • Type signatures become Image | ImageView or require generic constraints
  • Libraries have to handle both, or pick one and lose functionality

With bounds in the interface, a sub-image is an image - it works everywhere an image works, with zero special handling.

3. Sub-images aren’t a niche feature

Image processing commonly needs:

  • Tiling large images for parallel processing
  • Processing regions without copying data
  • Cropping without allocation
  • Border handling in filters

Making this a “separate thing for those who want it” pushes a common need into second-class status.

The current design gives you simplicity when you want it, and power when you need it, without forcing users to choose between two different types.

That said, I’d love to hear more about your thoughts on this - maybe I’m missing something!

1: Not enough. And limiting the purpose of this to just focusing on ‘creating images’ is one thing I’d disagree with.

2: With good and consistent APIs most of this is avoided.

3: All your examples seem pretty niche from my point of view. I’m not arguing views shouldn’t exist, just that they are a separate thing than to the root of the tree that usually will own the allocation.

Personally, I’d prefer something like pixelbuffer.cr · GitHub . I took the liberty to push some logic into `Rectangle` that isn’t provided.

Most operations just work directly on any `Buffer` so in most cases you can just type your things to that, rather than to both `Image` and `View`. I also took the opportunity to switch from `Bytes` to `Slice(Color)` as it simplifies a lot.

That said I’m uncertain of if it actually should use `Pointer(Color)` as the base type rather than slice, as images are one place where it actually is pretty damned easy to hit the size limits of slices.

Thanks for the concrete implementation - helpful to see the alternative!

On Slice(Color) vs Bytes:

I actually started with Slice(Color) but changed to Bytes in later iterations for C library interop - image codecs work with raw byte buffers, and Bytes gives direct zero-copy interop without unsafe casting.

On Image/BufferView separation vs single type:

I considered class hierarchies but avoided them - virtual types runtime dispatch overhead. Module + generics (PixelBuffer(T)) gives compile-time polymorphism with zero runtime cost.

Your Buffer module serves the same role as my PixelBuffer(T) - providing a common interface. The difference is whether sub-images are a separate type or the same type with different bounds. I chose the latter for uniformity (sub-images work everywhere images work), you chose the former for simplicity (most images are simple). Both valid depending on use cases.