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 +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +219 -0
- data/Rakefile +35 -0
- data/ext/texel/extconf.rb +28 -0
- data/ext/texel/shim.c +244 -0
- data/ext/texel/shim.h +35 -0
- data/ext/texel/texel_native.c +415 -0
- data/ext/texel/vendor/README.md +11 -0
- data/ext/texel/vendor/stb_image.h +7988 -0
- data/ext/texel/vendor/stb_image_resize2.h +10679 -0
- data/ext/texel/vendor/stb_image_write.h +1724 -0
- data/lib/texel/codec.rb +27 -0
- data/lib/texel/codecs/qoi.rb +228 -0
- data/lib/texel/codecs/stb.rb +104 -0
- data/lib/texel/color_transform.rb +63 -0
- data/lib/texel/errors.rb +9 -0
- data/lib/texel/gl.rb +36 -0
- data/lib/texel/half_float.rb +57 -0
- data/lib/texel/image.rb +106 -0
- data/lib/texel/image_transformations.rb +115 -0
- data/lib/texel/info.rb +10 -0
- data/lib/texel/native.rb +91 -0
- data/lib/texel/pixel_data.rb +79 -0
- data/lib/texel/registry.rb +66 -0
- data/lib/texel/resampler.rb +79 -0
- data/lib/texel/source_reader.rb +52 -0
- data/lib/texel/version.rb +5 -0
- data/lib/texel/wgpu.rb +138 -0
- data/lib/texel.rb +164 -0
- metadata +75 -0
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
#include <ruby.h>
|
|
2
|
+
#include <ruby/encoding.h>
|
|
3
|
+
#include <ruby/thread.h>
|
|
4
|
+
|
|
5
|
+
#include <limits.h>
|
|
6
|
+
#include <stdint.h>
|
|
7
|
+
#include <stdlib.h>
|
|
8
|
+
#include <string.h>
|
|
9
|
+
|
|
10
|
+
#include "shim.h"
|
|
11
|
+
|
|
12
|
+
#define TX_GVL_PIXEL_THRESHOLD 1000000
|
|
13
|
+
/* Larger than the embedded String capacity of every supported Ruby. */
|
|
14
|
+
#define TX_GVL_OUTPUT_THRESHOLD 128
|
|
15
|
+
|
|
16
|
+
typedef struct {
|
|
17
|
+
const void *input;
|
|
18
|
+
int input_length;
|
|
19
|
+
int requested_channels;
|
|
20
|
+
int dtype;
|
|
21
|
+
int width;
|
|
22
|
+
int height;
|
|
23
|
+
int source_channels;
|
|
24
|
+
void *pixels;
|
|
25
|
+
const char *error;
|
|
26
|
+
} tx_decode_args;
|
|
27
|
+
|
|
28
|
+
typedef struct {
|
|
29
|
+
unsigned char *data;
|
|
30
|
+
size_t length;
|
|
31
|
+
size_t capacity;
|
|
32
|
+
int failed;
|
|
33
|
+
} tx_buffer;
|
|
34
|
+
|
|
35
|
+
typedef struct {
|
|
36
|
+
int format;
|
|
37
|
+
tx_buffer *buffer;
|
|
38
|
+
int width;
|
|
39
|
+
int height;
|
|
40
|
+
int channels;
|
|
41
|
+
const void *pixels;
|
|
42
|
+
int quality_or_stride;
|
|
43
|
+
int result;
|
|
44
|
+
} tx_encode_args;
|
|
45
|
+
|
|
46
|
+
typedef struct {
|
|
47
|
+
int dtype;
|
|
48
|
+
int srgb;
|
|
49
|
+
const void *source;
|
|
50
|
+
int source_width;
|
|
51
|
+
int source_height;
|
|
52
|
+
void *destination;
|
|
53
|
+
int width;
|
|
54
|
+
int height;
|
|
55
|
+
int channels;
|
|
56
|
+
int filter;
|
|
57
|
+
int edge;
|
|
58
|
+
int premultiplied;
|
|
59
|
+
int result;
|
|
60
|
+
} tx_resize_args;
|
|
61
|
+
|
|
62
|
+
typedef struct {
|
|
63
|
+
const void *source;
|
|
64
|
+
uint16_t *destination;
|
|
65
|
+
size_t count;
|
|
66
|
+
} tx_f16_args;
|
|
67
|
+
|
|
68
|
+
static size_t
|
|
69
|
+
tx_checked_image_size(int width, int height, int channels, size_t component_size)
|
|
70
|
+
{
|
|
71
|
+
size_t value;
|
|
72
|
+
if (width <= 0 || height <= 0 || channels <= 0 || channels > 4) {
|
|
73
|
+
rb_raise(rb_eArgError, "invalid image dimensions or channel count");
|
|
74
|
+
}
|
|
75
|
+
value = (size_t)width;
|
|
76
|
+
if (value > SIZE_MAX / (size_t)height) {
|
|
77
|
+
rb_raise(rb_eArgError, "image dimensions are too large");
|
|
78
|
+
}
|
|
79
|
+
value *= (size_t)height;
|
|
80
|
+
if (value > SIZE_MAX / (size_t)channels) {
|
|
81
|
+
rb_raise(rb_eArgError, "image dimensions are too large");
|
|
82
|
+
}
|
|
83
|
+
value *= (size_t)channels;
|
|
84
|
+
if (value > SIZE_MAX / component_size || value * component_size > LONG_MAX) {
|
|
85
|
+
rb_raise(rb_eArgError, "image data is too large");
|
|
86
|
+
}
|
|
87
|
+
return value * component_size;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
static size_t
|
|
91
|
+
tx_component_size(int dtype)
|
|
92
|
+
{
|
|
93
|
+
switch (dtype) {
|
|
94
|
+
case 0:
|
|
95
|
+
return 1;
|
|
96
|
+
case 1:
|
|
97
|
+
return 2;
|
|
98
|
+
case 2:
|
|
99
|
+
return 4;
|
|
100
|
+
default:
|
|
101
|
+
rb_raise(rb_eArgError, "invalid pixel dtype");
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
static void *
|
|
106
|
+
tx_decode_without_gvl(void *pointer)
|
|
107
|
+
{
|
|
108
|
+
tx_decode_args *args = (tx_decode_args *)pointer;
|
|
109
|
+
switch (args->dtype) {
|
|
110
|
+
case 0:
|
|
111
|
+
args->pixels = tx_load_u8(args->input, args->input_length, &args->width, &args->height,
|
|
112
|
+
&args->source_channels, args->requested_channels, &args->error);
|
|
113
|
+
break;
|
|
114
|
+
case 1:
|
|
115
|
+
args->pixels = tx_load_u16(args->input, args->input_length, &args->width, &args->height,
|
|
116
|
+
&args->source_channels, args->requested_channels, &args->error);
|
|
117
|
+
break;
|
|
118
|
+
case 2:
|
|
119
|
+
args->pixels = tx_load_f32(args->input, args->input_length, &args->width, &args->height,
|
|
120
|
+
&args->source_channels, args->requested_channels, &args->error);
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
return NULL;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
static VALUE
|
|
127
|
+
tx_rb_metadata(VALUE self, VALUE input)
|
|
128
|
+
{
|
|
129
|
+
int width = 0;
|
|
130
|
+
int height = 0;
|
|
131
|
+
int channels = 0;
|
|
132
|
+
long length;
|
|
133
|
+
VALUE result;
|
|
134
|
+
(void)self;
|
|
135
|
+
|
|
136
|
+
Check_Type(input, T_STRING);
|
|
137
|
+
length = RSTRING_LEN(input);
|
|
138
|
+
if (length > INT_MAX) {
|
|
139
|
+
rb_raise(rb_eArgError, "encoded image is too large");
|
|
140
|
+
}
|
|
141
|
+
if (!tx_info(RSTRING_PTR(input), (int)length, &width, &height, &channels)) {
|
|
142
|
+
rb_raise(rb_eRuntimeError, "unsupported or corrupt image header");
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
result = rb_ary_new_capa(5);
|
|
146
|
+
rb_ary_push(result, INT2NUM(width));
|
|
147
|
+
rb_ary_push(result, INT2NUM(height));
|
|
148
|
+
rb_ary_push(result, INT2NUM(channels));
|
|
149
|
+
rb_ary_push(result, tx_is_16_bit(RSTRING_PTR(input), (int)length) ? Qtrue : Qfalse);
|
|
150
|
+
rb_ary_push(result, tx_is_hdr(RSTRING_PTR(input), (int)length) ? Qtrue : Qfalse);
|
|
151
|
+
return result;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
static VALUE
|
|
155
|
+
tx_rb_decode(VALUE self, VALUE input, VALUE requested_channels_value, VALUE dtype_value)
|
|
156
|
+
{
|
|
157
|
+
tx_decode_args args;
|
|
158
|
+
int info_width = 0;
|
|
159
|
+
int info_height = 0;
|
|
160
|
+
int info_channels = 0;
|
|
161
|
+
int output_channels;
|
|
162
|
+
long input_length;
|
|
163
|
+
size_t output_length;
|
|
164
|
+
VALUE output;
|
|
165
|
+
(void)self;
|
|
166
|
+
|
|
167
|
+
Check_Type(input, T_STRING);
|
|
168
|
+
input_length = RSTRING_LEN(input);
|
|
169
|
+
if (input_length > INT_MAX) {
|
|
170
|
+
rb_raise(rb_eArgError, "encoded image is too large");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
memset(&args, 0, sizeof(args));
|
|
174
|
+
args.input = RSTRING_PTR(input);
|
|
175
|
+
args.input_length = (int)input_length;
|
|
176
|
+
args.requested_channels = NUM2INT(requested_channels_value);
|
|
177
|
+
args.dtype = NUM2INT(dtype_value);
|
|
178
|
+
(void)tx_component_size(args.dtype);
|
|
179
|
+
if (args.requested_channels < 0 || args.requested_channels > 4) {
|
|
180
|
+
rb_raise(rb_eArgError, "requested channels must be between 0 and 4");
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (tx_info(args.input, args.input_length, &info_width, &info_height, &info_channels) &&
|
|
184
|
+
(size_t)info_width * (size_t)info_height >= TX_GVL_PIXEL_THRESHOLD) {
|
|
185
|
+
rb_thread_call_without_gvl(tx_decode_without_gvl, &args, NULL, NULL);
|
|
186
|
+
} else {
|
|
187
|
+
tx_decode_without_gvl(&args);
|
|
188
|
+
}
|
|
189
|
+
if (args.pixels == NULL) {
|
|
190
|
+
rb_raise(rb_eRuntimeError, "%s", args.error != NULL ? args.error : "image decode failed");
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
output_channels = args.requested_channels == 0 ? args.source_channels : args.requested_channels;
|
|
194
|
+
output_length = tx_checked_image_size(args.width, args.height, output_channels,
|
|
195
|
+
tx_component_size(args.dtype));
|
|
196
|
+
/* stb owns this allocation, so copy it once into Ruby-managed storage. */
|
|
197
|
+
output = rb_str_new((const char *)args.pixels, (long)output_length);
|
|
198
|
+
tx_free(args.pixels);
|
|
199
|
+
rb_enc_associate_index(output, rb_ascii8bit_encindex());
|
|
200
|
+
rb_obj_freeze(output);
|
|
201
|
+
RB_GC_GUARD(input);
|
|
202
|
+
return output;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
static void
|
|
206
|
+
tx_append_bytes(void *user_data, void *data, int size)
|
|
207
|
+
{
|
|
208
|
+
tx_buffer *buffer = (tx_buffer *)user_data;
|
|
209
|
+
size_t required;
|
|
210
|
+
size_t capacity;
|
|
211
|
+
unsigned char *replacement;
|
|
212
|
+
if (buffer->failed || size < 0) {
|
|
213
|
+
buffer->failed = 1;
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
required = buffer->length + (size_t)size;
|
|
217
|
+
if (required < buffer->length) {
|
|
218
|
+
buffer->failed = 1;
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
if (required > buffer->capacity) {
|
|
222
|
+
capacity = buffer->capacity == 0 ? 4096 : buffer->capacity;
|
|
223
|
+
while (capacity < required) {
|
|
224
|
+
if (capacity > SIZE_MAX / 2) {
|
|
225
|
+
capacity = required;
|
|
226
|
+
break;
|
|
227
|
+
}
|
|
228
|
+
capacity *= 2;
|
|
229
|
+
}
|
|
230
|
+
replacement = (unsigned char *)realloc(buffer->data, capacity);
|
|
231
|
+
if (replacement == NULL) {
|
|
232
|
+
buffer->failed = 1;
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
buffer->data = replacement;
|
|
236
|
+
buffer->capacity = capacity;
|
|
237
|
+
}
|
|
238
|
+
memcpy(buffer->data + buffer->length, data, (size_t)size);
|
|
239
|
+
buffer->length = required;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
static void *
|
|
243
|
+
tx_encode_without_gvl(void *pointer)
|
|
244
|
+
{
|
|
245
|
+
tx_encode_args *args = (tx_encode_args *)pointer;
|
|
246
|
+
args->result = tx_write(args->format, tx_append_bytes, args->buffer, args->width, args->height,
|
|
247
|
+
args->channels, args->pixels, args->quality_or_stride);
|
|
248
|
+
return NULL;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
static VALUE
|
|
252
|
+
tx_rb_encode(VALUE self, VALUE input, VALUE format_value, VALUE width_value, VALUE height_value,
|
|
253
|
+
VALUE channels_value, VALUE dtype_value, VALUE quality_value)
|
|
254
|
+
{
|
|
255
|
+
tx_buffer buffer = {0};
|
|
256
|
+
tx_encode_args args;
|
|
257
|
+
int dtype;
|
|
258
|
+
size_t expected_length;
|
|
259
|
+
VALUE output;
|
|
260
|
+
(void)self;
|
|
261
|
+
|
|
262
|
+
Check_Type(input, T_STRING);
|
|
263
|
+
memset(&args, 0, sizeof(args));
|
|
264
|
+
dtype = NUM2INT(dtype_value);
|
|
265
|
+
args.format = NUM2INT(format_value);
|
|
266
|
+
args.buffer = &buffer;
|
|
267
|
+
args.width = NUM2INT(width_value);
|
|
268
|
+
args.height = NUM2INT(height_value);
|
|
269
|
+
args.channels = NUM2INT(channels_value);
|
|
270
|
+
args.pixels = RSTRING_PTR(input);
|
|
271
|
+
args.quality_or_stride = NUM2INT(quality_value);
|
|
272
|
+
expected_length = tx_checked_image_size(args.width, args.height, args.channels,
|
|
273
|
+
tx_component_size(dtype));
|
|
274
|
+
if ((size_t)RSTRING_LEN(input) != expected_length) {
|
|
275
|
+
rb_raise(rb_eArgError, "pixel data length does not match image dimensions");
|
|
276
|
+
}
|
|
277
|
+
if ((args.format == 4 && dtype != 2) || (args.format != 4 && dtype != 0)) {
|
|
278
|
+
rb_raise(rb_eArgError, "pixel dtype is incompatible with output format");
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if ((size_t)args.width * (size_t)args.height >= TX_GVL_PIXEL_THRESHOLD) {
|
|
282
|
+
rb_thread_call_without_gvl(tx_encode_without_gvl, &args, NULL, NULL);
|
|
283
|
+
} else {
|
|
284
|
+
tx_encode_without_gvl(&args);
|
|
285
|
+
}
|
|
286
|
+
if (!args.result || buffer.failed) {
|
|
287
|
+
free(buffer.data);
|
|
288
|
+
rb_raise(rb_eRuntimeError, "image encode failed");
|
|
289
|
+
}
|
|
290
|
+
if (buffer.length > LONG_MAX) {
|
|
291
|
+
free(buffer.data);
|
|
292
|
+
rb_raise(rb_eRuntimeError, "encoded image is too large");
|
|
293
|
+
}
|
|
294
|
+
output = rb_str_new((const char *)buffer.data, (long)buffer.length);
|
|
295
|
+
free(buffer.data);
|
|
296
|
+
rb_enc_associate_index(output, rb_ascii8bit_encindex());
|
|
297
|
+
rb_obj_freeze(output);
|
|
298
|
+
RB_GC_GUARD(input);
|
|
299
|
+
return output;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
static void *
|
|
303
|
+
tx_resize_without_gvl(void *pointer)
|
|
304
|
+
{
|
|
305
|
+
tx_resize_args *args = (tx_resize_args *)pointer;
|
|
306
|
+
args->result = tx_resize(args->dtype, args->srgb, args->source,
|
|
307
|
+
args->source_width, args->source_height,
|
|
308
|
+
args->destination, args->width, args->height,
|
|
309
|
+
args->channels, args->filter, args->edge,
|
|
310
|
+
args->premultiplied);
|
|
311
|
+
return NULL;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
static VALUE
|
|
315
|
+
tx_rb_resize(VALUE self, VALUE input, VALUE source_width_value, VALUE source_height_value,
|
|
316
|
+
VALUE width_value, VALUE height_value, VALUE channels_value, VALUE dtype_value,
|
|
317
|
+
VALUE srgb_value, VALUE filter_value, VALUE edge_value, VALUE premultiplied_value)
|
|
318
|
+
{
|
|
319
|
+
tx_resize_args args;
|
|
320
|
+
size_t input_length;
|
|
321
|
+
size_t output_length;
|
|
322
|
+
VALUE output;
|
|
323
|
+
(void)self;
|
|
324
|
+
|
|
325
|
+
Check_Type(input, T_STRING);
|
|
326
|
+
memset(&args, 0, sizeof(args));
|
|
327
|
+
args.dtype = NUM2INT(dtype_value);
|
|
328
|
+
args.srgb = RTEST(srgb_value);
|
|
329
|
+
args.source = RSTRING_PTR(input);
|
|
330
|
+
args.source_width = NUM2INT(source_width_value);
|
|
331
|
+
args.source_height = NUM2INT(source_height_value);
|
|
332
|
+
args.width = NUM2INT(width_value);
|
|
333
|
+
args.height = NUM2INT(height_value);
|
|
334
|
+
args.channels = NUM2INT(channels_value);
|
|
335
|
+
args.filter = NUM2INT(filter_value);
|
|
336
|
+
args.edge = NUM2INT(edge_value);
|
|
337
|
+
args.premultiplied = RTEST(premultiplied_value);
|
|
338
|
+
|
|
339
|
+
input_length = tx_checked_image_size(args.source_width, args.source_height, args.channels,
|
|
340
|
+
tx_component_size(args.dtype));
|
|
341
|
+
output_length = tx_checked_image_size(args.width, args.height, args.channels,
|
|
342
|
+
tx_component_size(args.dtype));
|
|
343
|
+
if ((size_t)RSTRING_LEN(input) != input_length) {
|
|
344
|
+
rb_raise(rb_eArgError, "pixel data length does not match image dimensions");
|
|
345
|
+
}
|
|
346
|
+
output = rb_str_new(NULL, (long)output_length);
|
|
347
|
+
args.destination = RSTRING_PTR(output);
|
|
348
|
+
|
|
349
|
+
if ((size_t)args.source_width * (size_t)args.source_height >= TX_GVL_PIXEL_THRESHOLD &&
|
|
350
|
+
output_length >= TX_GVL_OUTPUT_THRESHOLD) {
|
|
351
|
+
rb_thread_call_without_gvl(tx_resize_without_gvl, &args, NULL, NULL);
|
|
352
|
+
} else {
|
|
353
|
+
tx_resize_without_gvl(&args);
|
|
354
|
+
}
|
|
355
|
+
if (!args.result) {
|
|
356
|
+
rb_raise(rb_eRuntimeError, "pixel resize failed");
|
|
357
|
+
}
|
|
358
|
+
rb_enc_associate_index(output, rb_ascii8bit_encindex());
|
|
359
|
+
rb_obj_freeze(output);
|
|
360
|
+
RB_GC_GUARD(input);
|
|
361
|
+
RB_GC_GUARD(output);
|
|
362
|
+
return output;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
static void *
|
|
366
|
+
tx_f16_without_gvl(void *pointer)
|
|
367
|
+
{
|
|
368
|
+
tx_f16_args *args = (tx_f16_args *)pointer;
|
|
369
|
+
tx_f32_to_f16(args->source, args->destination, args->count);
|
|
370
|
+
return NULL;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
static VALUE
|
|
374
|
+
tx_rb_pack_f16(VALUE self, VALUE input)
|
|
375
|
+
{
|
|
376
|
+
tx_f16_args args;
|
|
377
|
+
long input_length;
|
|
378
|
+
size_t output_length;
|
|
379
|
+
VALUE output;
|
|
380
|
+
(void)self;
|
|
381
|
+
|
|
382
|
+
Check_Type(input, T_STRING);
|
|
383
|
+
input_length = RSTRING_LEN(input);
|
|
384
|
+
if (input_length % (long)sizeof(float) != 0) {
|
|
385
|
+
rb_raise(rb_eArgError, "f32 data length must be a multiple of 4");
|
|
386
|
+
}
|
|
387
|
+
args.source = RSTRING_PTR(input);
|
|
388
|
+
args.count = (size_t)input_length / sizeof(float);
|
|
389
|
+
output_length = args.count * sizeof(uint16_t);
|
|
390
|
+
output = rb_str_new(NULL, (long)output_length);
|
|
391
|
+
args.destination = (uint16_t *)RSTRING_PTR(output);
|
|
392
|
+
|
|
393
|
+
if (args.count >= TX_GVL_PIXEL_THRESHOLD) {
|
|
394
|
+
rb_thread_call_without_gvl(tx_f16_without_gvl, &args, NULL, NULL);
|
|
395
|
+
} else {
|
|
396
|
+
tx_f16_without_gvl(&args);
|
|
397
|
+
}
|
|
398
|
+
rb_enc_associate_index(output, rb_ascii8bit_encindex());
|
|
399
|
+
rb_obj_freeze(output);
|
|
400
|
+
RB_GC_GUARD(input);
|
|
401
|
+
RB_GC_GUARD(output);
|
|
402
|
+
return output;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
void
|
|
406
|
+
Init_texel_native(void)
|
|
407
|
+
{
|
|
408
|
+
VALUE texel = rb_define_module("Texel");
|
|
409
|
+
VALUE bridge = rb_define_module_under(texel, "NativeBridge");
|
|
410
|
+
rb_define_singleton_method(bridge, "metadata", tx_rb_metadata, 1);
|
|
411
|
+
rb_define_singleton_method(bridge, "decode", tx_rb_decode, 3);
|
|
412
|
+
rb_define_singleton_method(bridge, "encode", tx_rb_encode, 7);
|
|
413
|
+
rb_define_singleton_method(bridge, "resize", tx_rb_resize, 11);
|
|
414
|
+
rb_define_singleton_method(bridge, "pack_f16", tx_rb_pack_f16, 1);
|
|
415
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Vendored stb headers
|
|
2
|
+
|
|
3
|
+
These headers are vendored from the public `nothings/stb` repository:
|
|
4
|
+
|
|
5
|
+
- `stb_image.h`
|
|
6
|
+
- `stb_image_write.h`
|
|
7
|
+
- `stb_image_resize2.h`
|
|
8
|
+
|
|
9
|
+
Each header includes its upstream Public Domain/MIT dual-license notice. Keeping
|
|
10
|
+
the headers in the gem makes the native extension build independent of system
|
|
11
|
+
packages.
|