image_voodoo 0.8.7 → 0.9.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.
@@ -1,18 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'image_voodoo/awt/core_ext/buffered_image'
4
+ require 'image_voodoo/awt/core_ext/graphics2d'
1
5
  require 'image_voodoo/awt/shapes'
2
6
 
7
+ # AWT Implementation
3
8
  class ImageVoodoo
4
9
  include ImageVoodoo::Shapes
5
10
 
11
+ java_import java.awt.AlphaComposite
12
+ java_import java.awt.Color
13
+ java_import java.awt.Label
14
+ java_import java.awt.MediaTracker
6
15
  java_import java.awt.RenderingHints
16
+ java_import java.awt.Toolkit
7
17
  java_import java.awt.color.ColorSpace
18
+ java_import java.awt.event.WindowAdapter
8
19
  java_import java.awt.geom.AffineTransform
9
- java_import java.awt.image.BufferedImage
10
20
  java_import java.awt.image.ShortLookupTable
11
21
  java_import java.awt.image.ColorConvertOp
12
22
  java_import java.awt.image.LookupOp
13
23
  java_import java.awt.image.RescaleOp
14
24
  java_import java.io.ByteArrayInputStream
15
25
  java_import java.io.ByteArrayOutputStream
26
+ java_import java.io.IOException
27
+ java_import java.net.MalformedURLException
28
+ java_import java.net.URL
16
29
  java_import javax.imageio.ImageIO
17
30
  java_import javax.imageio.IIOImage
18
31
  java_import javax.imageio.ImageWriteParam
@@ -20,42 +33,7 @@ class ImageVoodoo
20
33
  java_import javax.swing.JFrame
21
34
  java_import javax.imageio.IIOException
22
35
 
23
- require 'CMYKDemo.jar'
24
- java_import org.monte.media.jpeg.CMYKJPEGImageReader
25
- java_import org.monte.media.jpeg.CMYKJPEGImageReaderSpi
26
-
27
- # FIXME: Move and rewrite in terms of new shape
28
- ##
29
- #
30
- # *AWT* (experimental) Add a border to the image and yield/return a new
31
- # image. The following options are supported:
32
- # - width: How thick is the border (default: 3)
33
- # - color: Which color is the border (in rrggbb hex value)
34
- # - style: etched, raised, plain (default: plain)
35
- #
36
- def add_border(options = {})
37
- border_width = options[:width].to_i || 2
38
- color = hex_to_color(options[:color]) || hex_to_color("000000")
39
- style = options[:style]
40
- style = nil if style.to_sym == :plain
41
- new_width, new_height = width + 2*border_width, height + 2*border_width
42
- target = paint(BufferedImage.new(new_width, new_height, color_type)) do |g|
43
- g.color = color
44
- if style
45
- raised = style.to_sym == :raised ? true : false
46
- g.fill3DRect(0, 0, new_width, new_height, raised)
47
- else
48
- g.fill_rect(0, 0, new_width, new_height)
49
- end
50
- g.draw_image(@src, nil, border_width, border_width)
51
- end
52
- block_given? ? yield(target) : target
53
- end
54
-
55
- ##
56
- #
57
36
  # A simple swing wrapper around an image voodoo object.
58
- #
59
37
  class JImagePanel < javax.swing.JPanel
60
38
  def initialize(image, x=0, y=0)
61
39
  super()
@@ -79,126 +57,170 @@ class ImageVoodoo
79
57
  ImageVoodoo::JImagePanel.__persistent__ = true
80
58
 
81
59
  # Internal class for closing preview window
82
- class WindowClosed
60
+ class WindowClosed < WindowAdapter
83
61
  def initialize(block = nil)
84
62
  @block = block || proc { java.lang.System.exit(0) }
63
+ super()
64
+ end
65
+
66
+ def windowClosing(_)
67
+ @block.call
85
68
  end
86
- def method_missing(meth,*args); end
87
- def windowClosing(event); @block.call; end
88
69
  end
89
70
 
