Texel
Texel is a texture-oriented image I/O library for Ruby. It combines a small C extension built from vendored stb headers with a pure Ruby QOI codec, an immutable pixel container, color and channel conversion, mipmap generation, and WebGPU/OpenGL upload helpers.
Texel intentionally does not provide drawing, compositing, or image filters. Its job is the path from encoded image bytes to tightly packed texture data.
Requirements
- Ruby 3.2 or newer
- A C99 compiler when installing the source gem
- No runtime gem or system image-library dependencies
The stb headers are included in the gem. QOI remains usable when a compiler is
unavailable by setting TEXEL_DISABLE_NATIVE=1 while installing:
TEXEL_DISABLE_NATIVE=1 gem install texel
ruby your_qoi_program.rb
The environment variable is only required during installation. At runtime, Texel detects that the extension is absent and keeps the pure Ruby QOI codec available automatically.
This source-gem fallback still requires the platform's make command because
RubyGems invokes it for every declared extension. Precompiled platform gems do
not require a compiler or make.
Installation
Add Texel to your bundle:
gem "texel", "~> 0.1"
Then run bundle install, or install it directly:
gem install texel
Release artifacts include precompiled gems for x86-64 and ARM64 Linux, Intel and Apple Silicon macOS, and 64-bit UCRT Windows. Each platform gem contains extensions for Ruby 3.2, 3.3, 3.4, and 4.0, selected automatically at load time. RubyGems falls back to the source gem on other platforms.
Loading and saving
Texel accepts a path, a readable IO, or an encoded binary String. The format is detected from magic bytes, with the file extension used for TGA as a fallback.
require "texel"
image = Texel.load("albedo.png")
rgba = Texel.load("albedo.jpg", channels: 4)
floats = Texel.load(File.binread("albedo.png"), dtype: :f32)
normal = Texel.load("normal.png", color_space: :linear)
= Texel.info("albedo.png")
# => #<data Texel::Info width=... height=... channels=... format=:png>
Texel.save(rgba, "output.png")
Texel.save(rgba, "output.jpg", quality: 90)
encoded = Texel.encode(rgba, :qoi)
Texel.load_all(paths, threads: 4) decodes a collection concurrently while
preserving input order. Native decode and resize operations of at least one
megapixel release the GVL.
Formats
| Format | Decode | Encode | Notes |
|---|---|---|---|
| PNG | yes | yes | u8 and u16 decode |
| JPEG | yes | yes | quality: controls encoding |
| BMP | yes | yes | lossless |
| TGA | yes | yes | lossless |
| GIF | yes | no | first frame only |
| PSD | yes | no | composite image |
| HDR/RGBE | yes | yes | defaults to f32/linear |
| QOI | yes | yes | pure Ruby |
KTX2, EXR, and WebP signatures are recognized. Until their extension gems are
loaded, Texel raises Texel::MissingCodecError with the required gem name.
Image data
Texel::Image stores frozen, tightly packed, top-left-origin pixel data:
image.width # Integer
image.height # Integer
image.channels # 1..4
image.dtype # :u8, :u16, or :f32
image.color_space # :srgb, :linear, or :unknown
image.data # frozen ASCII-8BIT String
image.row_bytes
image.pixel(0, 0) # debug-oriented Array access
Frozen binary input is adopted directly; mutable input is copied once to keep the container immutable. Native resize and f32-to-f16 conversion write into Ruby-owned buffers directly. stb decode and encode require one ownership copy because stb allocates their result buffers itself.
Construct images directly with packed data when needed:
image = Texel::Image.new(
width: 1,
height: 1,
channels: 4,
dtype: :f32,
color_space: :linear,
data: [1.0, 0.5, 0.25, 1.0].pack("e*")
)
The constructor checks dimensions, tags, and the exact data size. Transforming methods return new images:
image.flip_y
image.convert(channels: 4, dtype: :f32)
image.resize(512, 512, filter: :mitchell)
image.resize_to_fit(1024)
image.to_linear
image.to_srgb
image.premultiply
image.unpremultiply
image.mipmaps
Resize filters are :box, :triangle, :cubic, :mitchell, and :lanczos.
sRGB mipmaps are resized in linear space by default; use mipmaps(fast: true)
to resize encoded sRGB values directly.
WebGPU upload
The optional helper targets the keyword API used by webgpu-ruby and handles
format inference, mip generation, f32-to-f16 conversion, and 256-byte row
alignment:
require "texel/wgpu"
texture = Texel::WGPU.upload(
image,
device: device,
queue: queue,
label: "albedo"
)
Supported inferred layouts are u8 R/RG/RGBA and f32 R/RG/RGBA. WebGPU has no
three-channel texture format, so RGB input raises Texel::FormatError; convert
it explicitly with image.convert(channels: 4). Texel::WGPU.pad_rows is also
available when only transfer-buffer preparation is needed.
OpenGL upload metadata
texel/gl returns the internal format, external format, and component type
without loading an OpenGL binding:
require "texel/gl"
Texel::GL.format(image)
# => #<data Texel::GL::Format
# internal_format=:srgb8_alpha8 format=:rgba type=:unsigned_byte>
Errors
All library errors inherit from Texel::Error:
Texel::DecodeErrorTexel::EncodeErrorTexel::FormatErrorTexel::MissingCodecError
Development
Install development dependencies and run the complete suite:
bundle install
bundle exec rake spec
rake spec builds the native extension first. The pure Ruby fallback can be
checked independently:
TEXEL_DISABLE_NATIVE=1 bundle exec rspec
The committed minimal image corpus can be regenerated when ImageMagick is available:
bundle exec rake fixtures
The Native gems GitHub Actions workflow compiles and tests all supported Ruby
and platform combinations. Tag builds collect the five platform gems and the
source gem into a single workflow artifact for release.
License
Texel is available under the MIT License. The vendored stb headers retain their upstream Public Domain/MIT dual-license notices.