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::DecodeError
  • Texel::EncodeError
  • Texel::FormatError
  • Texel::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.