90
- ##
91
- #
92
- # *AWT* Creates a viewable frame displaying current image within it.
93
- #
71
+ # *AWT-only* Return awt Color object.
72
+ def color_at(x, y)
73
+ Color.new(pixel(x, y))
74
+ end
75
+
76
+ # *AWT-only* Creates a viewable frame displaying current image within it.
94
77
  def preview(&block)
95
- frame = JFrame.new("Preview")
78
+ frame = JFrame.new('Preview')
96
79
  frame.add_window_listener WindowClosed.new(block)
97
80
  frame.set_bounds 0, 0, width + 20, height + 40
98
81
  frame.add JImagePanel.new(self, 10, 10)
99
82
  frame.visible = true
100
83
  end
101
84
 
102
- ##
103
- # *AWT* paint/render to the source
104
- #
85
+ # *AWT-only* paint/render to the source
105
86
  def paint(src=dup_src)
106
- yield src.graphics
87
+ yield src.graphics, src
107
88
  src.graphics.dispose
108
- ImageVoodoo.new(src, @format)
89
+ ImageVoodoo.new(@io, src, @format)
109
90
  end
110
91
 
111
- ##
112
- #
113
92
  # TODO: Figure out how to determine whether source has alpha or not
114
- # Experimental: Read an image from the url source and yield/return that
115
- # image.
116
- #
93
+ # Experimental: Read an image from the url source and yield/return that image.
117
94
  def self.from_url(source)
118
- url = java.net.URL.new(source)
119
- image = java.awt.Toolkit.default_toolkit.create_image(url)
120
- tracker = java.awt.MediaTracker.new(java.awt.Label.new(""))
121
- tracker.addImage(image, 0);
122
- tracker.waitForID(0)
95
+ image = image_from_url source
123
96
  target = paint(BufferedImage.new(image.width, image.height, RGB)) do |g|
124
97
  g.draw_image image, 0, 0, nil
125
98
  end
126
99
  block_given? ? yield(target) : target
127
- rescue java.io.IOException, java.net.MalformedURLException
128
- raise ArgumentError.new "Trouble retrieving image: #{$!.message}"
129
100
  end
130
101
 
131
- ##
132
- # *AWT* Create an image of width x height filled with a single color.
133
- #
102
+ def self.image_from_url(source)
103
+ image = Toolkit.default_toolkit.create_image(URL.new(source))
104
+ tracker = MediaTracker.new(Label.new(''))
105
+ tracker.addImage(image, 0)
106
+ tracker.waitForID(0)
107
+ image
108
+ rescue IOException, MalformedURLException
109
+ raise ArgumentError, "Trouble retrieving image: #{$!.message}"
110
+ end
111
+
112
+ # *AWT-only* Create an image of width x height filled with a single color.
134
113
  def self.canvas(width, height, rgb='000000')
135
- image = ImageVoodoo.new(BufferedImage.new(width, height, ARGB))
114
+ image = ImageVoodoo.new(@io, BufferedImage.new(width, height, ARGB))
136
115
  image.rect(0, 0, width, height, rgb)
137
116
  end
138
117
 
139
- private
118
+ class << self
119
+ private
140
120
 
141
- NEGATIVE_OP = LookupOp.new(ShortLookupTable.new(0, (0...256).to_a.reverse.to_java(:short)), nil)
142
- GREY_OP = ColorConvertOp.new(ColorSpace.getInstance(ColorSpace::CS_GRAY), nil)
143
- ARGB = BufferedImage::TYPE_INT_ARGB
144
- RGB = BufferedImage::TYPE_INT_RGB
145
- SCALE_SMOOTH = java.awt.Image::SCALE_SMOOTH
121
+ def detect_format_from_input(input)
122
+ stream = ImageIO.createImageInputStream(input)
123
+ readers = ImageIO.getImageReaders(stream)
124
+ readers.has_next ? readers.next.format_name.upcase : nil
125
+ end
146
126
 
