image_voodoo 0.8.8 → 0.9.2
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 +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
|