rszr 0.4.0 → 0.8.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.
data/lib/rszr/image.rb CHANGED
@@ -1,97 +1,161 @@
1
1
  module Rszr
2
2
  class Image
3
- include Base
4
-
3
+ extend Identification
4
+ include Buffered
5
+ include Orientation
6
+
5
7
  class << self
6
-
7
- alias_method :instantiate, :new
8
- protected :instantiate
9
-
10
- def new(width, height)
11
- ptr = with_lock { imlib_create_image(width, height) }
12
- raise Error, 'Could not instantiate image' if ptr.null?
13
- instantiate(ptr)
14
- end
15
-
16
- def load(path, options = {})
8
+
9
+ def load(path, autorotate: Rszr.autorotate, **opts)
17
10
  path = path.to_s
18
11
  raise FileNotFound unless File.exist?(path)
19
- load_error = LoadError.new
20
- ptr = with_lock do
21
- imlib_set_cache_size(0)
22
- imlib_load_image_with_error_return(path, load_error.ptr)
23
- end
24
- raise load_error, load_error.message if ptr.null?
25
- return instantiate(ptr)
12
+ image = _load(path)
13
+ autorotate(image, path) if autorotate
14
+ image
26
15
  end
27
16
  alias :open :load
28
17
 
29
- protected
30
-
31
- def finalize(ptr)
32
- with_lock do
33
- imlib_context_set_image(ptr)
34
- imlib_free_image
18
+ def load_data(data, autorotate: Rszr.autorotate, **opts)
19
+ raise LoadError, 'Unknown format' unless format = identify(data)
20
+ with_tempfile(format, data) do |file|
21
+ load(file.path, autorotate: autorotate, **opts)
35
22
  end
36
23
  end
37
-
38
- end
39
-
40
- def initialize(ptr)
41
- @handle = Handle.new(self, ptr)
42
- end
43
-
44
- def width
45
- with_image { imlib_image_get_width }
46
- end
47
-
48
- def height
49
- with_image { imlib_image_get_height }
24
+
50
25
  end
51
-
26
+
52
27
  def dimensions
53
28
  [width, height]
54
29
  end
55
30
 
56
31
  def format
57
- str_ptr = with_image { imlib_image_format }
58
- return if str_ptr.null?
59
- str_ptr.to_s
32
+ fmt = _format
33
+ fmt == 'jpg' ? 'jpeg' : fmt
60
34
  end
61
35
 
62
- def resize(*args)
63
- instantiate(create_resized_image(*args))
36
+ def format=(fmt)
37
+ fmt = fmt.to_s if fmt.is_a?(Symbol)
38
+ self._format = fmt
64
39
  end
65
-
66
- def resize!(*args)
67
- handle.replace!(create_resized_image(*args))
68
- self
40
+
41
+ def inspect
42
+ fmt = format
43
+ fmt = " #{fmt.upcase}" if fmt
44
+ "#<#{self.class.name}:0x#{object_id.to_s(16)} #{width}x#{height}#{fmt}>"
69
45
  end
46
+
47
+ module Transformations
48
+ def resize(*args, **opts)
49
+ _resize(false, *calculate_size(*args, **opts))
50
+ end
51
+
52
+ def resize!(*args, **opts)
53
+ _resize(true, *calculate_size(*args, **opts))
54
+ end
55
+
56
+ def crop(x, y, width, height)
57
+ _crop(false, x, y, width, height)
58
+ end
59
+
60
+ def crop!(x, y, width, height)
61
+ _crop(true, x, y, width, height)
62
+ end
63
+
64
+ def turn(orientation)
65
+ dup.turn!(orientation)
66
+ end
67
+
68
+ def turn!(orientation)
69
+ orientation = orientation.abs + 2 if orientation.negative?
70
+ _turn!(orientation % 4)
71
+ end
70
72
 
71
- def crop(*args)
72
- instantiate(create_cropped_image(*args))
73
- end
73
+ def rotate(deg)
74
+ _rotate(false, deg.to_f * Math::PI / 180.0)
75
+ end
74
76
 
75
- def crop!(*args)
76
- handle.replace!(create_cropped_image(*args))
77
- self
78
- end
77
+ def rotate!(deg)
78
+ _rotate(true, deg.to_f * Math::PI / 180.0)
79
+ end
80
+
81
+ # horizontal
82
+ def flop
83
+ dup.flop!
84
+ end
85
+
86
+ # vertical
87
+ def flip
88
+ dup.flip!
89
+ end
79
90
 
80
- def save(path, format = nil)
81
- with_image do
82
- format ||= format_from_filename(path) || 'jpg'
83
- imlib_image_set_format(format)
84
- save_error = SaveError.new
85
- imlib_save_image_with_error_return(path, save_error.ptr)
86
- raise save_error, save_error.message if save_error.error?
87
- true
91
+ def sharpen(radius)
92
+ dup.sharpen!(radius)
88
93
  end
89
- end
90
94
 
