texel 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: dc746766a443584c97dcf37b69f42f2a77e04337488d1dccd794be408d59da52
4
+ data.tar.gz: 26d50b5b34ae127a95f62b46c5f1c029d7fdd876013cb62aa12cf6efb9a3328e
5
+ SHA512:
6
+ metadata.gz: '04686cc60e1595a187297ac36e487e571d300aa363090b1d32de7bd30ada0767cf697602f248b1acd22da8de21269701dfa5a393814b09b2ffab1aac5e2462bf'
7
+ data.tar.gz: a259720f0efc3eaa8d0928584ce4a0bd84ecec52b6a7d6f3a1d539b5a4d77fb0fad6512d238282bee033e531fc4da72ce8be7ad7ca57eed2d165cdd88400cf98
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Yudai Takada
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,219 @@
1
+ # Texel
2
+
3
+ Texel is a texture-oriented image I/O library for Ruby. It combines a small C
4
+ extension built from vendored stb headers with a pure Ruby QOI codec, an
5
+ immutable pixel container, color and channel conversion, mipmap generation,
6
+ and WebGPU/OpenGL upload helpers.
7
+
8
+ Texel intentionally does not provide drawing, compositing, or image filters.
9
+ Its job is the path from encoded image bytes to tightly packed texture data.
10
+
11
+ ## Requirements
12
+
13
+ - Ruby 3.2 or newer
14
+ - A C99 compiler when installing the source gem
15
+ - No runtime gem or system image-library dependencies
16
+
17
+ The stb headers are included in the gem. QOI remains usable when a compiler is
18
+ unavailable by setting `TEXEL_DISABLE_NATIVE=1` while installing:
19
+
20
+ ```sh
21
+ TEXEL_DISABLE_NATIVE=1 gem install texel
22
+ ruby your_qoi_program.rb
23
+ ```
24
+
25
+ The environment variable is only required during installation. At runtime,
26
+ Texel detects that the extension is absent and keeps the pure Ruby QOI codec
27
+ available automatically.
28
+
29
+ This source-gem fallback still requires the platform's `make` command because
30
+ RubyGems invokes it for every declared extension. Precompiled platform gems do
31
+ not require a compiler or `make`.
32
+
33
+ ## Installation
34
+
35
+ Add Texel to your bundle:
36
+
37
+ ```ruby
38
+ gem "texel", "~> 0.1"
39
+ ```
40
+
41
+ Then run `bundle install`, or install it directly:
42
+
43
+ ```sh
44
+ gem install texel
45
+ ```
46
+
47
+ Release artifacts include precompiled gems for x86-64 and ARM64 Linux,
48
+ Intel and Apple Silicon macOS, and 64-bit UCRT Windows. Each platform gem
49
+ contains extensions for Ruby 3.2, 3.3, 3.4, and 4.0, selected automatically at
50
+ load time. RubyGems falls back to the source gem on other platforms.
51
+
52
+ ## Loading and saving
53
+
54
+ Texel accepts a path, a readable IO, or an encoded binary String. The format is
55
+ detected from magic bytes, with the file extension used for TGA as a fallback.
56
+
57
+ ```ruby
58
+ require "texel"
59
+
60
+ image = Texel.load("albedo.png")
61
+ rgba = Texel.load("albedo.jpg", channels: 4)
62
+ floats = Texel.load(File.binread("albedo.png"), dtype: :f32)
63
+ normal = Texel.load("normal.png", color_space: :linear)
64
+
65
+ metadata = Texel.info("albedo.png")
66
+ # => #<data Texel::Info width=... height=... channels=... format=:png>
67
+
68
+ Texel.save(rgba, "output.png")
69
+ Texel.save(rgba, "output.jpg", quality: 90)
70
+ encoded = Texel.encode(rgba, :qoi)
71
+ ```
72
+
73
+ `Texel.load_all(paths, threads: 4)` decodes a collection concurrently while
74
+ preserving input order. Native decode and resize operations of at least one
75
+ megapixel release the GVL.
76
+
77
+ ### Formats
78
+
79
+ | Format | Decode | Encode | Notes |
80
+ |---|---:|---:|---|
81
+ | PNG | yes | yes | u8 and u16 decode |
82
+ | JPEG | yes | yes | `quality:` controls encoding |
83
+ | BMP | yes | yes | lossless |
84
+ | TGA | yes | yes | lossless |
85
+ | GIF | yes | no | first frame only |
86
+ | PSD | yes | no | composite image |
87
+ | HDR/RGBE | yes | yes | defaults to f32/linear |
88
+ | QOI | yes | yes | pure Ruby |
89
+
90
+ KTX2, EXR, and WebP signatures are recognized. Until their extension gems are
91
+ loaded, Texel raises `Texel::MissingCodecError` with the required gem name.
92
+
93
+ ## Image data
94
+
95
+ `Texel::Image` stores frozen, tightly packed, top-left-origin pixel data:
96
+
97
+ ```ruby
98
+ image.width # Integer
99
+ image.height # Integer
100
+ image.channels # 1..4
101
+ image.dtype # :u8, :u16, or :f32
102
+ image.color_space # :srgb, :linear, or :unknown
103
+ image.data # frozen ASCII-8BIT String
104
+ image.row_bytes
105
+ image.pixel(0, 0) # debug-oriented Array access
106
+ ```
107
+
108
+ Frozen binary input is adopted directly; mutable input is copied once to keep
109
+ the container immutable. Native resize and f32-to-f16 conversion write into
110
+ Ruby-owned buffers directly. stb decode and encode require one ownership copy
111
+ because stb allocates their result buffers itself.
112
+
113
+ Construct images directly with packed data when needed:
114
+
115
+ ```ruby
116
+ image = Texel::Image.new(
117
+ width: 1,
118
+ height: 1,
119
+ channels: 4,
120
+ dtype: :f32,
121
+ color_space: :linear,
122
+ data: [1.0, 0.5, 0.25, 1.0].pack("e*")
123
+ )
124
+ ```
125
+
126
+ The constructor checks dimensions, tags, and the exact data size. Transforming
127
+ methods return new images:
128
+
129
+ ```ruby
130
+ image.flip_y
131
+ image.convert(channels: 4, dtype: :f32)
132
+ image.resize(512, 512, filter: :mitchell)
133
+ image.resize_to_fit(1024)
134
+ image.to_linear
135
+ image.to_srgb
136
+ image.premultiply
137
+ image.unpremultiply
138
+ image.mipmaps
139
+ ```
140
+
141
+ Resize filters are `:box`, `:triangle`, `:cubic`, `:mitchell`, and `:lanczos`.
142
+ sRGB mipmaps are resized in linear space by default; use `mipmaps(fast: true)`
143
+ to resize encoded sRGB values directly.
144
+
145
+ ## WebGPU upload
146
+
147
+ The optional helper targets the keyword API used by `webgpu-ruby` and handles
148
+ format inference, mip generation, f32-to-f16 conversion, and 256-byte row
149
+ alignment:
150
+
151
+ ```ruby
152
+ require "texel/wgpu"
153
+
154
+ texture = Texel::WGPU.upload(
155
+ image,
156
+ device: device,
157
+ queue: queue,
158
+ label: "albedo"
159
+ )
160
+ ```
161
+
162
+ Supported inferred layouts are u8 R/RG/RGBA and f32 R/RG/RGBA. WebGPU has no
163
+ three-channel texture format, so RGB input raises `Texel::FormatError`; convert
164
+ it explicitly with `image.convert(channels: 4)`. `Texel::WGPU.pad_rows` is also
165
+ available when only transfer-buffer preparation is needed.
166
+
167
+ ## OpenGL upload metadata
168
+
169
+ `texel/gl` returns the internal format, external format, and component type
170
+ without loading an OpenGL binding:
171
+
172
+ ```ruby
173
+ require "texel/gl"
174
+
175
+ Texel::GL.format(image)
176
+ # => #<data Texel::GL::Format
177
+ # internal_format=:srgb8_alpha8 format=:rgba type=:unsigned_byte>
178
+ ```
179
+
180
+ ## Errors
181
+
182
+ All library errors inherit from `Texel::Error`:
183
+
184
+ - `Texel::DecodeError`
185
+ - `Texel::EncodeError`
186
+ - `Texel::FormatError`
187
+ - `Texel::MissingCodecError`
188
+
189
+ ## Development
190
+
191
+ Install development dependencies and run the complete suite:
192
+
193
+ ```sh
194
+ bundle install
195
+ bundle exec rake spec
196
+ ```
197
+
198
+ `rake spec` builds the native extension first. The pure Ruby fallback can be
199
+ checked independently:
200
+
201
+ ```sh
202
+ TEXEL_DISABLE_NATIVE=1 bundle exec rspec
203
+ ```
204
+
205
+ The committed minimal image corpus can be regenerated when ImageMagick is
206
+ available:
207
+
208
+ ```sh
209
+ bundle exec rake fixtures
210
+ ```
211
+
212
+ The `Native gems` GitHub Actions workflow compiles and tests all supported Ruby
213
+ and platform combinations. Tag builds collect the five platform gems and the
214
+ source gem into a single workflow artifact for release.
215
+
216
+ ## License
217
+
218
+ Texel is available under the MIT License. The vendored stb headers retain their
219
+ upstream Public Domain/MIT dual-license notices.
data/Rakefile ADDED
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rake/clean"
5
+ require "rspec/core/rake_task"
6
+ require "rbconfig"
7
+
8
+ EXTENSION_DIRECTORY = File.expand_path("ext/texel", __dir__)
9
+ EXTENSION_FILE = File.join(EXTENSION_DIRECTORY, "texel_native.#{RbConfig::CONFIG.fetch("DLEXT")}")
10
+
11
+ task :compile do
12
+ next if ENV["TEXEL_DISABLE_NATIVE"] == "1"
13
+
14
+ Dir.chdir(EXTENSION_DIRECTORY) do
15
+ sh RbConfig.ruby, "extconf.rb"
16
+ sh ENV.fetch("MAKE", "make")
17
+ end
18
+ end
19
+
20
+ RSpec::Core::RakeTask.new(:spec)
21
+ task spec: :compile
22
+
23
+ desc "Regenerate the minimal image codec fixtures"
24
+ task fixtures: :compile do
25
+ sh RbConfig.ruby, File.expand_path("spec/fixtures/generate.rb", __dir__)
26
+ end
27
+
28
+ CLEAN.include(
29
+ File.join(EXTENSION_DIRECTORY, "Makefile"),
30
+ File.join(EXTENSION_DIRECTORY, "*.o"),
31
+ EXTENSION_FILE,
32
+ File.join(EXTENSION_DIRECTORY, "mkmf.log")
33
+ )
34
+
35
+ task default: :spec
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ if ENV["TEXEL_DISABLE_NATIVE"] == "1"
4
+ File.write(
5
+ "Makefile",
6
+ <<~MAKEFILE
7
+ .PHONY: all install clean
8
+ all:
9
+ install:
10
+ clean:
11
+ MAKEFILE
12
+ )
13
+ exit
14
+ end
15
+
16
+ require "mkmf"
17
+
18
+ $CFLAGS = "#{$CFLAGS} -std=c99 -Wall -Wextra"
19
+ $CFLAGS = "#{$CFLAGS} -Wno-unused-function -Wno-sign-compare"
20
+
21
+ abort "texel requires a C99 compiler" unless try_compile(<<~C)
22
+ #include <stdint.h>
23
+ int main(void) { uint32_t value = 0; return (int)value; }
24
+ C
25
+
26
+ have_library("m", "sinf")
27
+
28
+ create_makefile("texel/texel_native")
data/ext/texel/shim.c ADDED
@@ -0,0 +1,244 @@
1
+ #include "shim.h"
2
+
3
+ #include <math.h>
4
+ #include <string.h>
5
+
6
+ #define STBI_FAILURE_USERMSG
7
+ #define STB_IMAGE_IMPLEMENTATION
8
+ #define STB_IMAGE_WRITE_IMPLEMENTATION
9
+ #define STB_IMAGE_RESIZE_IMPLEMENTATION
10
+ #include "vendor/stb_image.h"
11
+ #include "vendor/stb_image_write.h"
12
+ #include "vendor/stb_image_resize2.h"
13
+
14
+ unsigned char *
15
+ tx_load_u8(const void *buffer, int length, int *width, int *height,
16
+ int *source_channels, int requested_channels, const char **error)
17
+ {
18
+ unsigned char *pixels = stbi_load_from_memory(
19
+ (const stbi_uc *)buffer, length, width, height, source_channels, requested_channels);
20
+ if (pixels == NULL && error != NULL) {
21
+ *error = stbi_failure_reason();
22
+ }
23
+ return pixels;
24
+ }
25
+
26
+ unsigned short *
27
+ tx_load_u16(const void *buffer, int length, int *width, int *height,
28
+ int *source_channels, int requested_channels, const char **error)
29
+ {
30
+ unsigned short *pixels = stbi_load_16_from_memory(
31
+ (const stbi_uc *)buffer, length, width, height, source_channels, requested_channels);
32
+ if (pixels == NULL && error != NULL) {
33
+ *error = stbi_failure_reason();
34
+ }
35
+ return pixels;
36
+ }
37
+
38
+ float *
39
+ tx_load_f32(const void *buffer, int length, int *width, int *height,
40
+ int *source_channels, int requested_channels, const char **error)
41
+ {
42
+ float *pixels = stbi_loadf_from_memory(
43
+ (const stbi_uc *)buffer, length, width, height, source_channels, requested_channels);
44
+ if (pixels == NULL && error != NULL) {
45
+ *error = stbi_failure_reason();
46
+ }
47
+ return pixels;
48
+ }
49
+
50
+ int
51
+ tx_info(const void *buffer, int length, int *width, int *height, int *channels)
52
+ {
53
+ return stbi_info_from_memory((const stbi_uc *)buffer, length, width, height, channels);
54
+ }
55
+
56
+ int
57
+ tx_is_16_bit(const void *buffer, int length)
58
+ {
59
+ return stbi_is_16_bit_from_memory((const stbi_uc *)buffer, length);
60
+ }
61
+
62
+ int
63
+ tx_is_hdr(const void *buffer, int length)
64
+ {
65
+ return stbi_is_hdr_from_memory((const stbi_uc *)buffer, length);
66
+ }
67
+
68
+ void
69
+ tx_free(void *pixels)
70
+ {
71
+ stbi_image_free(pixels);
72
+ }
73
+
74
+ int
75
+ tx_write(int format, tx_write_cb callback, void *user_data, int width, int height,
76
+ int channels, const void *pixels, int quality_or_stride)
77
+ {
78
+ switch (format) {
79
+ case 0:
80
+ return stbi_write_png_to_func(callback, user_data, width, height, channels,
81
+ pixels, quality_or_stride);
82
+ case 1:
83
+ return stbi_write_jpg_to_func(callback, user_data, width, height, channels,
84
+ pixels, quality_or_stride);
85
+ case 2:
86
+ return stbi_write_bmp_to_func(callback, user_data, width, height, channels, pixels);
87
+ case 3:
88
+ return stbi_write_tga_to_func(callback, user_data, width, height, channels, pixels);
89
+ case 4:
90
+ return stbi_write_hdr_to_func(callback, user_data, width, height, channels,
91
+ (const float *)pixels);
92
+ default:
93
+ return 0;
94
+ }
95
+ }
96
+
97
+ static stbir_pixel_layout
98
+ tx_pixel_layout(int channels, int premultiplied)
99
+ {
100
+ switch (channels) {
101
+ case 1:
102
+ return STBIR_1CHANNEL;
103
+ case 2:
104
+ return premultiplied ? STBIR_RA_PM : STBIR_RA;
105
+ case 3:
106
+ return STBIR_RGB;
107
+ case 4:
108
+ return premultiplied ? STBIR_RGBA_PM : STBIR_RGBA;
109
+ default:
110
+ return STBIR_1CHANNEL;
111
+ }
112
+ }
113
+
114
+ static float
115
+ tx_lanczos_kernel(float x, float scale, void *user_data)
116
+ {
117
+ const float pi = 3.14159265358979323846f;
118
+ (void)scale;
119
+ (void)user_data;
120
+ if (x < 0.0f) {
121
+ x = -x;
122
+ }
123
+ if (x < 0.000001f) {
124
+ return 1.0f;
125
+ }
126
+ if (x >= 3.0f) {
127
+ return 0.0f;
128
+ }
129
+ return (sinf(pi * x) / (pi * x)) * (sinf((pi * x) / 3.0f) / ((pi * x) / 3.0f));
130
+ }
131
+
132
+ static float
133
+ tx_lanczos_support(float scale, void *user_data)
134
+ {
135
+ (void)scale;
136
+ (void)user_data;
137
+ return 3.0f;
138
+ }
139
+
140
+ int
141
+ tx_resize(int dtype, int srgb, const void *source, int source_width, int source_height,
142
+ void *destination, int width, int height, int channels, int filter, int edge,
143
+ int premultiplied)
144
+ {
145
+ stbir_datatype data_type;
146
+ if (dtype == 0) {
147
+ data_type = srgb ? STBIR_TYPE_UINT8_SRGB : STBIR_TYPE_UINT8;
148
+ } else if (dtype == 1) {
149
+ data_type = STBIR_TYPE_UINT16;
150
+ } else if (dtype == 2) {
151
+ data_type = STBIR_TYPE_FLOAT;
152
+ } else {
153
+ return 0;
154
+ }
155
+
156
+ if (filter == STBIR_FILTER_OTHER) {
157
+ STBIR_RESIZE resize;
158
+ stbir_resize_init(&resize, source, source_width, source_height, 0,
159
+ destination, width, height, 0,
160
+ tx_pixel_layout(channels, premultiplied), data_type);
161
+ if (!stbir_set_edgemodes(&resize, (stbir_edge)edge, (stbir_edge)edge) ||
162
+ !stbir_set_filter_callbacks(&resize,
163
+ tx_lanczos_kernel, tx_lanczos_support,
164
+ tx_lanczos_kernel, tx_lanczos_support)) {
165
+ return 0;
166
+ }
167
+ return stbir_resize_extended(&resize);
168
+ }
169
+
170
+ return stbir_resize(source, source_width, source_height, 0,
171
+ destination, width, height, 0,
172
+ tx_pixel_layout(channels, premultiplied), data_type,
173
+ (stbir_edge)edge, (stbir_filter)filter) != NULL;
174
+ }
175
+
176
+ static uint16_t
177
+ tx_float_to_half(float value)
178
+ {
179
+ union {
180
+ float floating;
181
+ uint32_t bits;
182
+ } input;
183
+ uint32_t sign;
184
+ uint32_t exponent;
185
+ uint32_t mantissa;
186
+ int half_exponent;
187
+ uint32_t half_mantissa;
188
+ uint32_t remainder;
189
+ int shift;
190
+
191
+ input.floating = value;
192
+ sign = (input.bits >> 16) & 0x8000u;
193
+ exponent = (input.bits >> 23) & 0xffu;
194
+ mantissa = input.bits & 0x7fffffu;
195
+
196
+ if (exponent == 0xffu) {
197
+ return (uint16_t)(sign | (mantissa == 0 ? 0x7c00u : 0x7e00u));
198
+ }
199
+
200
+ half_exponent = (int)exponent - 127 + 15;
201
+ if (half_exponent >= 31) {
202
+ return (uint16_t)(sign | 0x7c00u);
203
+ }
204
+ if (half_exponent <= 0) {
205
+ if (half_exponent < -10) {
206
+ return (uint16_t)sign;
207
+ }
208
+ mantissa |= 0x800000u;
209
+ shift = 14 - half_exponent;
210
+ half_mantissa = mantissa >> shift;
211
+ remainder = mantissa & ((1u << shift) - 1u);
212
+ if (remainder > (1u << (shift - 1)) ||
213
+ (remainder == (1u << (shift - 1)) && (half_mantissa & 1u))) {
214
+ half_mantissa += 1u;
215
+ }
216
+ return (uint16_t)(sign | half_mantissa);
217
+ }
218
+
219
+ half_mantissa = mantissa >> 13;
220
+ remainder = mantissa & 0x1fffu;
221
+ if (remainder > 0x1000u || (remainder == 0x1000u && (half_mantissa & 1u))) {
222
+ half_mantissa += 1u;
223
+ if (half_mantissa == 0x400u) {
224
+ half_mantissa = 0;
225
+ half_exponent += 1;
226
+ if (half_exponent >= 31) {
227
+ return (uint16_t)(sign | 0x7c00u);
228
+ }
229
+ }
230
+ }
231
+ return (uint16_t)(sign | ((uint32_t)half_exponent << 10) | half_mantissa);
232
+ }
233
+
234
+ void
235
+ tx_f32_to_f16(const void *source, uint16_t *destination, size_t count)
236
+ {
237
+ const unsigned char *bytes = (const unsigned char *)source;
238
+ size_t index;
239
+ for (index = 0; index < count; index += 1) {
240
+ float value;
241
+ memcpy(&value, bytes + (index * sizeof(float)), sizeof(float));
242
+ destination[index] = tx_float_to_half(value);
243
+ }
244
+ }
data/ext/texel/shim.h ADDED
@@ -0,0 +1,35 @@
1
+ #ifndef TEXEL_SHIM_H
2
+ #define TEXEL_SHIM_H
3
+
4
+ #include <stddef.h>
5
+ #include <stdint.h>
6
+
7
+ #ifdef __cplusplus
8
+ extern "C" {
9
+ #endif
10
+
11
+ typedef void tx_write_cb(void *user_data, void *data, int size);
12
+
13
+ unsigned char *tx_load_u8(const void *buffer, int length, int *width, int *height,
14
+ int *source_channels, int requested_channels, const char **error);
15
+ unsigned short *tx_load_u16(const void *buffer, int length, int *width, int *height,
16
+ int *source_channels, int requested_channels, const char **error);
17
+ float *tx_load_f32(const void *buffer, int length, int *width, int *height,
18
+ int *source_channels, int requested_channels, const char **error);
19
+ int tx_info(const void *buffer, int length, int *width, int *height, int *channels);
20
+ int tx_is_16_bit(const void *buffer, int length);
21
+ int tx_is_hdr(const void *buffer, int length);
22
+ void tx_free(void *pixels);
23
+
24
+ int tx_write(int format, tx_write_cb callback, void *user_data, int width, int height,
25
+ int channels, const void *pixels, int quality_or_stride);
26
+ int tx_resize(int dtype, int srgb, const void *source, int source_width, int source_height,
27
+ void *destination, int width, int height, int channels, int filter, int edge,
28
+ int premultiplied);
29
+ void tx_f32_to_f16(const void *source, uint16_t *destination, size_t count);
30
+
31
+ #ifdef __cplusplus
32
+ }
33
+ #endif
34
+
35
+ #endif