147
- def self.with_image_impl(file)
148
- format = detect_format_from_input(file)
149
- buffered_image = read_image_from_input(file)
150
- buffered_image ? ImageVoodoo.new(buffered_image, format) : nil
151
- end
127
+ # FIXME: use library to figure this out
128
+ def determine_image_type_from_ext(ext)
129
+ case ext
130
+ when 'jpg' then RGB
131
+ else ARGB
132
+ end
133
+ end
134
+
135
+ def determine_format_from_file_name(file_name)
136
+ ext = file_name.split('.')[-1]
137
+
138
+ raise ArgumentError, "no extension in file name #{file_name}" unless ext
152
139
 
153
- def self.read_image_from_input(input)
140
+ ext
141
+ end
142
+
143
+ def new_image_impl(width, height, file_name)
144
+ format = determine_format_from_file_name file_name
145
+ image_type = determine_image_type_from_ext format
146
+ buffered_image = BufferedImage.new width, height, image_type
147
+ ImageVoodoo.new file_name, buffered_image, format
148
+ end
149
+
150
+ def read_image_from_input(input)
154
151
  ImageIO.read(input)
155
152
  rescue IIOException
156
- cmyk_reader = CMYKJPEGImageReader.new(CMYKJPEGImageReaderSpi.new)
157
- cmyk_reader.setInput(ImageIO.createImageInputStream(input))
158
- cmyk_reader.read(0)
159
- end
153
+ require 'CMYKDemo.jar'
154
+ jpeg = org.monte.media.jpeg
155
+
156
+ cmyk_reader = jpeg.CMYKJPEGImageReader.new jpeg.CMYKJPEGImageReaderSpi.new
157
+ cmyk_reader.input = ImageIO.createImageInputStream(input)
158
+ cmyk_reader.read 0
159
+ end
160
+
161
+ def with_bytes_impl(bytes)
162
+ input_stream = ByteArrayInputStream.new(bytes)
163
+ format = detect_format_from_input(input_stream)
164
+ input_stream.reset
165
+ buffered_image = read_image_from_input(input_stream)
166
+ input_stream.reset
167
+ ImageVoodoo.new(input_stream, buffered_image, format)
168
+ end
160
169
 
161
- def self.detect_format_from_input(input)
162
- stream = ImageIO.createImageInputStream(input)
163
- readers = ImageIO.getImageReaders(stream)
164
- readers.has_next ? readers.next.format_name.upcase : nil
170
+ def with_image_impl(file)
171
+ format = detect_format_from_input(file)
172
+ buffered_image = read_image_from_input(file)
173
+ buffered_image ? ImageVoodoo.new(file, buffered_image, format) : nil
174
+ end
165
175
  end
166
176
 
167
- def self.with_bytes_impl(bytes)
168
- input_stream = ByteArrayInputStream.new(bytes)
169
- format = detect_format_from_input(input_stream)
170
- input_stream.reset
171
- ImageVoodoo.new(read_image_from_input(input_stream), format)
177
+ # Save using the format string (jpg, gif, etc..) to the open Java File
178
+ # instance passed in.
179
+ def save_impl(format, file)
180
+ write_new_image format, FileImageOutputStream.new(file)
172
181
  end
173
182
 
174
- #
183
+ private
184
+
175
185
  # Converts a RGB hex value into a java.awt.Color object or dies trying
176
186
  # with an ArgumentError.
177
- #
178
- def self.hex_to_color(rgb)
179
- raise ArgumentError.new "hex rrggbb needed" if rgb !~ /[[:xdigit:]]{6,6}/
187
+ def hex_to_color(rgb='000000')
188
+ rgb ||= '000000'
189
+
190
+ raise ArgumentError, 'hex rrggbb needed' if rgb !~ /[[:xdigit:]]{6,6}/
180
191
 
181
- java.awt.Color.new(rgb[0,2].to_i(16), rgb[2,2].to_i(16), rgb[4,2].to_i(16))
192
+ Color.new(rgb[0, 2].to_i(16), rgb[2, 2].to_i(16), rgb[4, 2].to_i(16))
182
193
  end
183
194
 
