rszr 0.7.1 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bfbe455a6a95a6fc5161b65c03450e66e3e7165794556abd90cfef22da5a1f32
4
- data.tar.gz: 1ced66bb5a143f23d55dd1496fc79cf29d58b529138405741f93ba6379e62ba5
3
+ metadata.gz: 453bbac498bb376737ac50b776a9d1824cbb0f2338753b82c09c36ef08bc152b
4
+ data.tar.gz: 66941eca0be0d49922627be9329ca6c623686809094be55b80fc490f8dd59290
5
5
  SHA512:
6
- metadata.gz: 007c4d61f001ec86a6be7db860a68edf61f1eac63dfbf1b55afc24e51aa4033248b9224a3706b53c2887555812c0eeaac1d7e62ef66c40e0dd0544c8d67642a3
7
- data.tar.gz: ef41050b95a085ba6d1fd87c26596e3387a9eb6b248e1d0f741c079dcabaa381912e9c07f28916ff8739df29c903513c0cf78e22384396f5a16384428f6b8014
6
+ metadata.gz: ed7559dfe4f2a754f9f4b46fe0cf3b88f1003d8c055dc38d5eb9335f49ff8656689464fbb8f0c0756466ab9dd6af775e9bb12add63f29d167a20e832354d3adc
7
+ data.tar.gz: 49b195f7d27005a81175ae467e0f7f05d974b020db07a90a1f34999df2dd69cd96591d2a96d7ae07872886074dbc937e8d53bf99380fa0bf5d94181065461682
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- [![Gem Version](https://badge.fury.io/rb/rszr.svg)](http://badge.fury.io/rb/rszr) [![Build Status](https://travis-ci.org/mtgrosser/rszr.svg)](https://travis-ci.org/mtgrosser/rszr)
1
+ [![Gem Version](https://badge.fury.io/rb/rszr.svg)](http://badge.fury.io/rb/rszr) [![build](https://github.com/mtgrosser/rszr/actions/workflows/build.yml/badge.svg)](https://github.com/mtgrosser/rszr/actions/workflows/build.yml)
2
2
  # Rszr - fast image resizer for Ruby
3
3
 
4
4
  Rszr is an image resizer for Ruby based on the Imlib2 library.
@@ -12,29 +12,32 @@ In your Gemfile:
12
12
  gem 'rszr'
13
13
  ```
14
14
 
15
- ### Imlib2 and libexif
15
+ ### Imlib2
16
16
 
17
17
  Rszr requires the `Imlib2` library to do the heavy lifting.
18
- `libexif` is required for EXIF auto orientation.
19
18
 
20
19
  #### OS X
21
20
 
22
21
  Using homebrew:
23
22
 
24
23
  ```bash
25
- brew install imlib2 libexif
24
+ brew install imlib2
26
25
  ```
27
26
 
28
27
  #### Linux
29
28
 
30
29
  Using your favourite package manager:
31
30
 
31
+ ##### RedHat-based
32
+
32
33
  ```bash
33
- yum install imlib2 imlib2-devel libexif libexif-devel
34
+ yum install imlib2 imlib2-devel
34
35
  ```
35
36
 
37
+ ##### Debian-based
38
+
36
39
  ```bash
37
- apt-get install libimlib2 libimlib2-dev libexif libexif-dev
40
+ apt-get install libimlib2 libimlib2-dev
38
41
  ```
39
42
 
40
43
  ## Usage
@@ -51,12 +54,25 @@ image.save('resized.jpg')
51
54
  image.save('resized.png')
52
55
  ```
53
56
 
57
+ ### Image info
58
+ ```ruby
59
+ image.width => 400
60
+ image.height => 300
61
+ image.dimensions => [400, 300]
62
+ image.format => "jpeg"
63
+ image[0, 0] => <Rszr::Color::RGBA @red=38, @green=115, @blue=141, @alpha=255>
64
+ image[0, 0].to_hex => "26738dff"
65
+ image[0, 0].to_hex(rgb: true) => "26738d"
66
+ ```
67
+
54
68
  ### Transformations
55
69
 
56
70
  For each transformation, there is a bang! and non-bang method.
57
71
  The bang method changes the image in place, while the non-bang method
58
72
  creates a copy of the image in memory.
59
73
 
74
+ #### Resizing
75
+
60
76
  ```ruby
61
77
  # auto height
62
78
  image.resize(400, :auto)
@@ -67,6 +83,22 @@ image.resize(:auto, 300)
67
83
  # scale factor
68
84
  image.resize(0.5)
69
85
 
86
+ # resize to fill
87
+ image.resize(400, 300, crop: true)
88
+
89
+ # resize to fill with gravity
90
+ # where gravity in [:n, :nw, :w, :sw, :w, :se, :e, :ne, :center]
91
+ image.resize(400, 300, crop: gravity)
92
+
93
+ # save memory, do not duplicate instance
94
+ image.resize!(400, :auto)
95
+ ```
96
+
97
+ Check out the [full list and demo of resize options](https://mtgrosser.github.io/rszr/resizing.html)!
98
+
99
+ #### Other transformations
100
+
101
+ ```ruby
70
102
  # crop
71
103
  image.crop(200, 200, 100, 100)
72
104
 
@@ -79,45 +111,65 @@ image.turn!(-1)
79
111
  # rotate by arbitrary angle
80
112
  image.rotate(45)
81
113
 
114
+ # flip vertically
115
+ image.flip
116
+
117
+ # flop horizontally
118
+ image.flop
119
+
120
+ # initialize copy
121
+ image.dup
122
+ ```
123
+
124
+ ### Filters
125
+
126
+ Filters also support bang! and non-bang methods.
127
+
128
+ ```ruby
82
129
  # sharpen image by pixel radius
83
130
  image.sharpen!(1)
84
131
 
85
132
  # blur image by pixel radius
86
133
  image.blur!(1)
87
134
 
88
- # initialize copy
89
- image.dup
135
+ # brighten
136
+ image.brighten(0.1)
90
137
 
91
- # save memory, do not duplicate instance
92
- image.resize!(400, :auto)
93
- ```
138
+ # darken
139
+ image.brighten(-0.1)
94
140
 
95
- ### Image info
96
- ```ruby
97
- image.width => 400
98
- image.height => 300
99
- image.dimensions => [400, 300]
100
- image.format => "jpeg"
141
+ # contrast
142
+ image.contrast(0.5)
143
+
144
+ # gamma
145
+ image.gamma(1.1)
101
146
  ```
102
147
 
103
148
  ### Image auto orientation
104
149
 
150
+ Auto-rotation is supported for JPEG and TIFF files that include the necessary
151
+ EXIF metadata.
152
+
105
153
  ```ruby
106
154
  # load and autorotate
107
155
  image = Rszr::Image.load('image.jpg', autorotate: true)
156
+ ```
157
+
158
+ To enable autorotation by default:
108
159
 
160
+ ```ruby
109
161
  # auto-rotate by default, for Rails apps put this into an initializer
110
162
  Rszr.autorotate = true
111
163
  ```
112
164
 
113
165
  ## Rails / ActiveStorage interface
114
166
 
115
- Rszr provides a drop-in interface to the `image_resizing` gem.
167
+ Rszr provides a drop-in interface to the `image_processing` gem.
116
168
  It is faster than both `mini_magick` and `vips` and way easier to install than the latter.
117
169
 
118
170
  ```ruby
119
171
  # Gemfile
120
- gem 'image_resizing'
172
+ gem 'image_processing'
121
173
  gem 'rszr'
122
174
 
123
175
  # config/initializers/rszr.rb
@@ -133,6 +185,19 @@ When creating image variants, you can use all of Rszr's transformation methods:
133
185
  <%= image_tag user.avatar.variant(resize_to_fit: [300, 200]) %>
134
186
  ```
135
187
 
188
+ ## Loading from and saving to memory
189
+
190
+ The `Imlib2` library is mainly file-oriented and doesn't provide a way of loading
191
+ the undecoded image from a memory buffer. Therefore, the functionality is
192
+ implemented on the Ruby side of the gem, writing the memory buffer to a Tempfile.
193
+ Currently, this local write cannot be avoided.
194
+
195
+ ```ruby
196
+ image = Rszr::Image.load_data(binary_data)
197
+
198
+ data = image.save_data(format: :jpeg)
199
+ ```
200
+
136
201
  ## Thread safety
137
202
 
138
203
  As of version 0.5.0, Rszr is thread safe through Ruby's global VM lock.
data/ext/rszr/errors.c CHANGED
@@ -42,10 +42,15 @@ void Init_rszr_errors()
42
42
 
43
43
  static void rszr_raise_error_with_message(VALUE rb_error_class, Imlib_Load_Error error)
44
44
  {
45
- int error_index = (int) error - 1;
45
+ VALUE rb_error;
46
+ int error_index;
47
+
48
+ error_index = (int) error - 1;
49
+
46
50
  if (error_index < 1 || error_index > RSZR_MAX_ERROR_INDEX)
47
51
  error_index = RSZR_MAX_ERROR_INDEX;
48
- VALUE rb_error = rb_exc_new2(rb_error_class, sRszrErrorMessages[error_index]);
52
+
53
+ rb_error = rb_exc_new2(rb_error_class, sRszrErrorMessages[error_index]);
49
54
  rb_exc_raise(rb_error);
50
55
  }
51
56
 
data/ext/rszr/extconf.rb CHANGED
@@ -1,10 +1,9 @@
1
1
  require 'mkmf'
2
2
  require 'rbconfig'
3
3
 
4
- imlib2_config = with_config('imlib2-config', 'imlib2-config')
4
+ pkg_config('imlib2')
5
5
 
6
- $CFLAGS << ' -DX_DISPLAY_MISSING ' << `#{imlib2_config} --cflags`.chomp
7
- $LDFLAGS << ' ' << `#{imlib2_config} --libs`.chomp
6
+ $CFLAGS << ' -DX_DISPLAY_MISSING'
8
7
  $LDFLAGS.gsub!(/\ -lX11\ -lXext/, '') if RUBY_PLATFORM =~ /darwin/
9
8
 
10
9
  unless find_header('Imlib2.h')
@@ -15,11 +14,4 @@ unless find_library('Imlib2', 'imlib_set_cache_size')
15
14
  abort 'Imlib2 is missing'
16
15
  end
17
16
 
18
- unless find_library('exif', 'exif_data_new_from_file')
19
- abort 'libexif is missing'
20
- end
21
-
22
- have_library('exif')
23
- have_header('libexif/exif-data.h')
24
-
25
17
  create_makefile 'rszr/rszr'
data/ext/rszr/image.c CHANGED
@@ -27,43 +27,6 @@ static void rszr_image_deallocate(rszr_image_handle * handle)
27
27
  // fprintf(stderr, "\n");
28
28
  }
29
29
 
30
-
31
- static void rszr_image_autorotate(Imlib_Image image, char * path)
32
- {
33
- ExifData * exifData;
34
- ExifByteOrder byteOrder;
35
- ExifEntry * exifEntry;
36
- int orientation = 0;
37
- int turns = 0;
38
-
39
- exifData = exif_data_new_from_file(path);
40
-
41
- if (exifData) {
42
- byteOrder = exif_data_get_byte_order(exifData);
43
- exifEntry = exif_data_get_entry(exifData, EXIF_TAG_ORIENTATION);
44
- if (exifEntry) {
45
- orientation = exif_get_short(exifEntry->data, byteOrder);
46
- }
47
- }
48
-
49
- if (orientation < 2 || orientation > 8) return;
50
-
51
- imlib_context_set_image(image);
52
-
53
- if (orientation == 2 || orientation == 4 || orientation == 5 || orientation == 7) {
54
- imlib_image_flip_horizontal();
55
- }
56
-
57
- if (orientation == 5 || orientation == 6) {
58
- imlib_image_orientate(1);
59
- } else if (orientation == 3 || orientation == 4) {
60
- imlib_image_orientate(2);
61
- } else if (orientation == 7 || orientation == 8) {
62
- imlib_image_orientate(3);
63
- }
64
- }
65
-
66
-
67
30
  static VALUE rszr_image_s_allocate(VALUE klass)
68
31
  {
69
32
  rszr_image_handle * handle = calloc(1, sizeof(rszr_image_handle));
@@ -86,7 +49,7 @@ static VALUE rszr_image_initialize(VALUE self, VALUE rb_width, VALUE rb_height)
86
49
  }
87
50
 
88
51
 
89
- static VALUE rszr_image_s__load(VALUE klass, VALUE rb_path, VALUE autorotate)
52
+ static VALUE rszr_image_s__load(VALUE klass, VALUE rb_path)
90
53
  {
91
54
  rszr_image_handle * handle;
92
55
  Imlib_Image image;
@@ -97,14 +60,19 @@ static VALUE rszr_image_s__load(VALUE klass, VALUE rb_path, VALUE autorotate)
97
60
  path = StringValueCStr(rb_path);
98
61
 
99
62
  imlib_set_cache_size(0);
100
- image = imlib_load_image_with_error_return(path, &error);
63
+ image = imlib_load_image_without_cache(path);
101
64
 
102
65
  if (!image) {
103
- rszr_raise_load_error(error);
104
- return Qnil;
66
+ image = imlib_load_image_with_error_return(path, &error);
67
+
68
+ if (!image) {
69
+ rszr_raise_load_error(error);
70
+ return Qnil;
71
+ }
105
72
  }
106
73
 
107
- if (RTEST(autorotate)) rszr_image_autorotate(image, path);
74
+ imlib_context_set_image(image);
75
+ imlib_image_set_irrelevant_format(0);
108
76
 
109
77
  oImage = rszr_image_s_allocate(cImage);
110
78
  Data_Get_Struct(oImage, rszr_image_handle, handle);
@@ -113,7 +81,7 @@ static VALUE rszr_image_s__load(VALUE klass, VALUE rb_path, VALUE autorotate)
113
81
  }
114
82
 
115
83
 
116
- static VALUE rszr_image_format_get(VALUE self)
84
+ static VALUE rszr_image__format_get(VALUE self)
117
85
  {
118
86
  rszr_image_handle * handle;
119
87
  char * format;
@@ -172,6 +140,32 @@ static VALUE rszr_image_height(VALUE self)
172
140
  return INT2NUM(height);
173
141
  }
174
142
 
143
+
144
+ static VALUE rszr_image__pixel_get(VALUE self, VALUE rb_x, VALUE rb_y)
145
+ {
146
+ rszr_image_handle * handle;
147
+ Imlib_Color color_return;
148
+ VALUE rb_rgba;
149
+ int x, y;
150
+
151
+ Check_Type(rb_x, T_FIXNUM);
152
+ x = FIX2INT(rb_x);
153
+ Check_Type(rb_y, T_FIXNUM);
154
+ y = FIX2INT(rb_y);
155
+
156
+ Data_Get_Struct(self, rszr_image_handle, handle);
157
+
158
+ imlib_context_set_image(handle->image);
159
+ imlib_image_query_pixel(x, y, &color_return);
160
+
161
+ rb_rgba = rb_ary_new3(4, INT2NUM(color_return.red),
162
+ INT2NUM(color_return.green),
163
+ INT2NUM(color_return.blue),
164
+ INT2NUM(color_return.alpha));
165
+ return rb_rgba;
166
+ }
167
+
168
+
175
169
  /*
176
170
  static VALUE rszr_image_get_quality(VALUE self)
177
171
  {
@@ -249,6 +243,32 @@ static VALUE rszr_image__turn_bang(VALUE self, VALUE orientation)
249
243
  }
250
244
 
251
245
 
246
+ static VALUE rszr_image_flop_bang(VALUE self)
247
+ {
248
+ rszr_image_handle * handle;
249
+
250
+ Data_Get_Struct(self, rszr_image_handle, handle);
251
+
252
+ imlib_context_set_image(handle->image);
253
+ imlib_image_flip_horizontal();
254
+
255
+ return self;
256
+ }
257
+
258
+
259
+ static VALUE rszr_image_flip_bang(VALUE self)
260
+ {
261
+ rszr_image_handle * handle;
262
+
263
+ Data_Get_Struct(self, rszr_image_handle, handle);
264
+
265
+ imlib_context_set_image(handle->image);
266
+ imlib_image_flip_vertical();
267
+
268
+ return self;
269
+ }
270
+
271
+
252
272
  static VALUE rszr_image__rotate(VALUE self, VALUE bang, VALUE rb_angle)
253
273
  {
254
274
  rszr_image_handle * handle;
@@ -288,7 +308,6 @@ static VALUE rszr_image__rotate(VALUE self, VALUE bang, VALUE rb_angle)
288
308
  static VALUE rszr_image_filter_bang(VALUE self, VALUE rb_filter_expr)
289
309
  {
290
310
  rszr_image_handle * handle;
291
- Imlib_Image image;
292
311
  char * filter_expr;
293
312
 
294
313
  filter_expr = StringValueCStr(rb_filter_expr);
@@ -302,22 +321,6 @@ static VALUE rszr_image_filter_bang(VALUE self, VALUE rb_filter_expr)
302
321
  }
303
322
 
304
323
 
305
- static VALUE rszr_image__brighten_bang(VALUE self, VALUE rb_brightness)
306
- {
307
- rszr_image_handle * handle;
308
- double brightness;
309
-
310
- brightness = NUM2DBL(rb_brightness);
311
-
312
- Data_Get_Struct(self, rszr_image_handle, handle);
313
-
314
- imlib_context_set_image(handle->image);
315
- imlib_apply_filter("brightness(10);");
316
-
317
- return self;
318
- }
319
-
320
-
321
324
  static VALUE rszr_image__sharpen_bang(VALUE self, VALUE rb_radius)
322
325
  {
323
326
  rszr_image_handle * handle;
@@ -332,7 +335,7 @@ static VALUE rszr_image__sharpen_bang(VALUE self, VALUE rb_radius)
332
335
  if (radius >= 0) {
333
336
  imlib_image_sharpen(radius);
334
337
  } else {
335
- imlib_image_blur(radius);
338
+ imlib_image_blur(-radius);
336
339
  }
337
340
 
338
341
  return self;
@@ -476,27 +479,29 @@ void Init_rszr_image()
476
479
  rb_define_alloc_func(cImage, rszr_image_s_allocate);
477
480
 
478
481
  // Class methods
479
- rb_define_private_method(rb_singleton_class(cImage), "_load", rszr_image_s__load, 2);
482
+ rb_define_private_method(rb_singleton_class(cImage), "_load", rszr_image_s__load, 1);
480
483
 
481
484
  // Instance methods
482
485
  rb_define_method(cImage, "initialize", rszr_image_initialize, 2);
483
486
  rb_define_method(cImage, "width", rszr_image_width, 0);
484
487
  rb_define_method(cImage, "height", rszr_image_height, 0);
485
- rb_define_method(cImage, "format", rszr_image_format_get, 0);
486
488
  rb_define_method(cImage, "dup", rszr_image_dup, 0);
487
489
  rb_define_method(cImage, "filter!", rszr_image_filter_bang, 1);
490
+ rb_define_method(cImage, "flop!", rszr_image_flop_bang, 0);
491
+ rb_define_method(cImage, "flip!", rszr_image_flip_bang, 0);
488
492
 
489
493
  // rb_define_method(cImage, "quality", rszr_image_get_quality, 0);
490
494
  // rb_define_method(cImage, "quality=", rszr_image_set_quality, 1);
491
495
 
496
+ rb_define_protected_method(cImage, "_format", rszr_image__format_get, 0);
492
497
  rb_define_protected_method(cImage, "_format=", rszr_image__format_set, 1);
493
498
 
494
499
  rb_define_private_method(cImage, "_resize", rszr_image__resize, 7);
495
500
  rb_define_private_method(cImage, "_crop", rszr_image__crop, 5);
496
501
  rb_define_private_method(cImage, "_turn!", rszr_image__turn_bang, 1);
497
- rb_define_private_method(cImage, "_rotate", rszr_image__rotate, 2);
498
- rb_define_private_method(cImage, "_sharpen!", rszr_image__sharpen_bang, 1);
499
- rb_define_private_method(cImage, "_brighten!", rszr_image__brighten_bang, 1);
502
+ rb_define_private_method(cImage, "_rotate", rszr_image__rotate, 2);
503
+ rb_define_private_method(cImage, "_sharpen!", rszr_image__sharpen_bang, 1);
504
+ rb_define_private_method(cImage, "_pixel", rszr_image__pixel_get, 2);
500
505
 
501
506
  rb_define_private_method(cImage, "_save", rszr_image__save, 3);
502
507
  }
data/ext/rszr/rszr.h CHANGED
@@ -3,7 +3,6 @@
3
3
 
4
4
  #include "ruby.h"
5
5
  #include <Imlib2.h>
6
- #include <libexif/exif-data.h>
7
6
 
8
7
  extern VALUE mRszr;
9
8
  void Init_rszr();
@@ -0,0 +1,25 @@
1
+ module Rszr
2
+ module Buffered
3
+ def self.included(base)
4
+ base.extend Buffered
5
+ end
6
+
7
+ private
8
+
9
+ def with_tempfile(format, data = nil)
10
+ raise ArgumentError, 'format is required' unless format
11
+ result = nil
12
+ Tempfile.create(['rszr-buffer', ".#{format}"], encoding: 'BINARY') do |file|
13
+ if data
14
+ file.binmode
15
+ file << data
16
+ file.fsync
17
+ file.rewind
18
+ end
19
+ result = yield(file)
20
+ end
21
+ result
22
+ end
23
+
24
+ end
25
+ end
data/lib/rszr/color.rb ADDED
@@ -0,0 +1,25 @@
1
+ module Rszr
2
+ module Color
3
+
4
+ class RGBA
5
+ attr_reader :red, :green, :blue, :alpha
6
+
7
+ def initialize(red, green, blue, alpha = 255)
8
+ if red < 0 || red > 255 || green < 0 || green > 255 || blue < 0 || blue > 255 || alpha < 0 || alpha > 255
9
+ raise ArgumentError, 'color out of range'
10
+ end
11
+ @red, @green, @blue, @alpha = red, green, blue, alpha
12
+ end
13
+
14
+ def to_i(rgb: false)
15
+ i = red.to_i << 24 | green.to_i << 16 | blue.to_i << 8 | alpha.to_i
16
+ rgb ? i >> 8 : i
17
+ end
18
+
19
+ def to_hex(rgb: false)
20
+ "%0#{rgb ? 6 : 8}x" % to_i(rgb: rgb)
21
+ end
22
+ end
23
+
24
+ end
25
+ end
@@ -0,0 +1,60 @@
1
+ # Type reader adapted from fastimage
2
+ # https://github.com/sdsykes/fastimage/
3
+
4
+ module Rszr
5
+ module Identification
6
+
7
+ private
8
+
9
+ def identify(data)
10
+ case data[0, 2]
11
+ when 'BM'
12
+ :bmp
13
+ when 'GI'
14
+ :gif
15
+ when 0xff.chr + 0xd8.chr
16
+ :jpeg
17
+ when 0x89.chr + 'P'
18
+ :png
19
+ when 'II', 'MM'
20
+ case data[0, 11][8..10] # @stream.peek(11)[8..10]
21
+ when 'APC', "CR\002"
22
+ nil # do not recognise CRW or CR2 as tiff
23
+ else
24
+ :tiff
25
+ end
26
+ when '8B'
27
+ :psd
28
+ when "\0\0"
29
+ case data[0, 3].bytes.last #@stream.peek(3).bytes.to_a.last
30
+ when 0
31
+ # http://www.ftyps.com/what.html
32
+ # HEIC is composed of nested "boxes". Each box has a header composed of
33
+ # - Size (32 bit integer)
34
+ # - Box type (4 chars)
35
+ # - Extended size: only if size === 1, the type field is followed by 64 bit integer of extended size
36
+ # - Payload: Type-dependent
37
+ case data[0, 12][4..-1] #@stream.peek(12)[4..-1]
38
+ when 'ftypheic'
39
+ :heic
40
+ when 'ftypmif1'
41
+ :heif
42
+ end
43
+ # ico has either a 1 (for ico format) or 2 (for cursor) at offset 3
44
+ when 1 then :ico
45
+ when 2 then :cur
46
+ end
47
+ when 'RI'
48
+ :webp if data[0, 12][8..11] == 'WEBP' #@stream.peek(12)[8..11] == "WEBP"
49
+ when "<s"
50
+ :svg if data[0, 4] == '<svg' #@stream.peek(4) == "<svg"
51
+ when /\s\s|\s<|<[?!]/, 0xef.chr + 0xbb.chr
52
+ # Peek 10 more chars each time, and if end of file is reached just raise
53
+ # unknown. We assume the <svg tag cannot be within 10 chars of the end of
54
+ # the file, and is within the first 250 chars.
55
+ :svg if (1..25).detect { |n| data[0, 10 * n]&.include?('<svg') }
56
+ end
57
+ end
58
+
59
+ end
60
+ end
data/lib/rszr/image.rb CHANGED
@@ -1,25 +1,50 @@
1
1
  module Rszr
2
2
  class Image
3
+ GRAVITIES = [true, :center, :n, :nw, :w, :sw, :s, :se, :e, :ne].freeze
4
+
5
+ extend Identification
6
+ include Buffered
7
+ include Orientation
3
8
 
4
9
  class << self
5
-
10
+
6
11
  def load(path, autorotate: Rszr.autorotate, **opts)
7
12
  path = path.to_s
8
13
  raise FileNotFound unless File.exist?(path)
9
- _load(path, autorotate)
14
+ image = _load(path)
15
+ autorotate(image, path) if autorotate
16
+ image
10
17
  end
11
18
  alias :open :load
12
19
 
20
+ def load_data(data, autorotate: Rszr.autorotate, **opts)
21
+ raise LoadError, 'Unknown format' unless format = identify(data)
22
+ with_tempfile(format, data) do |file|
23
+ load(file.path, autorotate: autorotate, **opts)
24
+ end
25
+ end
26
+
13
27
  end
14
28
 
15
29
  def dimensions
16
30
  [width, height]
17
31
  end
18
32
 
33
+ def format
34
+ fmt = _format
35
+ fmt == 'jpg' ? 'jpeg' : fmt
36
+ end
37
+
19
38
  def format=(fmt)
20
39
  fmt = fmt.to_s if fmt.is_a?(Symbol)
21
40
  self._format = fmt
22
41
  end
42
+
43
+ def [](x, y)
44
+ if x >= 0 && x <= width - 1 && y >= 0 && y <= height - 1
45
+ Color::RGBA.new(*_pixel(x, y))
46
+ end
47
+ end
23
48
 
24
49
  def inspect
25
50
  fmt = format
@@ -28,12 +53,12 @@ module Rszr
28
53
  end
29
54
 
30
55
  module Transformations
31
- def resize(*args)
32
- _resize(false, *calculate_size(*args))
56
+ def resize(*args, **opts)
57
+ _resize(false, *calculate_size(*args, **opts))
33
58
  end
34
59
 
35
- def resize!(*args)
36
- _resize(true, *calculate_size(*args))
60
+ def resize!(*args, **opts)
61
+ _resize(true, *calculate_size(*args, **opts))
37
62
  end
38
63
 
39
64
  def crop(x, y, width, height)
@@ -60,6 +85,16 @@ module Rszr
60
85
  def rotate!(deg)
61
86
  _rotate(true, deg.to_f * Math::PI / 180.0)
62
87
  end
88
+
89
+ # horizontal
90
+ def flop
91
+ dup.flop!
92
+ end
93
+
94
+ # vertical
95
+ def flip
96
+ dup.flip!
97
+ end
63
98
 
64
99
  def sharpen(radius)
65
100
  dup.sharpen!(radius)
@@ -88,8 +123,8 @@ module Rszr
88
123
  filter!("colormod(brightness=#{value.to_f});")
89
124
  end
90
125
 
91
- def brighten(*args)
92
- dup.brighten!(*args)
126
+ def brighten(*args, **opts)
127
+ dup.brighten!(*args, **opts)
93
128
  end
94
129
 
95
130
  def contrast!(value, r: nil, g: nil, b: nil, a: nil)
@@ -97,8 +132,8 @@ module Rszr
97
132
  filter!("colormod(contrast=#{value.to_f});")
98
133
  end
99
134
 
100
- def contrast(*args)
101
- dup.contrast!(*args)
135
+ def contrast(*args, **opts)
136
+ dup.contrast!(*args, **opts)
102
137
  end
103
138
 
104
139
  def gamma!(value, r: nil, g: nil, b: nil, a: nil)
@@ -106,8 +141,8 @@ module Rszr
106
141
  filter!("colormod(gamma=#{value.to_f});")
107
142
  end
108
143
 
109
- def gamma(*args)
110
- dup.gamma!(*args)
144
+ def gamma(*args, **opts)
145
+ dup.gamma!(*args, **opts)
111
146
  end
112
147
  end
113
148
 
@@ -116,8 +151,18 @@ module Rszr
116
151
  def save(path, format: nil, quality: nil)
117
152
  format ||= format_from_filename(path) || self.format || 'jpg'
118
153
  raise ArgumentError, "invalid quality #{quality.inspect}" if quality && !(0..100).cover?(quality)
154
+ ensure_path_is_writable(path)
119
155
  _save(path.to_s, format.to_s, quality)
120
156
  end
157
+
158
+ def save_data(format: nil, quality: nil)
159
+ format ||= self.format || 'jpg'
160
+ with_tempfile(format) do |file|
161
+ save(file.path, format: format, quality: quality)
162
+ file.rewind
163
+ file.read
164
+ end
165
+ end
121
166
 
122
167
  private
123
168
 
@@ -129,52 +174,114 @@ module Rszr
129
174
  # 400, 300, background: rgba
130
175
  # 400, 300, skew: true
131
176
 
132
- def calculate_size(*args)
133
- options = args.last.is_a?(Hash) ? args.pop : {}
134
- assert_valid_keys options, :crop, :background, :skew #:extend, :width, :height, :max_width, :max_height, :box
135
- original_width, original_height = width, height
136
- x, y, = 0, 0
177
+ def calculate_size(*args, crop: nil, skew: nil, inflate: true)
178
+ #options = args.last.is_a?(Hash) ? args.pop : {}
179
+ #assert_valid_keys options, :crop, :background, :skew #:extend, :width, :height, :max_width, :max_height, :box
137
180
  if args.size == 1
138
- scale = args.first
139
- raise ArgumentError, "scale factor #{scale.inspect} out of range" unless scale > 0 && scale < 1
140
- new_width = original_width.to_f * scale
141
- new_height = original_height.to_f * scale
181
+ calculate_size_for_scale(args.first)
142
182
  elsif args.size == 2
143
183
  box_width, box_height = args
144
- if :auto == box_width && box_height.is_a?(Numeric)
145
- new_height = box_height
146
- new_width = box_height.to_f / original_height.to_f * original_width.to_f
147
- elsif box_width.is_a?(Numeric) && :auto == box_height
148
- new_width = box_width
149
- new_height = box_width.to_f / original_width.to_f * original_height.to_f
184
+ if args.include?(:auto)
185
+ calculate_size_for_auto(box_width, box_height)
150
186
  elsif box_width.is_a?(Numeric) && box_height.is_a?(Numeric)
151
- if options[:skew]
152
- new_width, new_height = box_width, box_height
153
- elsif options[:crop]
154
- # TODO: calculate x, y offset if crop
187
+ if not inflate and width <= box_width and height <= box_height
188
+ [0, 0, width, height, width, height]
189
+ elsif skew
190
+ calculate_size_for_skew(box_width, box_height)
191
+ elsif crop
192
+ calculate_size_for_crop(box_width, box_height, crop)
155
193
  else
156
- scale = original_width.to_f / original_height.to_f
157
- box_scale = box_width.to_f / box_height.to_f
158
- if scale >= box_scale # wider
159
- new_width = box_width
160
- new_height = original_height.to_f * box_width.to_f / original_width.to_f
161
- else # narrower
162
- new_height = box_height
163
- new_width = original_width.to_f * box_height.to_f / original_height.to_f
164
- end
194
+ calculate_size_for_limit(box_width, box_height)
165
195
  end
166
- else
167
- raise ArgumentError, "unconclusive arguments #{args.inspect} #{options.inspect}"
168
196
  end
169
197
  else
170
198
  raise ArgumentError, "wrong number of arguments (#{args.size} for 1..2)"
171
199
  end
172
- [x, y, original_width, original_height, new_width.round, new_height.round]
200
+ end
201
+
202
+ def calculate_size_for_scale(factor)
203
+ raise ArgumentError, "scale factor #{factor.inspect} out of range" unless factor > 0 && factor < 1
204
+ [0, 0, width, height, (width.to_f * factor).round, (height.to_f * factor).round]
205
+ end
206
+
207
+ def calculate_size_for_skew(box_width, box_height)
208
+ [0, 0, width, height, box_width, box_height]
209
+ end
210
+
211
+ def calculate_size_for_auto(box_width, box_height)
212
+ if :auto == box_width && box_height.is_a?(Numeric)
213
+ new_height = box_height
214
+ new_width = (box_height.to_f / height.to_f * width.to_f).round
215
+ elsif box_width.is_a?(Numeric) && :auto == box_height
216
+ new_width = box_width
217
+ new_height = (box_width.to_f / width.to_f * height.to_f).round
218
+ else
219
+ raise ArgumentError, "unconclusive arguments #{box_width.inspect}, #{box_height.inspect}"
220
+ end
221
+ [0, 0, width, height, new_width, new_height]
222
+ end
223
+
224
+ def calculate_size_for_crop(box_width, box_height, crop)
225
+ raise ArgumentError, "invalid crop gravity" unless GRAVITIES.include?(crop)
226
+ aspect = width.to_f / height.to_f
227
+ box_aspect = box_width.to_f / box_height.to_f
228
+ if aspect >= box_aspect # wider than box
229
+ src_width = (box_width.to_f * height.to_f / box_height.to_f).round
230
+ src_height = height
231
+ x = crop_horizontally(src_width, crop)
232
+ y = 0
233
+ else # narrower than box
234
+ src_width = width
235
+ src_height = (box_height.to_f * width.to_f / box_width.to_f).round
236
+ x = 0
237
+ y = crop_vertically(src_height, crop)
238
+ end
239
+ [x, y, src_width, src_height, box_width, box_height]
240
+ end
241
+
242
+ def crop_horizontally(src_width, crop)
243
+ case crop
244
+ when :nw, :w, :sw then 0
245
+ when :ne, :e, :se then width - src_width
246
+ else
247
+ ((width - src_width).to_f / 2.to_f).round
248
+ end
249
+ end
250
+
251
+ def crop_vertically(src_height, crop)
252
+ case crop
253
+ when :nw, :n, :ne then 0
254
+ when :sw, :s, :se then height - src_height
255
+ else
256
+ ((height - src_height).to_f / 2.to_f).round
257
+ end
258
+ end
259
+
260
+ def calculate_size_for_limit(box_width, box_height)
261
+ scale = width.to_f / height.to_f
262
+ box_scale = box_width.to_f / box_height.to_f
263
+ if scale >= box_scale # wider
264
+ new_width = box_width
265
+ new_height = (height.to_f * box_width.to_f / width.to_f).round
266
+ else # narrower
267
+ new_height = box_height
268
+ new_width = (width.to_f * box_height.to_f / height.to_f).round
269
+ end
270
+ [0, 0, width, height, new_width, new_height]
173
271
  end
174
272
 
175
273
  def format_from_filename(path)
176
274
  File.extname(path)[1..-1].to_s.downcase
177
275
  end
276
+
277
+ def ensure_path_is_writable(path)
278
+ path = Pathname.new(path)
279
+ path.dirname.realpath.writable?
280
+ rescue Errno::ENOENT => e
281
+ raise SaveError, 'Non-existant path component'
282
+ rescue SystemCallError => e
283
+ raise SaveError, e.message
284
+ end
178
285
 
179
286
  def assert_valid_keys(hsh, *valid_keys)
180
287
  if unknown_key = (hsh.keys - valid_keys).first
@@ -51,7 +51,7 @@ module ImageProcessing
51
51
  # Resizes the image to not be larger than the specified dimensions.
52
52
  def resize_to_limit(width, height, **options)
53
53
  width, height = default_dimensions(width, height)
54
- thumbnail(width, height, **options)
54
+ thumbnail(width, height, inflate: false, **options)
55
55
  end
56
56
 
57
57
  # Resizes the image to fit within the specified dimensions.
@@ -62,8 +62,8 @@ module ImageProcessing
62
62
 
63
63
  # Resizes the image to fill the specified dimensions, applying any
64
64
  # necessary cropping.
65
- def resize_to_fill(width, height, **options)
66
- thumbnail(width, height, crop: :center, **options)
65
+ def resize_to_fill(width, height, gravity: :center, **options)
66
+ thumbnail(width, height, crop: gravity, **options)
67
67
  end
68
68
 
69
69
  private
@@ -0,0 +1,107 @@
1
+ module Rszr
2
+ module Orientation
3
+ ROTATIONS = { 5 => 1, 6 => 1, 3 => 2, 4 => 2, 7 => 3, 8 => 3 }
4
+
5
+ def self.included(base)
6
+ base.extend(ClassMethods)
7
+ base.attr_reader :original_orientation
8
+ end
9
+
10
+ module ClassMethods
11
+
12
+ private
13
+
14
+ def autorotate(image, path)
15
+ return unless %w[jpeg tiff].include?(image.format)
16
+ File.open(path) do |file|
17
+ if orientation = send("parse_#{image.format}_orientation", file) and (1..8).member?(orientation)
18
+ image.instance_variable_set :@original_orientation, orientation
19
+ image.flop! if [2, 4, 5, 7].include?(orientation)
20
+ image.turn!(ROTATIONS[orientation]) if ROTATIONS.key?(orientation)
21
+ end
22
+ end
23
+ end
24
+
25
+ def parse_tiff_orientation(data)
26
+ exif_parse_orientation(Stream.new(data))
27
+ end
28
+
29
+ def parse_jpeg_orientation(data)
30
+ stream = Stream.new(data)
31
+ exif = nil
32
+ state = nil
33
+ loop do
34
+ state = case state
35
+ when nil
36
+ stream.skip(2)
37
+ :started
38
+ when :started
39
+ stream.read_byte == 0xFF ? :sof : :started
40
+ when :sof
41
+ case stream.read_byte
42
+ when 0xe1 # APP1
43
+ skip_chars = stream.read_int - 2
44
+ app1 = Stream.new(stream.read(skip_chars))
45
+ if app1.read(4) == 'Exif'
46
+ app1.skip(2)
47
+ orientation = exif_parse_orientation(app1.fast_forward)# rescue nil
48
+ return orientation
49
+ end
50
+ :started
51
+ when 0xe0..0xef
52
+ :skipframe
53
+ when 0xC0..0xC3, 0xC5..0xC7, 0xC9..0xCB, 0xCD..0xCF
54
+ :readsize
55
+ when 0xFF
56
+ :sof
57
+ else
58
+ :skipframe
59
+ end
60
+ when :skipframe
61
+ skip_chars = stream.read_int - 2
62
+ stream.skip(skip_chars)
63
+ :started
64
+ when :readsize
65
+ # stream.skip(3)
66
+ # height = stream.read_int
67
+ # width = stream.read_int
68
+ return exif&.orientation
69
+ end
70
+ end
71
+ end
72
+
73
+ def exif_byte_order(stream)
74
+ byte_order = stream.read(2)
75
+ case byte_order
76
+ when 'II'
77
+ %w[v V]
78
+ when 'MM'
79
+ %w[n N]
80
+ else
81
+ raise LoadError
82
+ end
83
+ end
84
+
85
+ def exif_parse_ifd(stream, short)
86
+ tag_count = stream.read(2).unpack(short)[0]
87
+ tag_count.downto(1) do
88
+ type = stream.read(2).unpack(short)[0]
89
+ stream.read(6)
90
+ data = stream.read(2).unpack(short)[0]
91
+ return data if 0x0112 == type
92
+ stream.read(2)
93
+ end
94
+ nil
95
+ end
96
+
97
+ def exif_parse_orientation(stream)
98
+ short, long = exif_byte_order(stream)
99
+ stream.read(2) # 42
100
+ offset = stream.read(4).unpack(long)[0]
101
+ stream.skip(offset - 8)
102
+ exif_parse_ifd(stream, short)
103
+ end
104
+ end
105
+
106
+ end
107
+ end
@@ -0,0 +1,61 @@
1
+ module Rszr
2
+ class Stream
3
+ attr_reader :pos, :data
4
+ protected :data
5
+
6
+ def initialize(data, start: 0)
7
+ raise ArgumentError, 'start must be > 0' if start < 0
8
+ @data = case data
9
+ when IO then data
10
+ when String then StringIO.new(data)
11
+ when Stream then data.data
12
+ else
13
+ raise ArgumentError, "data must be File or String, got #{data.class}"
14
+ end
15
+ @data.binmode
16
+ @data.seek(start)
17
+ @pos = 0
18
+ end
19
+
20
+ def read(n)
21
+ @data.read(n).tap { @pos += n }
22
+ end
23
+
24
+ def peek(n)
25
+ old_pos = @data.pos
26
+ @data.read(n)
27
+ ensure
28
+ @data.pos = old_pos
29
+ end
30
+
31
+ def skip(n)
32
+ @data.seek(n, IO::SEEK_CUR).tap { @pos += n }
33
+ end
34
+
35
+ def substream
36
+ self.class.new(self, @data.pos)
37
+ end
38
+
39
+ def fast_forward
40
+ @pos = 0
41
+ self
42
+ end
43
+
44
+ def read_byte
45
+ read(1)[0].ord
46
+ end
47
+
48
+ def read_int
49
+ read(2).unpack('n')[0]
50
+ end
51
+
52
+ def read_string_int
53
+ value = []
54
+ while read(1) =~ /(\d)/
55
+ value << $1
56
+ end
57
+ value.join.to_i
58
+ end
59
+
60
+ end
61
+ end
data/lib/rszr/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Rszr
2
- VERSION = '0.7.1'
2
+ VERSION = '1.1.0'
3
3
  end
data/lib/rszr.rb CHANGED
@@ -1,8 +1,15 @@
1
1
  require 'rbconfig'
2
2
  require 'pathname'
3
+ require 'tempfile'
4
+ require 'stringio'
3
5
 
4
6
  require 'rszr/rszr'
5
7
  require 'rszr/version'
8
+ require 'rszr/stream'
9
+ require 'rszr/identification'
10
+ require 'rszr/orientation'
11
+ require 'rszr/buffered'
12
+ require 'rszr/color'
6
13
  require 'rszr/image'
7
14
 
8
15
  module Rszr
metadata CHANGED
@@ -1,183 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rszr
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.1
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matthias Grosser
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-05-10 00:00:00.000000000 Z
12
- dependencies:
13
- - !ruby/object:Gem::Dependency
14
- name: bundler
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - ">="
18
- - !ruby/object:Gem::Version
19
- version: '0'
20
- type: :development
21
- prerelease: false
22
- version_requirements: !ruby/object:Gem::Requirement
23
- requirements:
24
- - - ">="
25
- - !ruby/object:Gem::Version
26
- version: '0'
27
- - !ruby/object:Gem::Dependency
28
- name: rake
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - "~>"
32
- - !ruby/object:Gem::Version
33
- version: '13.0'
34
- type: :development
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - "~>"
39
- - !ruby/object:Gem::Version
40
- version: '13.0'
41
- - !ruby/object:Gem::Dependency
42
- name: rake-compiler
43
- requirement: !ruby/object:Gem::Requirement
44
- requirements:
45
- - - ">="
46
- - !ruby/object:Gem::Version
47
- version: '0'
48
- type: :development
49
- prerelease: false
50
- version_requirements: !ruby/object:Gem::Requirement
51
- requirements:
52
- - - ">="
53
- - !ruby/object:Gem::Version
54
- version: '0'
55
- - !ruby/object:Gem::Dependency
56
- name: byebug
57
- requirement: !ruby/object:Gem::Requirement
58
- requirements:
59
- - - ">="
60
- - !ruby/object:Gem::Version
61
- version: '0'
62
- type: :development
63
- prerelease: false
64
- version_requirements: !ruby/object:Gem::Requirement
65
- requirements:
66
- - - ">="
67
- - !ruby/object:Gem::Version
68
- version: '0'
69
- - !ruby/object:Gem::Dependency
70
- name: rspec
71
- requirement: !ruby/object:Gem::Requirement
72
- requirements:
73
- - - ">="
74
- - !ruby/object:Gem::Version
75
- version: '0'
76
- type: :development
77
- prerelease: false
78
- version_requirements: !ruby/object:Gem::Requirement
79
- requirements:
80
- - - ">="
81
- - !ruby/object:Gem::Version
82
- version: '0'
83
- - !ruby/object:Gem::Dependency
84
- name: minitest
85
- requirement: !ruby/object:Gem::Requirement
86
- requirements:
87
- - - ">="
88
- - !ruby/object:Gem::Version
89
- version: '0'
90
- type: :development
91
- prerelease: false
92
- version_requirements: !ruby/object:Gem::Requirement
93
- requirements:
94
- - - ">="
95
- - !ruby/object:Gem::Version
96
- version: '0'
97
- - !ruby/object:Gem::Dependency
98
- name: simplecov
99
- requirement: !ruby/object:Gem::Requirement
100
- requirements:
101
- - - ">="
102
- - !ruby/object:Gem::Version
103
- version: '0'
104
- type: :development
105
- prerelease: false
106
- version_requirements: !ruby/object:Gem::Requirement
107
- requirements:
108
- - - ">="
109
- - !ruby/object:Gem::Version
110
- version: '0'
111
- - !ruby/object:Gem::Dependency
112
- name: image_processing
113
- requirement: !ruby/object:Gem::Requirement
114
- requirements:
115
- - - ">="
116
- - !ruby/object:Gem::Version
117
- version: '0'
118
- type: :development
119
- prerelease: false
120
- version_requirements: !ruby/object:Gem::Requirement
121
- requirements:
122
- - - ">="
123
- - !ruby/object:Gem::Version
124
- version: '0'
125
- - !ruby/object:Gem::Dependency
126
- name: gd2-ffij
127
- requirement: !ruby/object:Gem::Requirement
128
- requirements:
129
- - - ">="
130
- - !ruby/object:Gem::Version
131
- version: '0'
132
- type: :development
133
- prerelease: false
134
- version_requirements: !ruby/object:Gem::Requirement
135
- requirements:
136
- - - ">="
137
- - !ruby/object:Gem::Version
138
- version: '0'
139
- - !ruby/object:Gem::Dependency
140
- name: mini_magick
141
- requirement: !ruby/object:Gem::Requirement
142
- requirements:
143
- - - ">="
144
- - !ruby/object:Gem::Version
145
- version: '0'
146
- type: :development
147
- prerelease: false
148
- version_requirements: !ruby/object:Gem::Requirement
149
- requirements:
150
- - - ">="
151
- - !ruby/object:Gem::Version
152
- version: '0'
153
- - !ruby/object:Gem::Dependency
154
- name: ruby-vips
155
- requirement: !ruby/object:Gem::Requirement
156
- requirements:
157
- - - ">="
158
- - !ruby/object:Gem::Version
159
- version: '0'
160
- type: :development
161
- prerelease: false
162
- version_requirements: !ruby/object:Gem::Requirement
163
- requirements:
164
- - - ">="
165
- - !ruby/object:Gem::Version
166
- version: '0'
167
- - !ruby/object:Gem::Dependency
168
- name: memory_profiler
169
- requirement: !ruby/object:Gem::Requirement
170
- requirements:
171
- - - ">="
172
- - !ruby/object:Gem::Version
173
- version: '0'
174
- type: :development
175
- prerelease: false
176
- version_requirements: !ruby/object:Gem::Requirement
177
- requirements:
178
- - - ">="
179
- - !ruby/object:Gem::Version
180
- version: '0'
11
+ date: 2022-02-09 00:00:00.000000000 Z
12
+ dependencies: []
181
13
  description: Fast image resizer - do one thing and do it fast.
182
14
  email:
183
15
  - mtgrosser@gmx.net
@@ -198,8 +30,13 @@ files:
198
30
  - ext/rszr/rszr.h
199
31
  - lib/rszr.rb
200
32
  - lib/rszr/batch_transformation.rb
33
+ - lib/rszr/buffered.rb
34
+ - lib/rszr/color.rb
35
+ - lib/rszr/identification.rb
201
36
  - lib/rszr/image.rb
202
37
  - lib/rszr/image_processing.rb
38
+ - lib/rszr/orientation.rb
39
+ - lib/rszr/stream.rb
203
40
  - lib/rszr/version.rb
204
41
  homepage: https://github.com/mtgrosser/rszr
205
42
  licenses:
@@ -222,7 +59,6 @@ required_rubygems_version: !ruby/object:Gem::Requirement
222
59
  version: '0'
223
60
  requirements:
224
61
  - imlib2
225
- - libexif
226
62
  rubygems_version: 3.1.4
227
63
  signing_key:
228
64
  specification_version: 4