magro 0.1.0 → 0.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 0b0bad63e73de3bde86fdd749536f8949fd84e8c
4
- data.tar.gz: a6c86139fe5cc7c401a9090a34400bf2387f5cc6
2
+ SHA256:
3
+ metadata.gz: a8bfcf8b7886c7fa5d89556642be97b077f6bf96d7a1725fd2cb8cf1b15c0aef
4
+ data.tar.gz: b0ab5bff6959f1264ed584dcefc25fb7a64fc866164f73dcf11b1556e7cf6377
5
5
  SHA512:
6
- metadata.gz: 872b26cc6825cd1bb7954e5104d6b196944798126d2d3ae66f08380d7017e747a8eb4748b6fbe2d688feff6ed79fbdb0d082ec29ed188b6cab38601125d7543e
7
- data.tar.gz: 53de5a2b240ce63404a8831f61b6285bf3009d3647c487ac6fb7fa32cada5372dced92d839297af911ee4a6b4c6e062bef88a692aac8d4ccdfa7ca1c7f5f45ea
6
+ metadata.gz: f40b650588ec98b33aa8d88bd5c0fb11d4dc8b14faaa197751275f478c2c316e7348b5bd1ff4cc55d33e451036b6b6411fc4f60d9d374c787c91d30ff661abc4
7
+ data.tar.gz: 7df4bfc3e747f2e8d8fb2dad307df3a0b6370fae1b6c53df66797252629851cf57ab256fdcc64088950ce9177ee2d6ca3a694e498588dc3b4baeef17c771d928
@@ -0,0 +1 @@
1
+ service_name: travis-ci
data/.gitignore CHANGED
@@ -7,6 +7,10 @@
7
7
  /pkg/
8
8
  /spec/reports/
9
9
  /tmp/
10
+ /spec/files/tmp.jpeg
11
+ /spec/files/tmp.png
12
+ /spec/files/tmp_UC.JPEG
13
+ /spec/files/tmp_UC.PNG
10
14
 
11
15
  # rspec failure tracking
12
16
  .rspec_status
@@ -1,5 +1,4 @@
1
1
  ---
2
- sudo: true
3
2
  os: linux
4
3
  dist: bionic
5
4
  language: ruby
@@ -8,7 +7,13 @@ rvm:
8
7
  - '2.4'
9
8
  - '2.5'
10
9
  - '2.6'
10
+ - '2.7'
11
+
12
+ addons:
13
+ apt:
14
+ packages:
15
+ - libpng-dev
16
+ - libjpeg-dev
11
17
 
12
18
  before_install:
