image_voodoo 0.8.8 → 0.9.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.gitignore +4 -0
- data/.rubocop.yml +165 -0
- data/.travis.yml +16 -1
- data/Gemfile +5 -0
- data/Jars.lock +3 -0
- data/Rakefile +14 -4
- data/bin/image_voodoo +73 -84
- data/image_voodoo.gemspec +14 -7
- data/lib/image_science.rb +3 -1
- data/lib/image_voodoo/awt/core_ext/buffered_image.rb +15 -0
- data/lib/image_voodoo/awt/core_ext/graphics2d.rb +15 -0
- data/lib/image_voodoo/awt/shapes.rb +41 -3
- data/lib/image_voodoo/awt.rb +162 -152
- data/lib/image_voodoo/gae.rb +12 -6
- data/lib/image_voodoo/metadata.rb +138 -90
- data/lib/image_voodoo/needs_head.rb +3 -0
- data/lib/image_voodoo/version.rb +3 -1
- data/lib/image_voodoo.rb +47 -96
- data/samples/bench.rb +30 -36
- data/samples/file_greyscale.rb +2 -0
- data/samples/file_thumbnail.rb +2 -0
- data/samples/file_view.rb +4 -1
- data/samples/{in-memory.rb → in_memory.rb} +2 -0
- data/samples/lossy.rb +2 -0
- data/test/test_image_science.rb +34 -74
- data/test/test_image_voodoo.rb +84 -0
- data/test/test_metadata.rb +31 -16
- data/test/test_shapes.rb +23 -0
- data/tools/gen.rb +34 -27
- metadata +87 -10
- data/vendor/metadata-extractor-2.7.0.jar +0 -0
- data/vendor/xmpcore-5.1.2.jar +0 -0
data/lib/image_voodoo/awt.rb
CHANGED
@@ -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,38 +33,7 @@ class ImageVoodoo
|
|
20
33
|
java_import javax.swing.JFrame
|
21
34
|
java_import javax.imageio.IIOException
|
22
35
|
|
23
|
-
# FIXME: Move and rewrite in terms of new shape
|
24
|
-
##
|
25
|
-
#
|
26
|
-
# *AWT* (experimental) Add a border to the image and yield/return a new
|
27
|
-
# image. The following options are supported:
|
28
|
-
# - width: How thick is the border (default: 3)
|
29
|
-
# - color: Which color is the border (in rrggbb hex value)
|
30
|
-
# - style: etched, raised, plain (default: plain)
|
31
|
-
#
|
32
|
-
def add_border(options = {})
|
33
|
-
border_width = options[:width].to_i || 2
|
34
|
-
color = hex_to_color(options[:color]) || hex_to_color("000000")
|
35
|
-
style = options[:style]
|
36
|
-
style = nil if style.to_sym == :plain
|
37
|
-
new_width, new_height = width + 2*border_width, height + 2*border_width
|
38
|
-
target = paint(BufferedImage.new(new_width, new_height, color_type)) do |g|
|
39
|
-
g.color = color
|
40
|
-
if style
|
41
|
-
raised = style.to_sym == :raised ? true : false
|
42
|
-
g.fill3DRect(0, 0, new_width, new_height, raised)
|
43
|
-
else
|
44
|
-
g.fill_rect(0, 0, new_width, new_height)
|
45
|
-
end
|
46
|
-
g.draw_image(@src, nil, border_width, border_width)
|
47
|
-
end
|
48
|
-
block_given? ? yield(target) : target
|
49
|
-
end
|
50
|
-
|
51
|
-
##
|
52
|
-
#
|
53
36
|
# A simple swing wrapper around an image voodoo object.
|
54
|
-
#
|
55
37
|
class JImagePanel < javax.swing.JPanel
|
56
38
|
def initialize(image, x=0, y=0)
|
57
39
|
super()
|
@@ -75,131 +57,170 @@ class ImageVoodoo
|
|
75
57
|
ImageVoodoo::JImagePanel.__persistent__ = true
|
76
58
|
|
77
59
|
# Internal class for closing preview window
|
78
|
-
class WindowClosed
|
60
|
+
class WindowClosed < WindowAdapter
|
79
61
|
def initialize(block = nil)
|
80
62
|
@block = block || proc { java.lang.System.exit(0) }
|
63
|
+
super()
|
64
|
+
end
|
65
|
+
|
66
|
+
def windowClosing(_)
|
67
|
+
@block.call
|
81
68
|
end
|
82
|
-
def method_missing(meth,*args); end
|
83
|
-
def windowClosing(event); @block.call; end
|
84
69
|
end
|
85
70
|
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
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.
|
90
77
|
def preview(&block)
|
91
|
-
frame = JFrame.new(
|
78
|
+
frame = JFrame.new('Preview')
|
92
79
|
frame.add_window_listener WindowClosed.new(block)
|
93
80
|
frame.set_bounds 0, 0, width + 20, height + 40
|
94
81
|
frame.add JImagePanel.new(self, 10, 10)
|
95
82
|
frame.visible = true
|
96
83
|
end
|
97
84
|
|
98
|
-
|
99
|
-
# *AWT* paint/render to the source
|
100
|
-
#
|
85
|
+
# *AWT-only* paint/render to the source
|
101
86
|
def paint(src=dup_src)
|
102
|
-
yield src.graphics
|
87
|
+
yield src.graphics, src
|
103
88
|
src.graphics.dispose
|
104
89
|
ImageVoodoo.new(@io, src, @format)
|
105
90
|
end
|
106
91
|
|
107
|
-
##
|
108
|
-
#
|
109
92
|
# TODO: Figure out how to determine whether source has alpha or not
|
110
|
-
# Experimental: Read an image from the url source and yield/return that
|
111
|
-
# image.
|
112
|
-
#
|
93
|
+
# Experimental: Read an image from the url source and yield/return that image.
|
113
94
|
def self.from_url(source)
|
114
|
-
|
115
|
-
image = java.awt.Toolkit.default_toolkit.create_image(url)
|
116
|
-
tracker = java.awt.MediaTracker.new(java.awt.Label.new(""))
|
117
|
-
tracker.addImage(image, 0);
|
118
|
-
tracker.waitForID(0)
|
95
|
+
image = image_from_url source
|
119
96
|
target = paint(BufferedImage.new(image.width, image.height, RGB)) do |g|
|
120
97
|
g.draw_image image, 0, 0, nil
|
121
98
|
end
|
122
99
|
block_given? ? yield(target) : target
|
123
|
-
rescue java.io.IOException, java.net.MalformedURLException
|
124
|
-
raise ArgumentError.new "Trouble retrieving image: #{$!.message}"
|
125
100
|
end
|
126
101
|
|
127
|
-
|
128
|
-
|
129
|
-
|
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.
|
130
113
|
def self.canvas(width, height, rgb='000000')
|
131
114
|
image = ImageVoodoo.new(@io, BufferedImage.new(width, height, ARGB))
|
132
115
|
image.rect(0, 0, width, height, rgb)
|
133
116
|
end
|
134
117
|
|
135
|
-
|
118
|
+
class << self
|
119
|
+
private
|
136
120
|
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
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
|
142
126
|
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
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
|
148
134
|
|
149
|
-
|
150
|
-
|
151
|
-
rescue IIOException
|
152
|
-
require 'CMYKDemo.jar'
|
135
|
+
def determine_format_from_file_name(file_name)
|
136
|
+
ext = file_name.split('.')[-1]
|
153
137
|
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
138
|
+
raise ArgumentError, "no extension in file name #{file_name}" unless ext
|
139
|
+
|
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)
|
151
|
+
ImageIO.read(input)
|
152
|
+
rescue IIOException
|
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
|
159
160
|
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
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
|
169
|
+
|
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
|
164
175
|
end
|
165
176
|
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
buffered_image = read_image_from_input(input_stream)
|
171
|
-
input_stream.reset
|
172
|
-
ImageVoodoo.new(input_stream, buffered_image, 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)
|
173
181
|
end
|
174
182
|
|
175
|
-
|
183
|
+
private
|
184
|
+
|
176
185
|
# Converts a RGB hex value into a java.awt.Color object or dies trying
|
177
186
|
# with an ArgumentError.
|
178
|
-
|
179
|
-
|
180
|
-
|
187
|
+
def hex_to_color(rgb='000000')
|
188
|
+
rgb ||= '000000'
|
189
|
+
|
190
|
+
raise ArgumentError, 'hex rrggbb needed' if rgb !~ /[[:xdigit:]]{6,6}/
|
181
191
|
|
182
|
-
|
192
|
+
Color.new(rgb[0, 2].to_i(16), rgb[2, 2].to_i(16), rgb[4, 2].to_i(16))
|
183
193
|
end
|
184
194
|
|
185
|
-
|
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
|
+
|
186
201
|
# Determines the best colorspace for a new image based on whether the
|
187
202
|
# existing image contains an alpha channel or not.
|
188
|
-
#
|
189
203
|
def color_type
|
190
204
|
@src.color_model.has_alpha ? ARGB : RGB
|
191
205
|
end
|
192
206
|
|
193
|
-
#
|
194
207
|
# Make a duplicate of the underlying Java src image
|
195
|
-
#
|
196
208
|
def dup_src
|
197
209
|
BufferedImage.new to_java.color_model, to_java.raster, true, nil
|
198
210
|
end
|
199
211
|
|
200
|
-
|
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
|
+
|
201
223
|
# Do simple AWT operation transformation to target.
|
202
|
-
#
|
203
224
|
def transform(operation, target=dup_src)
|
204
225
|
paint(target) do |g|
|
205
226
|
g.draw_image(@src, 0, 0, nil)
|
@@ -212,51 +233,45 @@ class ImageVoodoo
|
|
212
233
|
end
|
213
234
|
|
214
235
|
def alpha_impl(rgb)
|
215
|
-
color = hex_to_color(rgb)
|
216
|
-
|
217
|
-
g.set_composite
|
236
|
+
color = hex_to_color(rgb).getRGB
|
237
|
+
paint(BufferedImage.new(width, height, ARGB)) do |g, target|
|
238
|
+
g.set_composite AlphaComposite::Src
|
218
239
|
g.draw_image(@src, nil, 0, 0)
|
219
|
-
|
220
|
-
|
221
|
-
target.setRGB(j, i, 0x8F1C1C) if target.getRGB(j, i) == color.getRGB
|
222
|
-
end
|
240
|
+
target.each do |i, j|
|
241
|
+
target.setRGB(i, j, 0x8F1C1C) if target.getRGB(i, j) == color
|
223
242
|
end
|
224
243
|
end
|
225
244
|
end
|
226
245
|
|
227
246
|
def bytes_impl(format)
|
228
|
-
|
229
|
-
|
230
|
-
|
247
|
+
ByteArrayOutputStream.new.tap do |out|
|
248
|
+
write_new_image format, ImageIO.create_image_output_stream(out)
|
249
|
+
end.to_byte_array
|
231
250
|
end
|
232
251
|
|
233
252
|
def correct_orientation_impl
|
234
253
|
case metadata.orientation
|
235
|
-
when 2 then
|
236
|
-
|
237
|
-
when
|
238
|
-
|
239
|
-
when
|
240
|
-
|
241
|
-
when
|
242
|
-
|
243
|
-
rotate 270
|
244
|
-
when 6 then
|
245
|
-
rotate 90
|
246
|
-
when 7 then
|
247
|
-
flip_horizontally
|
248
|
-
rotate 270
|
249
|
-
else
|
250
|
-
self
|
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
|
251
262
|
end
|
252
263
|
end
|
253
264
|
|
254
265
|
def flip_horizontally_impl
|
255
|
-
paint
|
266
|
+
paint do |g|
|
267
|
+
g.draw_image @src, 0, 0, width, height, width, 0, 0, height, nil
|
268
|
+
end
|
256
269
|
end
|
257
270
|
|
258
271
|
def flip_vertically_impl
|
259
|
-
paint
|
272
|
+
paint do |g|
|
273
|
+
g.draw_image @src, 0, 0, width, height, 0, height, width, 0, nil
|
274
|
+
end
|
260
275
|
end
|
261
276
|
|
262
277
|
def greyscale_impl
|
@@ -265,7 +280,7 @@ class ImageVoodoo
|
|
265
280
|
|
266
281
|
def metadata_impl
|
267
282
|
require 'image_voodoo/metadata'
|
268
|
-
|
283
|
+
|
269
284
|
@metadata ||= ImageVoodoo::Metadata.new(@io)
|
270
285
|
end
|
271
286
|
|
@@ -274,33 +289,27 @@ class ImageVoodoo
|
|
274
289
|
end
|
275
290
|
|
276
291
|
def resize_impl(width, height)
|
277
|
-
|
278
|
-
|
279
|
-
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))
|
280
294
|
end
|
281
295
|
end
|
282
296
|
|
283
|
-
def rotate_impl(
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
paint(BufferedImage.new(new_width, new_height, color_type)) do |g|
|
290
|
-
|
291
|
-
g.java_send :translate, [::Java::int, ::Java::int],
|
292
|
-
(new_width - width)/2, (new_height - height)/2
|
293
|
-
g.rotate(radians, width / 2, height / 2);
|
294
|
-
g.draw_image @src, 0, 0, nil
|
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
|
295
303
|
end
|
296
304
|
end
|
297
305
|
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
def
|
303
|
-
|
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]
|
304
313
|
end
|
305
314
|
|
306
315
|
def with_crop_impl(left, top, right, bottom)
|
@@ -318,6 +327,7 @@ class ImageVoodoo
|
|
318
327
|
param.compression_quality = @quality
|
319
328
|
end
|
320
329
|
|
321
|
-
|
330
|
+
src = format.downcase == 'jpg' ? src_without_alpha : @src
|
331
|
+
writer.write nil, IIOImage.new(src, nil, nil), param
|
322
332
|
end
|
323
333
|
end
|
data/lib/image_voodoo/gae.rb
CHANGED
@@ -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 bytes, 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
|