rszr 0.4.0 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
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