rszr 0.7.1 → 1.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 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