rszr 0.5.3 → 1.0.1

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: d7322db19722cd5afc93e887fabe4031fa3a3064949956dc3bac562299918f10
4
- data.tar.gz: 8c03c8922f29d3f7989f427018e7baeb6f09c4c9f561fef1b6a92a80f4042ec8
3
+ metadata.gz: 51f96b1754949daff45bf2d18b32d2857d5b82f33c285b85bbaf5fdb25d12f0b
4
+ data.tar.gz: 956217311a71053dcf0a781afa306b4f27c49ff4b00487e7d470f9290b91a0c4
5
5
  SHA512:
6
- metadata.gz: 82b7c3ea10061666bacd712c39319ef36f19b2aecce3fa89556c591c0ef6f2cedf10b0fb1ed4ee6ac8613cfb40904b7e42306cd08d247d2653ca3135450244ba
7
- data.tar.gz: 52cac4fe43b0feb24e6fb837fdbed69bb4c91e53807ef5d5821c5ab1793d41a698632e1b342e8b14fed0219db080d05179d37eeb6fcef91e8052b66205c2ab07
6
+ metadata.gz: 5fd6b4f8cf30f5f0a811b802aed1ab6dcfd709bdde3c803689d700276f6c0a996d5a93ecaee2587679097cb6faddea689296788c773215ff19ac4033191ee2ea
7
+ data.tar.gz: 7fc2f31485281ed5ccfc2b7c679fbda4cb03baa261aee18e661331dbc71adb5f98db8015aae46bbea6f27f016bd2f641ca7e46b12f6d5cebe975e09240b25d0b
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.
@@ -14,7 +14,7 @@ gem 'rszr'
14
14
 
15
15
  ### Imlib2
16
16
 
17
- Rszr requires the Imlib2 library to do the heavy lifting.
17
+ Rszr requires the `Imlib2` library to do the heavy lifting.
18
18
 
19
19
  #### OS X
20
20
 
@@ -28,10 +28,14 @@ brew install imlib2
28
28
 
29
29
  Using your favourite package manager:
30
30
 
31
+ ##### RedHat-based
32
+
31
33
  ```bash
32
34
  yum install imlib2 imlib2-devel
33
35
  ```
34
36
 
37
+ ##### Debian-based
38
+
35
39
  ```bash
36
40
  apt-get install libimlib2 libimlib2-dev
37
41
  ```
@@ -50,12 +54,25 @@ image.save('resized.jpg')
50
54
  image.save('resized.png')
51
55
  ```
52
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
+
53
68
  ### Transformations
54
69
 
55
70
  For each transformation, there is a bang! and non-bang method.
56
71
  The bang method changes the image in place, while the non-bang method
57
72
  creates a copy of the image in memory.
58
73
 
74
+ #### Resizing
75
+
59
76
  ```ruby
60
77
  # auto height
61
78
  image.resize(400, :auto)
@@ -66,6 +83,22 @@ image.resize(:auto, 300)
66
83
  # scale factor
67
84
  image.resize(0.5)
68
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
69
102
  # crop
70
103
  image.crop(200, 200, 100, 100)
71
104
 
@@ -78,35 +111,65 @@ image.turn!(-1)
78
111
  # rotate by arbitrary angle
79
112
  image.rotate(45)
80
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
81
129
  # sharpen image by pixel radius
82
130
  image.sharpen!(1)
83
131
 
84
132
  # blur image by pixel radius
85
133
  image.blur!(1)
86
134
 
87
- # initialize copy
88
- image.dup
135
+ # brighten
136
+ image.brighten(0.1)
89
137
 
90
- # save memory, do not duplicate instance
91
- image.resize!(400, :auto)
138
+ # darken
139
+ image.brighten(-0.1)
140
+
141
+ # contrast
142
+ image.contrast(0.5)
143
+
144
+ # gamma
145
+ image.gamma(1.1)
92
146
  ```
93
147
 
94
- ### Image info
148
+ ### Image auto orientation
149
+
150
+ Auto-rotation is supported for JPEG and TIFF files that include the necessary
151
+ EXIF metadata.
152
+
95
153
  ```ruby
96
- image.width => 400
97
- image.height => 300
98
- image.dimensions => [400, 300]
99
- image.format => "jpeg"
154
+ # load and autorotate
155
+ image = Rszr::Image.load('image.jpg', autorotate: true)
156
+ ```
157
+
158
+ To enable autorotation by default:
159
+
160
+ ```ruby
161
+ # auto-rotate by default, for Rails apps put this into an initializer
162
+ Rszr.autorotate = true
100
163
  ```
101
164
 
102
165
  ## Rails / ActiveStorage interface
103
166
 
104
- Rszr provides a drop-in interface to the `image_resizing` gem.
167
+ Rszr provides a drop-in interface to the `image_processing` gem.
105
168
  It is faster than both `mini_magick` and `vips` and way easier to install than the latter.
106
169
 
107
170
  ```ruby