13
- - sudo apt-get install -y libpng-dev libjpeg-dev
14
- - gem install bundler -v 2.0.2
19
+ - gem install bundler -v 2.1.4
@@ -1,2 +1,23 @@
1
+ # 0.4.0
2
+ - Rename extension file for reading and writing image file.
3
+ - Update documentations.
4
+
5
+ # 0.3.0
6
+ - Add [filter module](https://yoshoku.github.io/magro/doc/Magro/Filter.html) consists of image filtering methods.
7
+ - Change to use round instead of ceil in [quantization of resize method](https://github.com/yoshoku/magro/commit/1b3308ddfb98a650889483af3cd2045aaf6b8837) when given integer image.
8
+
9
+ # 0.2.0
10
+ - Add [transform module](https://yoshoku.github.io/magro/doc/Magro/Transform.html) and resize method.
11
+ - Fix some configulation files.
12
+
13
+ # 0.1.2
14
+ - Fix bug that fails to read and save file with upper case file extension.
15
+
16
+ # 0.1.1
17
+ - Refactor extension codes.
18
+ - Fix to raise IOError when occured file reading / writing error.
19
+ - Fix to raise NoMemoryError when occured memory allocation error.
20
+ - Several documentation improvements.
21
+
1
22
  # 0.1.0
2
23
  - First release.
data/Gemfile CHANGED
@@ -1,4 +1,10 @@
1
- source "https://rubygems.org"
1
+ source 'https://rubygems.org'
2
2
 
3
3
  # Specify your gem's dependencies in magro.gemspec
4
4
  gemspec
5
+
6
+ gem 'coveralls', '~> 0.8'
7
+ gem 'bundler', '~> 2.0'
8
+ gem 'rake', '~> 12.0'
9
+ gem 'rake-compiler', '~> 1.0'
10
+ gem 'rspec', '~> 3.0'
@@ -1,4 +1,4 @@
1
- Copyright (c) 2019 Atsushi Tatsuma
1
+ Copyright (c) 2019-2020 Atsushi Tatsuma
2
2
  All rights reserved.
3
3
 
4
4
  Redistribution and use in source and binary forms, with or without
data/README.md CHANGED
@@ -1,20 +1,27 @@
1
1
  # Magro
2
2
 
3
3
  [![Build Status](https://travis-ci.org/yoshoku/magro.svg?branch=master)](https://travis-ci.org/yoshoku/magro)
4
+ [![Coverage Status](https://coveralls.io/repos/github/yoshoku/magro/badge.svg?branch=master)](https://coveralls.io/github/yoshoku/magro?branch=master)
5
+ [![Gem Version](https://badge.fury.io/rb/magro.svg)](https://badge.fury.io/rb/magro)
4
6
  [![BSD 3-Clause License](https://img.shields.io/badge/License-BSD%203--Clause-orange.svg)](https://github.com/yoshoku/numo-liblinear/blob/master/LICENSE.txt)
7
+ [![Documentation](http://img.shields.io/badge/api-reference-blue.svg)](https://yoshoku.github.io/magro/doc/)
5
8
 
6
- Magro is an image processing library in Ruby.
7
- Magro uses [Numo::NArray](https://github.com/ruby-numo/numo-narray) arrays as image objects.
9
+ Magro is a minimal image processing library in Ruby.
10
+ Magro uses [Numo::NArray](https://github.com/ruby-numo/numo-narray) arrays as image objects and
11
+ provides basic image processing functions.
12
+ Current supporting features are reading and writing JPEG and PNG images,
13
+ image resizing with bilinear interpolation method, and image filtering.
8
14
 
9
15
  ## Installation
10
16
 
11
- Magro dependents some external libraries to provides functions loading image file.
17
+ Magro dependents libpng and libjpeg to provides functions reading and writing image file.
18
+ Moreover, it is recommended that using libpng version 1.6 or later.
12
19
 
13
20
  macOS:
14
21
 
15
22
  $ brew install libpng libjpeg
16
23
 
17
- Ubuntu:
24
+ Ubuntu (bionic):
18
25
 
19
26
  $ sudo apt-get install libpng-dev libjpeg-dev
20
27
 
@@ -32,25 +39,23 @@ Or install it yourself as:
32
39
 
33
40
  $ gem install magro
34
41
 
42
+ ## Documentation
43
+
44
+ - [Magro API Documentation](https://yoshoku.github.io/magro/doc/)
45
+
35
46
  ## Usage
36
47
 
37
48
  ```ruby
38
49
  > require 'magro'
39
50
  => true
40
- > image = Magro::IO.imread('lena.png')
51
+ > image = Magro::IO.imread('foo.png')
41
52
  => Numo::UInt8#shape=[512,512,3]
42
53
  > grayscale = image.median(axis: 2)
43
54
  => Numo::UInt8#shape=[512,512]
44
- > Magro::IO.imsave('lena_gray.png', grayscale)
55
+ > Magro::IO.imsave('foo_gray.png', grayscale)
45
56
  => true
46
57
  ```
47
58
 
48
- ## Development
49
-
50
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
51
-
52
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
53
-
54
59
  ## Contributing
55
60
 
56
61
  Bug reports and pull requests are welcome on GitHub at https://github.com/yoshoku/magro.
@@ -0,0 +1,411 @@
1
+ #include "imgrw.h"
2
+
3
+ RUBY_EXTERN VALUE mMagro;
4
+
5
+ /**
6
+ * @!visibility private
7
+ */
8
+ static
9
+ VALUE magro_io_read_png(VALUE self, VALUE filename_)
10
+ {
11
+ char* filename = StringValuePtr(filename_);
12
+ FILE* file_ptr = fopen(filename, "rb");
13
+ unsigned char header[8];
14
+ png_structp png_ptr;
15
+ png_infop info_ptr;
16
+ png_bytep* row_ptr_ptr;
17
+ png_bytep row_ptr;
18
+ png_uint_32 width, height;
19
+ int color_type;
20
+ int bit_depth;
21
+ png_uint_32 y;
22
+ int n_dims = 0;
23
+ int n_ch;
24
+ size_t shape[3] = { 0 };
25
+ VALUE nary;
26
+ uint8_t* nary_ptr;
27
+
28
+ if (file_ptr == NULL) {
29
+ rb_raise(rb_eIOError, "Failed to open file '%s'", filename);
30
+ return Qnil;
31
+ }
32
+
33
+ if (fread(header, 1, 8, file_ptr) < 8) {
34
+ fclose(file_ptr);
35
+ rb_raise(rb_eIOError, "Failed to read header info '%s'", filename);
36
+ return Qnil;
37
+ }
38
+
39
+ if (png_sig_cmp(header, 0, 8)) {
40
+ fclose(file_ptr);
41
+ rb_raise(rb_eIOError, "Failed to read header info '%s'", filename);
42
+ return Qnil;
43
+ }
44
+
45
+ png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
46
+ if (png_ptr == NULL) {
47
+ fclose(file_ptr);
48
+ rb_raise(rb_eNoMemError, "Failed to allocate memory.");
49
+ return Qnil;
50
+ }
51
+ info_ptr = png_create_info_struct(png_ptr);
52
+ if (info_ptr == NULL) {
53
+ png_destroy_read_struct(&png_ptr, NULL, NULL);
54
+ fclose(file_ptr);
55
+ rb_raise(rb_eNoMemError, "Failed to allocate memory.");
56
+ return Qnil;
57
+ }
58
+ if (setjmp(png_jmpbuf(png_ptr))) {
59
+ png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
60
+ fclose(file_ptr);
61
+ rb_raise(rb_eIOError, "Error happened while reading file '%s'", filename);
62
+ return Qnil;
63
+ }
64
+
65
+ png_init_io(png_ptr, file_ptr);
66
+ png_set_sig_bytes(png_ptr, 8);
67
+
68
+ png_read_png(png_ptr, info_ptr, PNG_TRANSFORM_PACKING | PNG_TRANSFORM_EXPAND | PNG_TRANSFORM_STRIP_16, NULL);
69
+ row_ptr_ptr = png_get_rows(png_ptr, info_ptr);
70
+ png_get_IHDR(png_ptr, info_ptr, &width, &height, &bit_depth, &color_type, NULL, NULL, NULL);
71
+
72
+ if (color_type == PNG_COLOR_TYPE_PALETTE) {
73
+ png_set_palette_to_rgb(png_ptr);
74
+ png_read_update_info(png_ptr, info_ptr);
75
+ png_get_IHDR(png_ptr, info_ptr, &width, &height, &bit_depth, &color_type, NULL, NULL, NULL);
76
+ }
77
+
78
+ switch (color_type) {
79
+ case PNG_COLOR_TYPE_GRAY:
80
+ n_ch = 1;
81
+ n_dims = 2;
82
+ shape[0] = height;
83
+ shape[1] = width;
84
+ break;
85
+ case PNG_COLOR_TYPE_GRAY_ALPHA:
86
+ n_ch = 2;
87
+ n_dims = 3;
88
+ shape[0] = height;
89
+ shape[1] = width;
90
+ shape[2] = 2;
91
+ break;
92
+ case PNG_COLOR_TYPE_RGB:
93
+ n_ch = 3;
94
+ n_dims = 3;
95
+ shape[0] = height;
96
+ shape[1] = width;
97
+ shape[2] = 3;
98
+ break;
99
+ case PNG_COLOR_TYPE_RGB_ALPHA:
100
+ n_ch = 4;
101
+ n_dims = 3;
102
+ shape[0] = height;
103
+ shape[1] = width;
104
+ shape[2] = 4;
105
+ break;
106
+ default:
107
+ n_dims = 0;
108
+ break;
109
+ }
110
+
111
+ if (n_dims == 0) {
112
+ fclose(file_ptr);
113
+ png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
114
+ rb_raise(rb_eIOError, "Unsupported color type of input file '%s'", filename);
115
+ return Qnil;
116
+ }
117
+
118
+ nary = rb_narray_new(numo_cUInt8, n_dims, shape);
119
+ nary_ptr = (uint8_t*)na_get_pointer_for_write(nary);
120
+
121
+ for (y = 0; y < height; y++) {
122
+ row_ptr = row_ptr_ptr[y];
123
+ memcpy(nary_ptr + y * width * n_ch, row_ptr, width * n_ch);
124
+ }
125
+
126
+ fclose(file_ptr);
127
+ png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
128
+
129
+ return nary;
130
+ }
131
+
132
+ /**
133
+ * @!visibility private
134
+ */
135
+ static
136
+ VALUE magro_io_save_png(VALUE self, VALUE filename_, VALUE image)
137
+ {
138
+ char* filename = StringValuePtr(filename_);
139
+ FILE* file_ptr = fopen(filename, "wb");
140
+ png_structp png_ptr;
141
+ png_infop info_ptr;
142
+ png_bytep* row_ptr_ptr;
143
+ png_uint_32 width, height;
144
+ int color_type;
145
+ int bit_depth = 8;
146
+ png_uint_32 y;
147
+ int n_ch;
148
+ int n_dims;
149
+ narray_t* image_nary;
150
+ uint8_t* image_ptr;
151
+
152
+ if (file_ptr == NULL) {
153
+ rb_raise(rb_eIOError, "Failed to open file '%s'", filename);
154
+ return Qfalse;
155
+ }
156
+
157
+ if (CLASS_OF(image) != numo_cUInt8) {
158
+ image = rb_funcall(numo_cUInt8, rb_intern("cast"), 1, image);
159
+ }
160
+ if (!RTEST(nary_check_contiguous(image))) {
161
+ image = nary_dup(image);
162
+ }
163
+
164
+ GetNArray(image, image_nary);
165
+ n_dims = NA_NDIM(image_nary);
166
+ height = (png_uint_32)NA_SHAPE(image_nary)[0];
167
+ width = (png_uint_32)NA_SHAPE(image_nary)[1];
168
+ image_ptr = (uint8_t*)na_get_pointer_for_read(image);
169
+
170
+ n_ch = 1;
171
+ if (n_dims == 3) {
172
+ n_ch = (int)NA_SHAPE(image_nary)[2];
173
+ }
174
+
175
+ switch (n_ch) {
176
+ case 4:
177
+ color_type = PNG_COLOR_TYPE_RGBA;
178
+ break;
179
+ case 3:
180
+ color_type = PNG_COLOR_TYPE_RGB;
181
+ break;
182
+ case 2:
183
+ color_type = PNG_COLOR_TYPE_GRAY_ALPHA;
184
+ break;
185
+ default:
186
+ color_type = PNG_COLOR_TYPE_GRAY;
187
+ break;
188
+ }
189
+
190
+ png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
191
+ if (png_ptr == NULL) {
192
+ fclose(file_ptr);
193
+ rb_raise(rb_eNoMemError, "Failed to allocate memory.");
194
+ return Qfalse;
195
+ }
196
+ info_ptr = png_create_info_struct(png_ptr);
197
+ if (info_ptr == NULL) {
198
+ png_destroy_read_struct(&png_ptr, NULL, NULL);
199
+ fclose(file_ptr);
200
+ rb_raise(rb_eNoMemError, "Failed to allocate memory.");
201
+ return Qfalse;
202
+ }
203
+ if (setjmp(png_jmpbuf(png_ptr))) {
204
+ png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
205
+ fclose(file_ptr);
206
+ return Qfalse;
207
+ }
208
+
209
+ png_set_IHDR(png_ptr, info_ptr, width, height, bit_depth, color_type,
210
+ PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT);
211
+
212
+ row_ptr_ptr = png_malloc(png_ptr, height * sizeof(png_bytep));
213
+ for (y = 0; y < height; y++) {
214
+ row_ptr_ptr[y] = png_malloc(png_ptr, width * n_ch * sizeof(png_byte));
215
+ memcpy(row_ptr_ptr[y], image_ptr + y * width * n_ch, width * n_ch);
216
+ }
217
+
218
+ png_init_io(png_ptr, file_ptr);
219
+ png_set_rows(png_ptr, info_ptr, row_ptr_ptr);
220
+ png_write_png(png_ptr, info_ptr, PNG_TRANSFORM_IDENTITY, NULL);
221
+
222
+ fclose(file_ptr);
223
+ for (y = 0; y < height; y++) {
224
+ png_free(png_ptr, row_ptr_ptr[y]);
225
+ }
226
+ png_free(png_ptr, row_ptr_ptr);
227
+ png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
228
+
229
+ return Qtrue;
230
+ }
231
+
232
+ struct my_error_mgr {
233
+ struct jpeg_error_mgr pub;
234
+ jmp_buf setjmp_buffer;
235
+ };
236
+
237
+ static void
238
+ my_error_exit(j_common_ptr cinfo)
239
+ {
240
+ struct my_error_mgr* my_err = (struct my_error_mgr*)cinfo->err;
241
+ (*cinfo->err->output_message)(cinfo);
242
+ longjmp(my_err->setjmp_buffer, 1);
243
+ }
244
+
245
+ /**
246
+ * @!visibility private
247
+ */
248
+ static
249
+ VALUE magro_io_read_jpg(VALUE self, VALUE filename_)
250
+ {
251
+ char* filename = StringValuePtr(filename_);
252
+ FILE* file_ptr = fopen(filename, "rb");
253
+ struct jpeg_decompress_struct jpeg;
254
+ struct my_error_mgr err;
255
+ unsigned int width, height;
256
+ int n_colors;
257
+ size_t shape[3] = { 0 };
258
+ int n_dims;
259
+ unsigned int y;
260
+ VALUE nary;
261
+ uint8_t* nary_ptr;
262
+ JSAMPLE* tmp;
263
+
264
+ if (file_ptr == NULL) {
265
+ rb_raise(rb_eIOError, "Failed to open file '%s'", filename);
266
+ return Qnil;
267
+ }
268
+
269
+ jpeg.err = jpeg_std_error(&err.pub);
270
+ err.pub.error_exit = my_error_exit;
271
+ if (setjmp(err.setjmp_buffer)) {
272
+ rb_raise(rb_eIOError, "Error happened while reading file '%s'", filename);
273
+ return Qnil;
274
+ }
275
+
276
+ jpeg_create_decompress(&jpeg);
277
+ jpeg_stdio_src(&jpeg, file_ptr);
278
+ jpeg_read_header(&jpeg, TRUE);
279
+ jpeg_start_decompress(&jpeg);
280
+
281
+ width = jpeg.output_width;
282
+ height = jpeg.output_height;
283
+ n_colors = jpeg.out_color_components;
284
+
285
+ n_dims = n_colors == 1 ? 2 : 3;
286
+ shape[0] = height;
287
+ shape[1] = width;
288
+ shape[2] = n_colors;
289
+ nary = rb_narray_new(numo_cUInt8, n_dims, shape);
290
+ nary_ptr = (uint8_t*)na_get_pointer_for_write(nary);
291
+
292
+ for (y = 0; y < height; y++) {
293
+ tmp = nary_ptr + y * width * n_colors;
294
+ jpeg_read_scanlines(&jpeg, &tmp, 1);
295
+ }
296
+
297
+ fclose(file_ptr);
298
+ jpeg_finish_decompress(&jpeg);
299
+ jpeg_destroy_decompress(&jpeg);
300
+
301
+ return nary;
302
+ }
303
+
304
+ /**
305
+ * @!visibility private
306
+ */
307
+ static
308
+ VALUE magro_io_save_jpg(int argc, VALUE* argv, VALUE self)
309
+ {
310
+ VALUE filename_;
311
+ VALUE image;
312
+ VALUE quality_;
313
+ char* filename;
314
+ FILE* file_ptr;
315
+ struct jpeg_compress_struct jpeg;
316
+ struct my_error_mgr err;
317
+ narray_t* image_nary;
318
+ int quality;
319
+ int n_dims, n_ch;
320
+ unsigned int width, height, y;
321
+ uint8_t* image_ptr;
322
+ JSAMPLE* tmp;
323
+
324
+ rb_scan_args(argc, argv, "21", &filename_, &image, &quality_);
325
+
326
+ if (NIL_P(quality_)) {
327
+ quality = 95;
328
+ } else {
329
+ quality = NUM2INT(quality_);
330
+ }
331
+
332
+ filename = StringValuePtr(filename_);
333
+
334
+ if (CLASS_OF(image) != numo_cUInt8) {
335
+ image = rb_funcall(numo_cUInt8, rb_intern("cast"), 1, image);
336
+ }
337
+ if (!RTEST(nary_check_contiguous(image))) {
338
+ image = nary_dup(image);
339
+ }
340
+
341
+ jpeg.err = jpeg_std_error(&err.pub);
342
+ err.pub.error_exit = my_error_exit;
343
+ if (setjmp(err.setjmp_buffer)) {
344
+ return Qfalse;
345
+ }
346
+
347
+ jpeg_create_compress(&jpeg);
348
+
349
+ file_ptr = fopen(filename, "wb");
350
+ if (file_ptr == NULL) {
351
+ rb_raise(rb_eIOError, "Failed to open file '%s'", filename);
352
+ jpeg_destroy_compress(&jpeg);
353
+ return Qfalse;
354
+ }
355
+
356
+ GetNArray(image, image_nary);
357
+ n_dims = NA_NDIM(image_nary);
358
+ height = (unsigned int)NA_SHAPE(image_nary)[0];
359
+ width = (unsigned int)NA_SHAPE(image_nary)[1];
360
+ image_ptr = (uint8_t*)na_get_pointer_for_read(image);
361
+
362
+ n_ch = 1;
363
+ if (n_dims == 3) {
364
+ n_ch = (int)NA_SHAPE(image_nary)[2];
365
+ }
366
+
367
+ jpeg_stdio_dest(&jpeg, file_ptr);
368
+
369
+ jpeg.image_height = height;
370
+ jpeg.image_width = width;
371
+ jpeg.input_components = n_ch;
372
+
373
+ switch (n_ch) {
374
+ case 3:
375
+ jpeg.in_color_space = JCS_RGB;
376
+ break;
377
+ case 1:
378
+ jpeg.in_color_space = JCS_GRAYSCALE;
379
+ break;
380
+ default:
381
+ jpeg.in_color_space = JCS_UNKNOWN;
382
+ break;
383
+ }
384
+
385
+ jpeg_set_defaults(&jpeg);
386
+
387
+ jpeg_set_quality(&jpeg, quality, TRUE);
388
+
389
+ jpeg_start_compress(&jpeg, TRUE);
390
+
391
+ for (y = 0; y < height; y++) {
392
+ tmp = image_ptr + y * width * n_ch;
393
+ jpeg_write_scanlines(&jpeg, &tmp, 1);
394
+ }
395
+
396
+ jpeg_finish_compress(&jpeg);
397
+ jpeg_destroy_compress(&jpeg);
398
+
399
+ fclose(file_ptr);
400
+
401
+ return Qtrue;
402
+ }
403
+
404
+ void init_io_module()
405
+ {
406
+ VALUE mIO = rb_define_module_under(mMagro, "IO");
407
+ rb_define_module_function(mIO, "read_png", magro_io_read_png, 1);
408
+ rb_define_module_function(mIO, "save_png", magro_io_save_png, 2);
409
+ rb_define_module_function(mIO, "read_jpg", magro_io_read_jpg, 1);
410
+ rb_define_module_function(mIO, "save_jpg", magro_io_save_jpg, -1);
411
+ }