184
- #
195
+ NEGATIVE_OP = LookupOp.new(ShortLookupTable.new(0, (0...256).to_a.reverse.to_java(:short)), nil)
196
+ GREY_OP = ColorConvertOp.new(ColorSpace.getInstance(ColorSpace::CS_GRAY), nil)
197
+ ARGB = BufferedImage::TYPE_INT_ARGB
198
+ RGB = BufferedImage::TYPE_INT_RGB
199
+ SCALE_SMOOTH = java.awt.Image::SCALE_SMOOTH
200
+
185
201
  # Determines the best colorspace for a new image based on whether the
186
202
  # existing image contains an alpha channel or not.
187
- #
188
203
  def color_type
189
204
  @src.color_model.has_alpha ? ARGB : RGB
190
205
  end
191
206
 
192
- #
193
207
  # Make a duplicate of the underlying Java src image
194
- #
195
208
  def dup_src
196
209
  BufferedImage.new to_java.color_model, to_java.raster, true, nil
197
210
  end
198
211
 
199
- #
212
+ def src_without_alpha
213
+ if @src.color_model.has_alpha
214
+ img = BufferedImage.new(width, height, RGB)
215
+ img.graphics.draw_image(@src, 0, 0, nil)
216
+ img.graphics.dispose
217
+ img
218
+ else
219
+ @src
220
+ end
221
+ end
222
+
200
223
  # Do simple AWT operation transformation to target.
201
- #
202
224
  def transform(operation, target=dup_src)
203
225
  paint(target) do |g|
204
226
  g.draw_image(@src, 0, 0, nil)
@@ -211,57 +233,87 @@ class ImageVoodoo
211
233
  end
212
234
 
213
235
  def alpha_impl(rgb)
214
- color = hex_to_color(rgb)
215
- target = paint(BufferedImage.new(width, height, ARGB)) do |g|
216
- g.set_composite(java.awt.AlphaComposite::Src)
236
+ color = hex_to_color(rgb).getRGB
237
+ paint(BufferedImage.new(width, height, ARGB)) do |g, target|
238
+ g.set_composite AlphaComposite::Src
217
239
  g.draw_image(@src, nil, 0, 0)
218
- 0.upto(height-1) do |i|
219
- 0.upto(width-1) do |j|
220
- target.setRGB(j, i, 0x8F1C1C) if target.getRGB(j, i) == color.getRGB
221
- end
240
+ target.each do |i, j|
241
+ target.setRGB(i, j, 0x8F1C1C) if target.getRGB(i, j) == color
222
242
  end
223
243
  end
224
244
  end
225
245
 
226
246
  def bytes_impl(format)
227
- out = ByteArrayOutputStream.new
228
- write_new_image format, ImageIO.create_image_output_stream(out)
229
- out.to_byte_array
247
+ ByteArrayOutputStream.new.tap do |out|
248
+ write_new_image format, ImageIO.create_image_output_stream(out)
249
+ end.to_byte_array
250
+ end
251
+
252
+ def correct_orientation_impl
253
+ case metadata.orientation
254
+ when 2 then flip_horizontally
255
+ when 3 then rotate(180)
256
+ when 4 then flip_vertically
257
+ when 5 then flip_horizontally && rotate(90)
258
+ when 6 then rotate(90)
259
+ when 7 then flip_horizontally && rotate(270)
260
+ when 8 then rotate(270)
261
+ else self
262
+ end
230
263
  end
231
264
 
232
265
  def flip_horizontally_impl
233
- paint {|g| g.draw_image @src, 0, 0, width, height, width, 0, 0, height, nil}
266
+ paint do |g|
267
+ g.draw_image @src, 0, 0, width, height, width, 0, 0, height, nil
268
+ end
234
269
  end
235
270
 
236
271
  def flip_vertically_impl
237
- paint {|g| g.draw_image @src, 0, 0, width, height, 0, height, width, 0, nil}
272
+ paint do |g|
273
+ g.draw_image @src, 0, 0, width, height, 0, height, width, 0, nil
274
+ end
238
275
  end
239
276
 
240
277
  def greyscale_impl
241
278
  transform(GREY_OP)
242
279
  end
243
280
 