91
- def inspect
92
- "#<#{self.class.name}:0x#{object_id.to_s(16)} width=#{width} height=#{height} format=#{format.inspect}>"
95
+ def sharpen!(radius)
96
+ raise ArgumentError, 'illegal radius' if radius < 0
97
+ _sharpen!(radius)
98
+ end
99
+
100
+ def blur(radius)
101
+ dup.blur!(radius)
102
+ end
103
+
104
+ def blur!(radius)
105
+ raise ArgumentError, 'illegal radius' if radius < 0
106
+ _sharpen!(-radius)
107
+ end
108
+
109
+ def filter(filter_expr)
110
+ dup.filter!(filter_expr)
111
+ end
112
+
113
+ def brighten!(value, r: nil, g: nil, b: nil, a: nil)
114
+ raise ArgumentError, 'illegal brightness' if value > 1 || value < -1
115
+ filter!("colormod(brightness=#{value.to_f});")
116
+ end
117
+
118
+ def brighten(*args, **opts)
119
+ dup.brighten!(*args, **opts)
120
+ end
121
+
122
+ def contrast!(value, r: nil, g: nil, b: nil, a: nil)
123
+ raise ArgumentError, 'illegal contrast (must be > 0)' if value < 0
124
+ filter!("colormod(contrast=#{value.to_f});")
125
+ end
126
+
127
+ def contrast(*args, **opts)
128
+ dup.contrast!(*args, **opts)
129
+ end
130
+
131
+ def gamma!(value, r: nil, g: nil, b: nil, a: nil)
132
+ #raise ArgumentError, 'illegal gamma (must be > 0)' if value < 0
133
+ filter!("colormod(gamma=#{value.to_f});")
134
+ end
135
+
136
+ def gamma(*args, **opts)
137
+ dup.gamma!(*args, **opts)
138
+ end
93
139
  end
94
140
 
141
+ include Transformations
142
+
143
+ def save(path, format: nil, quality: nil)
144
+ format ||= format_from_filename(path) || self.format || 'jpg'
145
+ raise ArgumentError, "invalid quality #{quality.inspect}" if quality && !(0..100).cover?(quality)
146
+ ensure_path_is_writable(path)
147
+ _save(path.to_s, format.to_s, quality)
148
+ end
149
+
150
+ def save_data(format: nil, quality: nil)
151
+ format ||= self.format || 'jpg'
152
+ with_tempfile(format) do |file|
153
+ save(file.path, format: format, quality: quality)
154
+ file.rewind
155
+ file.read
156
+ end
157
+ end
158
+
95
159
  private
96
160
 
97
161
  # 0.5 0 < scale < 1
@@ -100,16 +164,16 @@ module Rszr
100
164
  # :auto, 300 auto width, fit height
101
165
  # 400, 300, crop: :center_middle
102
166
  # 400, 300, background: rgba
103
- # 400, 300, aspect: false
167
+ # 400, 300, skew: true
104
168
 
105
- def create_resized_image(*args)
169
+ def calculate_size(*args, crop: nil, skew: nil)
106
170
  options = args.last.is_a?(Hash) ? args.pop : {}
107
- assert_valid_keys options, :crop, :background, :skew #:extend, :width, :height, :max_width, :max_height, :box
171
+ #assert_valid_keys options, :crop, :background, :skew #:extend, :width, :height, :max_width, :max_height, :box
108
172
  original_width, original_height = width, height
109
173
  x, y, = 0, 0
110
174
  if args.size == 1
111
175
  scale = args.first
112
- raise ArgumentError, "scale #{scale.inspect} out of range" unless scale > 0 && scale < 1
176
+ raise ArgumentError, "scale factor #{scale.inspect} out of range" unless scale > 0 && scale < 1
113
177
  new_width = original_width.to_f * scale
114
178
  new_height = original_height.to_f * scale
115
179
  elsif args.size == 2
@@ -121,9 +185,9 @@ module Rszr
121
185
  new_width = box_width
122
186
  new_height = box_width.to_f / original_width.to_f * original_height.to_f
123
187
  elsif box_width.is_a?(Numeric) && box_height.is_a?(Numeric)
124
- if options[:skew]
188
+ if skew
125
189
  new_width, new_height = box_width, box_height
126
- elsif options[:crop]
190
+ elsif crop
127
191
  # TODO: calculate x, y offset if crop
128
192
  else
129
193
  scale = original_width.to_f / original_height.to_f
@@ -142,38 +206,27 @@ module Rszr
142
206
  else
143
207
  raise ArgumentError, "wrong number of arguments (#{args.size} for 1..2)"
144
208
  end
145
- resized_ptr = with_image do
146
- imlib_context_set_anti_alias(1)
147
- imlib_create_cropped_scaled_image(x, y, imlib_image_get_width, imlib_image_get_height, new_width.round, new_height.round)
148
- end
149
- raise TransformationError, "error resizing image" if resized_ptr.null?
150
- resized_ptr
151
- end
152
-
153
- def create_cropped_image(x, y, width, height)
154
- cropped_ptr = with_image { imlib_create_cropped_image(x, y, width, height) }
155
- raise TransformationError, 'error cropping image' if cropped_ptr.null?
156
- cropped_ptr
209
+ [x, y, original_width, original_height, new_width.round, new_height.round]
157
210
  end
