rszr 0.5.3 → 1.0.1

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: 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: []