281
+ def metadata_impl
282
+ require 'image_voodoo/metadata'
283
+
284
+ @metadata ||= ImageVoodoo::Metadata.new(@io)
285
+ end
286
+
244
287
  def negative_impl
245
288
  transform(NEGATIVE_OP)
246
289
  end
247
290
 
248
291
  def resize_impl(width, height)
249
- paint(BufferedImage.new(width, height, color_type)) do |g|
250
- scaled_image = @src.get_scaled_instance width, height, SCALE_SMOOTH
251
- g.draw_image scaled_image, 0, 0, nil
292
+ paint_new_buffered_image(width, height) do |g|
293
+ g.draw_this_image(@src.get_scaled_instance(width, height, SCALE_SMOOTH))
252
294
  end
253
295
  end
254
296
 
255
- #
256
- # Save using the format string (jpg, gif, etc..) to the open Java File
257
- # instance passed in.
258
- #
259
- def save_impl(format, file)
260
- write_new_image format, FileImageOutputStream.new(file)
297
+ def rotate_impl(radians)
298
+ new_width, new_height = rotate_new_dimensions(radians)
299
+ paint_new_buffered_image(new_width, new_height) do |g|
300
+ g.translate (new_width - width) / 2, (new_height - height) / 2
301
+ g.rotate radians, width / 2, height / 2
302
+ g.draw_this_image @src
303
+ end
304
+ end
305
+
306
+ def paint_new_buffered_image(width, height, color = color_type, &block)
307
+ paint BufferedImage.new(width, height, color), &block
308
+ end
309
+
310
+ def rotate_new_dimensions(radians)
311
+ sin, cos = Math.sin(radians).abs, Math.cos(radians).abs
312
+ [(width * cos + height * sin).floor, (width * sin + height * cos).floor]
261
313
  end
262
314
 
263
315
  def with_crop_impl(left, top, right, bottom)
264
- ImageVoodoo.new(@src.get_subimage(left, top, right-left, bottom-top), @format)
316
+ ImageVoodoo.new(@io, @src.get_subimage(left, top, right-left, bottom-top), @format)
265
317
  end
266
318
 
267
319
  def write_new_image(format, stream)
@@ -275,6 +327,7 @@ class ImageVoodoo
275
327
  param.compression_quality = @quality
276
328
  end
277
329
 
278
- writer.write nil, IIOImage.new(@src, nil, nil), param
330
+ src = format.downcase == 'jpg' ? src_without_alpha : @src
331
+ writer.write nil, IIOImage.new(src, nil, nil), param
279
332
  end
280
333
  end
@@ -1,3 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Google App Engine implementation (does this work?)
1
4
  class ImageVoodoo
2
5
  java_import com.google.appengine.api.images.Image
3
6
  java_import com.google.appengine.api.images.ImagesService
@@ -20,12 +23,20 @@ class ImageVoodoo
20
23
  # Implementations of standard features
21
24
  #++
22
25
 
26
+ class << self
27
+ private
28
+
29
+ def with_bytes_impl(bytes)
30
+ image = ImageServicesFactory.make_image(bytes)
31
+ ImageVoodoo.new bytes, image, image.format.to_s.upcase
32
+ end
33
+ end
34
+
23
35
  private
24
36
 
25
37
  def flip_horizontally_impl
26
38
  transform(ImagesServiceFactory.make_horizontal_flip)
27
39
  end
28
- private :flip_horizontally_impl
29
40
 
30
41
  def flip_vertically_impl
31
42
  transform(ImagesServiceFactory.make_vertical_flip)
@@ -39,11 +50,6 @@ class ImageVoodoo
39
50
  transform(ImagesServiceFactory.make_crop(left, top, right, bottom))
40
51
  end
41
52
 
42
- def self.with_bytes_impl(bytes)
43
- image = ImageServicesFactory.make_image(bytes)
44
- ImageVoodoo.new image, image.format.to_s.upcase
45
- end
46
-
47
53
  def from_java_bytes
48
54
  String.from_java_bytes @src.image_data
49
55
  end