158
-
211
+
159
212
  def format_from_filename(path)
160
- File.extname(path)[1..-1]
213
+ File.extname(path)[1..-1].to_s.downcase
161
214
  end
162
215
 
163
- def context_set_image
164
- imlib_context_set_image(ptr)
216
+ def ensure_path_is_writable(path)
217
+ path = Pathname.new(path)
218
+ path.dirname.realpath.writable?
219
+ rescue Errno::ENOENT => e
220
+ raise SaveError, 'Non-existant path component'
221
+ rescue SystemCallError => e
222
+ raise SaveError, e.message
165
223
  end
166
-
167
- def with_image
168
- with_lock do
169
- context_set_image
170
- yield
224
+
225
+ def assert_valid_keys(hsh, *valid_keys)
226
+ if unknown_key = (hsh.keys - valid_keys).first
227
+ raise ArgumentError.new("Unknown key: #{unknown_key.inspect}. Valid keys are: #{valid_keys.map(&:inspect).join(', ')}")
171
228
  end
172
229
  end
173
230
 
174
- def instantiate(ptr)
175
- self.class.send(:instantiate, ptr)
176
- end
177
-
178
231
  end
179
232
  end
@@ -0,0 +1,82 @@
1
+ require 'rszr'
2
+ require 'image_processing'
3
+
4
+ module ImageProcessing
5
+ module Rszr
6
+ extend Chainable
7
+
8
+ class << self
9
+
10
+ # Returns whether the given image file is processable.
11
+ def valid_image?(file)
12
+ ::Rszr::Image.load(file).width
13
+ true
14
+ rescue ::Rszr::Error
15
+ false
16
+ end
17
+
18
+ end
19
+
20
+ class Processor < ImageProcessing::Processor
21
+ accumulator :image, ::Rszr::Image
22
+
23
+ class << self
24
+
25
+ # Loads the image on disk into a Rszr::Image object
26
+ def load_image(path_or_image, **options)
27
+ if path_or_image.is_a?(::Rszr::Image)
28
+ path_or_image
29
+ else
30
+ ::Rszr::Image.load(path_or_image)
31
+ end
32
+ # TODO: image = image.autorot if autorot && !options.key?(:autorotate)
33
+ end
34
+
35
+ # Writes the image object to disk.
36
+ # Accepts additional options (quality, format).
37
+ def save_image(image, destination_path, **options)
38
+ image.save(destination_path, **options)
39
+ end
40
+
41
+ # Calls the operation to perform the processing. If the operation is
42
+ # defined on the processor (macro), calls it. Otherwise calls the
43
+ # bang variant of the method directly on the Rszr image object.
44
+ def apply_operation(accumulator, (name, args, block))
45
+ return super if method_defined?(name)
46
+ accumulator.send("#{name}!", *args, &block)
47
+ end
48
+
49
+ end
50
+
51
+ # Resizes the image to not be larger than the specified dimensions.
52
+ def resize_to_limit(width, height, **options)
53
+ width, height = default_dimensions(width, height)
54
+ thumbnail(width, height, **options)
55
+ end
56
+
57
+ # Resizes the image to fit within the specified dimensions.
58
+ def resize_to_fit(width, height, **options)
59
+ width, height = default_dimensions(width, height)
60
+ thumbnail(width, height, **options)
61
+ end
62
+
63
+ # Resizes the image to fill the specified dimensions, applying any
64
+ # necessary cropping.
65
+ def resize_to_fill(width, height, **options)
66
+ thumbnail(width, height, crop: :center, **options)
67
+ end
68
+
69
+ private
70
+
71
+ def thumbnail(width, height, **options)
72
+ image.resize!(width, height, **options)
73
+ end
74
+
75
+ def default_dimensions(width, height)
76
+ raise Error, 'either width or height must be specified' unless width || height
77
+ [width || :auto, height || :auto]
78
+ end
79
+
80
+ end
81
+ end
82
+ end
@@ -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, IO::SEEK_CUR)
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, 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.4.0"
2
+ VERSION = '0.8.0'
3
3
  end
data/lib/rszr.rb CHANGED
@@ -1,12 +1,26 @@
1
- require 'fiddle'
2
- require 'fiddle/import'
3
1
  require 'rbconfig'
4
2
  require 'pathname'
3
+ require 'tempfile'
4
+ require 'stringio'
5
5
 
6
+ require 'rszr/rszr'
6
7
  require 'rszr/version'
7
- require 'rszr/errors'
8
- require 'rszr/lib'
9
- require 'rszr/lock'
10
- require 'rszr/handle'
11
- require 'rszr/base'
8
+ require 'rszr/stream'
9
+ require 'rszr/identification'
10
+ require 'rszr/orientation'
11
+ require 'rszr/buffered'
12
12
  require 'rszr/image'
13
+
14
+ module Rszr
15
+ class << self
16
+ @@autorotate = nil
17
+
18
+ def autorotate
19
+ @@autorotate
20
+ end
21
+
22
+ def autorotate=(value)
23
+ @@autorotate = !!value
24
+ end
25
+ end
26
+ end