108
171
  # Gemfile
109
- gem 'image_resizing'
172
+ gem 'image_processing'
110
173
  gem 'rszr'
111
174
 
112
175
  # config/initializers/rszr.rb
@@ -122,6 +185,19 @@ When creating image variants, you can use all of Rszr's transformation methods:
122
185
  <%= image_tag user.avatar.variant(resize_to_fit: [300, 200]) %>
123
186
  ```
124
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
+
125
201
  ## Thread safety
126
202
 
127
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
- error_index = 13;
48
- VALUE rb_error = rb_exc_new2(rb_error_class, sRszrErrorMessages[error_index]);
51
+ error_index = RSZR_MAX_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/image.c CHANGED
@@ -27,7 +27,6 @@ static void rszr_image_deallocate(rszr_image_handle * handle)
27
27
  // fprintf(stderr, "\n");
28
28
  }
29
29
 
30
-
31
30
  static VALUE rszr_image_s_allocate(VALUE klass)
32
31
  {
33
32
  rszr_image_handle * handle = calloc(1, sizeof(rszr_image_handle));
@@ -61,13 +60,20 @@ static VALUE rszr_image_s__load(VALUE klass, VALUE rb_path)
61
60
  path = StringValueCStr(rb_path);
62
61
 
63
62
  imlib_set_cache_size(0);
64
- image = imlib_load_image_with_error_return(path, &error);
63
+ image = imlib_load_image_without_cache(path);
65
64
 
66
65
  if (!image) {
67
- rszr_raise_load_error(error);
68
- 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
+ }
69
72
  }
70
73
 
74
+ imlib_context_set_image(image);
75
+ imlib_image_set_irrelevant_format(0);
76
+
71
77
  oImage = rszr_image_s_allocate(cImage);
72
78
  Data_Get_Struct(oImage, rszr_image_handle, handle);
73
79
  handle->image = image;
@@ -75,7 +81,7 @@ static VALUE rszr_image_s__load(VALUE klass, VALUE rb_path)
75
81
  }
76
82
 
77
83
 
78
- static VALUE rszr_image_format_get(VALUE self)
84
+ static VALUE rszr_image__format_get(VALUE self)
79
85
  {
80
86
  rszr_image_handle * handle;
81
87
  char * format;
@@ -134,6 +140,32 @@ static VALUE rszr_image_height(VALUE self)
134
140
  return INT2NUM(height);
135
141
  }
136
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
+
137
169
  /*
138
170
  static VALUE rszr_image_get_quality(VALUE self)
139
171
  {
@@ -211,6 +243,32 @@ static VALUE rszr_image__turn_bang(VALUE self, VALUE orientation)
211
243
  }
212
244
 
213
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
+
214
272
  static VALUE rszr_image__rotate(VALUE self, VALUE bang, VALUE rb_angle)
215
273
  {
216
274
  rszr_image_handle * handle;
@@ -246,20 +304,22 @@ static VALUE rszr_image__rotate(VALUE self, VALUE bang, VALUE rb_angle)
246
304
  }
247
305
  }
248
306
 
249
- /*
250
- static VALUE rszr_image__brighten_bang(VALUE self, VALUE rb_brightness)
307
+
308
+ static VALUE rszr_image_filter_bang(VALUE self, VALUE rb_filter_expr)
251
309
  {
252
310
  rszr_image_handle * handle;
253
- double brightness;
311
+ char * filter_expr;
254
312
 
255
- brightness = NUM2DBL(rb_brightness);
313
+ filter_expr = StringValueCStr(rb_filter_expr);
314
+
315
+ Data_Get_Struct(self, rszr_image_handle, handle);
256
316
 
257
317
  imlib_context_set_image(handle->image);
258
- imlib_modify_color_modifier_brightness(brightness);
318
+ imlib_apply_filter(filter_expr);
259
319
 
260
320
  return self;
261
321
  }
262
- */
322
+
263
323
 
264
324
  static VALUE rszr_image__sharpen_bang(VALUE self, VALUE rb_radius)
265
325
  {
@@ -275,7 +335,7 @@ static VALUE rszr_image__sharpen_bang(VALUE self, VALUE rb_radius)
275
335
  if (radius >= 0) {
276
336
  imlib_image_sharpen(radius);
277
337
  } else {
278
- imlib_image_blur(radius);
338
+ imlib_image_blur(-radius);
279
339
  }
280
340
 
281
341
  return self;
@@ -425,19 +485,23 @@ void Init_rszr_image()
425
485
  rb_define_method(cImage, "initialize", rszr_image_initialize, 2);
426
486
  rb_define_method(cImage, "width", rszr_image_width, 0);
427
487
  rb_define_method(cImage, "height", rszr_image_height, 0);
428
- rb_define_method(cImage, "format", rszr_image_format_get, 0);
429
488
  rb_define_method(cImage, "dup", rszr_image_dup, 0);
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);
492
+
430
493
  // rb_define_method(cImage, "quality", rszr_image_get_quality, 0);
431
494
  // rb_define_method(cImage, "quality=", rszr_image_set_quality, 1);
432
495
 
496
+ rb_define_protected_method(cImage, "_format", rszr_image__format_get, 0);
433
497
  rb_define_protected_method(cImage, "_format=", rszr_image__format_set, 1);
434
498
 
435
499
  rb_define_private_method(cImage, "_resize", rszr_image__resize, 7);
436
500
  rb_define_private_method(cImage, "_crop", rszr_image__crop, 5);
437
501
  rb_define_private_method(cImage, "_turn!", rszr_image__turn_bang, 1);
438
- rb_define_private_method(cImage, "_rotate", rszr_image__rotate, 2);
439
- rb_define_private_method(cImage, "_sharpen!", rszr_image__sharpen_bang, 1);
440
- /* 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);
441
505
 
442
506
  rb_define_private_method(cImage, "_save", rszr_image__save, 3);
443
507
  }
@@ -0,0 +1,24 @@
1
+ module Rszr
2
+ class BatchTransformation
3
+ attr_reader :transformations, :image
4
+
5
+ def initialize(path, **opts)
6
+ puts "INITIALIZED BATCH for #{path}"
7
+ @image = path.is_a?(Image) ? path : Image.load(path, **opts)
8
+ @transformations = []
9
+ end
10
+
11
+ Image::Transformations.instance_methods.grep(/\w\z/) do |method|
12
+ define_method method do |*args|
13
+ transformations << [method, args]
14
+ self
15
+ end
16
+ end
17
+
18
+ def call
19
+ transformations.each { |method, args| image.public_send("#{method}!", *args) }
20
+ image
21
+ end
22
+
23
+ end
24
+ end
@@ -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
-
3
+ GRAVITIES = [true, :center, :n, :nw, :w, :sw, :s, :se, :e, :ne].freeze
4
+
5
+ extend Identification
6
+ include Buffered
7
+ include Orientation
8
+
4
9
  class << self
5
-
6
- def load(path, **opts)
10
+
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)
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)
@@ -43,7 +68,7 @@ module Rszr
43
68
  def crop!(x, y, width, height)
44
69
  _crop(true, x, y, width, height)
45
70
  end
46
-
71
+
47
72
  def turn(orientation)
48
73
  dup.turn!(orientation)
49
74
  end
@@ -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)
@@ -79,11 +114,36 @@ module Rszr
79
114
  _sharpen!(-radius)
80
115
  end
81
116
 
82
- # TODO
83
- #def brighten!(brightness)
84
- # raise ArgumentError, 'illegal brightness' if brightness > 1 || brightness < -1
85
- # _brighten!(brightness)
86
- #end
117
+ def filter(filter_expr)
118
+ dup.filter!(filter_expr)
119
+ end
120
+
121
+ def brighten!(value, r: nil, g: nil, b: nil, a: nil)
122
+ raise ArgumentError, 'illegal brightness' if value > 1 || value < -1
123
+ filter!("colormod(brightness=#{value.to_f});")
124
+ end
125
+
126
+ def brighten(*args, **opts)
127
+ dup.brighten!(*args, **opts)
128
+ end
129
+
130
+ def contrast!(value, r: nil, g: nil, b: nil, a: nil)
131
+ raise ArgumentError, 'illegal contrast (must be > 0)' if value < 0
132
+ filter!("colormod(contrast=#{value.to_f});")
133
+ end
134
+
135
+ def contrast(*args, **opts)
136
+ dup.contrast!(*args, **opts)
137
+ end
138
+
139
+ def gamma!(value, r: nil, g: nil, b: nil, a: nil)
140
+ #raise ArgumentError, 'illegal gamma (must be > 0)' if value < 0
141
+ filter!("colormod(gamma=#{value.to_f});")
142
+ end
143
+
144
+ def gamma(*args, **opts)
145
+ dup.gamma!(*args, **opts)
146
+ end
87
147
  end
88
148
 
89
149
  include Transformations
@@ -94,6 +154,15 @@ module Rszr
94
154
  ensure_path_is_writable(path)
95
155
  _save(path.to_s, format.to_s, quality)
96
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
97
166
 
98
167
  private
99
168
 
@@ -105,51 +174,104 @@ module Rszr
105
174
  # 400, 300, background: rgba
106
175
  # 400, 300, skew: true
107
176
 
108
- def calculate_size(*args)
109
- options = args.last.is_a?(Hash) ? args.pop : {}
110
- assert_valid_keys options, :crop, :background, :skew #:extend, :width, :height, :max_width, :max_height, :box
111
- original_width, original_height = width, height
112
- 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
113
180
  if args.size == 1
114
- scale = args.first
115
- raise ArgumentError, "scale #{scale.inspect} out of range" unless scale > 0 && scale < 1
116
- new_width = original_width.to_f * scale
117
- new_height = original_height.to_f * scale
181
+ calculate_size_for_scale(args.first)
118
182
  elsif args.size == 2
119
183
  box_width, box_height = args
120
- if :auto == box_width && box_height.is_a?(Numeric)
121
- new_height = box_height
122
- new_width = box_height.to_f / original_height.to_f * original_width.to_f
123
- elsif box_width.is_a?(Numeric) && :auto == box_height
124
- new_width = box_width
125
- 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)
126
186
  elsif box_width.is_a?(Numeric) && box_height.is_a?(Numeric)
127
- if options[:skew]
128
- new_width, new_height = box_width, box_height
129
- elsif options[:crop]
130
- # 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)
131
193
  else
132
- scale = original_width.to_f / original_height.to_f
133
- box_scale = box_width.to_f / box_height.to_f
134
- if scale >= box_scale # wider
135
- new_width = box_width
136
- new_height = original_height.to_f * box_width.to_f / original_width.to_f
137
- else # narrower
138
- new_height = box_height
139
- new_width = original_width.to_f * box_height.to_f / original_height.to_f
140
- end
194
+ calculate_size_for_limit(box_width, box_height)
141
195
  end
142
- else
143
- raise ArgumentError, "unconclusive arguments #{args.inspect} #{options.inspect}"
144
196
  end
145
197
  else
146
198
  raise ArgumentError, "wrong number of arguments (#{args.size} for 1..2)"
147
199
  end
148
- [x, y, original_width, original_height, new_width.round, new_height.round]
149
200
  end
150
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]
271
+ end
272
+
151
273
  def format_from_filename(path)
152
- File.extname(path)[1..-1]
274
+ File.extname(path)[1..-1].to_s.downcase
153
275
  end
154
276
 
155
277
  def ensure_path_is_writable(path)
@@ -47,11 +47,11 @@ module ImageProcessing
47
47
  end
48
48
 
49
49
  end
50
-
50
+
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.5.3'
2
+ VERSION = '1.0.1'
3
3
  end
data/lib/rszr.rb CHANGED
@@ -1,6 +1,27 @@
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'
14
+
15
+ module Rszr
16
+ class << self
17
+ @@autorotate = nil
18
+
19
+ def autorotate
20
+ @@autorotate
21
+ end
22
+
23
+ def autorotate=(value)
24
+ @@autorotate = !!value
25
+ end
26
+ end
27
+ end
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.5.3
4
+ version: 1.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matthias Grosser
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-03-13 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: 12.3.3
34
- type: :development
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - ">="
39
- - !ruby/object:Gem::Version
40
- version: 12.3.3
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: 2021-11-10 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
@@ -197,14 +29,20 @@ files:
197
29
  - ext/rszr/rszr.c
198
30
  - ext/rszr/rszr.h
199
31
  - lib/rszr.rb
32
+ - lib/rszr/batch_transformation.rb
33
+ - lib/rszr/buffered.rb
34
+ - lib/rszr/color.rb
35
+ - lib/rszr/identification.rb
200
36
  - lib/rszr/image.rb
201
37
  - lib/rszr/image_processing.rb
38
+ - lib/rszr/orientation.rb
39
+ - lib/rszr/stream.rb
202
40
  - lib/rszr/version.rb
203
41
  homepage: https://github.com/mtgrosser/rszr
204
42
  licenses:
205
43
  - MIT
206
44
  metadata: {}
207
- post_install_message:
45
+ post_install_message:
208
46
  rdoc_options: []
209
47
  require_paths:
210
48
  - lib
@@ -220,9 +58,9 @@ required_rubygems_version: !ruby/object:Gem::Requirement
220
58
  - !ruby/object:Gem::Version
221
59
  version: '0'
222
60
  requirements:
223
- - Imlib2
61
+ - imlib2
224
62
  rubygems_version: 3.1.4
225
- signing_key:
63
+ signing_key:
226
64
  specification_version: 4
227
65
  summary: Fast image resizer
228
66
  test_files: []