Summary
Add standard image types (colors, pixel formats, color spaces) to Crystal’s standard library to provide a common foundation for the image processing ecosystem.
Note
This RFC is meant to start a discussion about standardizing image types in Crystal. If the community finds value in this approach, anyone interested can help move it forward.
Motivation
The Problem: Ecosystem Fragmentation
Crystal’s image ecosystem is fragmented. Each library defines its own incompatible types:
Library A:
struct RGBA
property r, g, b, a : UInt16
end
Library B:
module Color
struct RGBA
property r, g, b, a : UInt8
end
end
Library C:
struct Color
property r, g, b, a : UInt8
end
Result:
- Developers must learn different APIs for each library
- No shared understanding of color representation
- Unclear semantics (premultiplied? color space? bit depth?)
- Difficult to build tools that work across libraries
- Each library reinvents basic concepts
Comparison to Other Languages
Go - Standard library provides foundation:
// In stdlib
package image/color
type RGBA struct {
R, G, B, A uint8
}
type RGBA64 struct {
R, G, B, A uint16
}
All Go image libraries use these types.
Rust - Community standard via image crate:
pub trait Pixel {
type Subpixel: Primitive;
const CHANNEL_COUNT: u8;
}
pub struct Rgba<T>([T; 4]);
pub struct Rgb<T>([T; 3]);
Most Rust image libraries implement these traits.
Python - PIL/Pillow provides foundation:
from PIL import Image
# Standard modes: "RGB", "RGBA", "L" (grayscale), etc.
img = Image.new("RGB", (width, height))
pixel = img.getpixel((x, y)) # Returns (r, g, b) tuple
Most Python image libraries interoperate via PIL’s standard modes.
Java/Kotlin - Java stdlib provides foundation:
// In java.awt
BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
int rgb = img.getRGB(x, y); // Standard packed ARGB format
Kotlin and Android use java.awt.BufferedImage or android.graphics.Bitmap.
What We Need
A minimal foundation that defines:
- Standard color types with clear semantics
- Standard pixel format definitions
- Standard buffer interface
- Standard color space definitions
This allows libraries to:
- Share a common vocabulary
- Interoperate naturally
- Build on a solid foundation
- Avoid reinventing basics
Guide-level explanation
For Library Users
When all libraries use the standard foundation, you have consistency:
require "image" # Standard foundation
# All libraries use the same color types
lib_a_color = Image::RGBA8.new(255, 0, 0, 255)
lib_b_color = Image::RGBA8.new(255, 0, 0, 255)
# Same type everywhere
lib_a_color == lib_b_color # true
For Library Authors
Build on the foundation instead of reinventing:
require "image"
module MyImageLib
class Canvas
@pixels : Slice(Image::RGBA8)
def [](x, y) : Image::RGBA8
@pixels[y * @width + x]
end
end
end
Reference-level explanation
Proposed Standard Library Addition
Add Image module to Crystal stdlib (or create minimal crystal-image shard):
Color Types
module Image
# 8-bit RGBA (premultiplied alpha)
struct RGBA8
getter r : UInt8
getter g : UInt8
getter b : UInt8
getter a : UInt8
def initialize(@r, @g, @b, @a = 255_u8)
end
# Premultiplied alpha semantics
# Valid range: 0 <= r,g,b <= a <= 255
end
# 8-bit RGBA (non-premultiplied alpha)
struct NRGBA8
getter r : UInt8
getter g : UInt8
getter b : UInt8
getter a : UInt8
def initialize(@r, @g, @b, @a = 255_u8)
end
end
# 16-bit RGBA (premultiplied alpha)
struct RGBA16
getter r : UInt16
getter g : UInt16
getter b : UInt16
getter a : UInt16
def initialize(@r, @g, @b, @a = 65535_u16)
end
end
# 16-bit RGBA (non-premultiplied alpha)
struct NRGBA16
getter r : UInt16
getter g : UInt16
getter b : UInt16
getter a : UInt16
def initialize(@r, @g, @b, @a = 65535_u16)
end
end
# 8-bit RGB (no alpha)
struct RGB8
getter r : UInt8
getter g : UInt8
getter b : UInt8
def initialize(@r, @g, @b)
end
end
# 8-bit grayscale
struct Gray8
getter y : UInt8
def initialize(@y)
end
end
# 16-bit grayscale
struct Gray16
getter y : UInt16
def initialize(@y)
end
end
end
Color Space
module Image
# Standard color spaces
enum ColorSpace
# sRGB (IEC 61966-2-1) - default for most images
SRGB
# Linear RGB - for compositing and blending
LinearRGB
# Adobe RGB (1998) - wider gamut
AdobeRGB
# Display P3 - modern wide gamut displays
DisplayP3
end
end
Pixel Format
module Image
# Describes pixel encoding in memory
enum PixelFormat
RGBA8 # 4 bytes: R, G, B, A (premultiplied)
NRGBA8 # 4 bytes: R, G, B, A (non-premultiplied)
RGB8 # 3 bytes: R, G, B
GRAY8 # 1 byte: Y
RGBA16 # 8 bytes: R, G, B, A (premultiplied, little-endian)
NRGBA16 # 8 bytes: R, G, B, A (non-premultiplied, little-endian)
RGB16 # 6 bytes: R, G, B (little-endian)
GRAY16 # 2 bytes: Y (little-endian)
def bytes_per_pixel : Int32
case self
in .rgba8?, .nrgba8? then 4
in .rgb8? then 3
in .gray8? then 1
in .rgba16?, .nrgba16? then 8
in .rgb16? then 6
in .gray16? then 2
end
end
def premultiplied? : Bool
case self
in .rgba8?, .rgba16? then true
else false
end
end
end
end
Rectangle
module Image
# Axis-aligned rectangle
struct Rectangle
getter min : Point
getter max : Point
def initialize(@min, @max)
end
def width : Int32
max.x - min.x
end
def height : Int32
max.y - min.y
end
end
struct Point
getter x : Int32
getter y : Int32
def initialize(@x, @y)
end
end
end
Buffer Interface (Minimal)
module Image
# Minimal interface for pixel buffers
# Libraries can implement this or provide their own richer APIs
module Buffer
abstract def width : Int32
abstract def height : Int32
abstract def pixel_format : PixelFormat
abstract def color_space : ColorSpace
# Optional: direct buffer access
# abstract def to_unsafe : Pointer(UInt8)
# abstract def stride : Int32
end
end
Design Principles
- Minimal: Only essential types, not operations
- Clear semantics: Explicit about premultiplication, bit depth, endianness
- Type-safe: Different types for different formats
- Extensible: Libraries can add their own types
What This Does NOT Include
- Image I/O (PNG, JPEG, etc.) - libraries handle this
- Image operations (resize, filter, etc.) - libraries handle this
- Format conversion - libraries handle this
- Drawing primitives - libraries handle this
This is foundation only - the building blocks.
Migration Path
Libraries can adopt incrementally:
Phase 1: Use standard color types internally
# In any library
alias RGBA = Image::RGBA16 # Use standard type
Phase 2: Expose standard types in API
def [](x, y) : Image::RGBA16
Phase 3: Implement standard interfaces
class Canvas
include Image::Buffer
end
Drawbacks
- Stdlib bloat: Adds types to standard library
- Breaking changes: Existing libraries must migrate
- Not comprehensive: Doesn’t cover all pixel formats
- Opinionated: Makes choices about representation
Rationale and alternatives
Why in stdlib (or minimal shard)?
Stdlib benefits:
- Always available, no dependencies
- Stable API, semantic versioning
- Community standard by default
- Like Go’s
image/colorpackage
Minimal shard alternative:
- Faster iteration
- Can be adopted before stdlib inclusion
- Easier to experiment
Why these specific types?
RGBA8/RGBA16: Most common formats
Premultiplied vs non-premultiplied: Both needed, different use cases
Explicit bit depth: Prevents confusion
Alternative 1: Do nothing
Let ecosystem remain fragmented.
Rejected: Problem will worsen as more libraries emerge
Alternative 2: Pick one library as standard
Make everyone use one specific library’s types.
Rejected: Creates monopoly, not neutral
Alternative 3: Comprehensive image library
Build full-featured stdlib image library.
Rejected: Too large for stdlib, limits innovation
Alternative 4: Just protocols/interfaces
Define interfaces without concrete types.
Rejected: Doesn’t solve the “every library has different RGBA” problem
Prior art
Go: image/color package
package color
type Color interface {
RGBA() (r, g, b, a uint32)
}
type RGBA struct {
R, G, B, A uint8
}
Success: All Go image libraries use these types
Rust: image crate
pub trait Pixel {
type Subpixel;
const CHANNEL_COUNT: u8;
}
pub struct Rgba<T>([T; 4]);
Success: De facto standard in Rust ecosystem
Python: PIL/Pillow
from PIL import Image
# Standard image modes
img = Image.new("RGB", (width, height))
img = Image.new("RGBA", (width, height))
img = Image.new("L", (width, height)) # Grayscale
Success: Standard modes used across Python image ecosystem
Java: java.awt.Color
public class Color {
public Color(int r, int g, int b, int a)
}
Success: Standard in Java ecosystem
Unresolved questions
Before RFC acceptance:
-
Stdlib or separate shard?
- Stdlib: More authoritative, always available
- Shard: Faster iteration, easier adoption
-
Which types to include?
- Current proposal: RGBA8, NRGBA8, RGBA16, NRGBA16, RGB8, Gray8, Gray16
- Missing: CMYK, YCbCr, HSV, LAB - add later?
-
Color space handling?
- Enum sufficient or need more metadata?
- ICC profile support?
-
Endianness?
- Assume little-endian or make explicit?
-
Buffer interface?
- Include in foundation or leave to libraries?
During implementation:
- How to encourage library adoption?
- Migration guide for existing libraries
- Performance implications
- Documentation and examples
Out of scope:
- Image operations (resize, filter, etc.)
- Format I/O (PNG, JPEG readers/writers)
- Color space conversion algorithms
- Drawing primitives
Future possibilities
Additional color types
struct CMYK8
struct YCbCr8
struct HSV
struct LAB
Color conversion
module Image
def self.convert(color : RGBA8, to : ColorSpace) : RGBA8
end
Buffer utilities
module Image
class SimpleBuffer
include Buffer
# Reference implementation
end
end
Format registry
module Image
def self.register_format(name : String, reader : FormatReader)
end
But these are future enhancements - start with minimal foundation.