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.
- checksums.yaml +5 -5
- data/.gitignore +4 -0
- data/.rubocop.yml +165 -0
- data/.travis.yml +17 -0
- data/Gemfile +5 -0
- data/Jars.lock +3 -0
- data/LICENSE-2.0.txt +202 -0
- data/README.exif +18 -0
- data/README.md +1 -0
- data/Rakefile +21 -0
- data/bin/image_voodoo +81 -72
- 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 +177 -124
- data/lib/image_voodoo/gae.rb +12 -6
- data/lib/image_voodoo/metadata.rb +1689 -0
- data/lib/image_voodoo/needs_head.rb +3 -0
- data/lib/image_voodoo/version.rb +3 -1
- data/lib/image_voodoo.rb +77 -90
- 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 +65 -0
- data/test/test_shapes.rb +23 -0
- data/tools/gen.rb +68 -0
- metadata +94 -7
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,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
|
-
|
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(
|
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
|
-
|
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
|
-
|
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
|
-
|
118
|
+
class << self
|
119
|
+
private
|
140
120
|
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
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
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
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
|
-
|
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
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
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
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
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
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
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
|
-
|
179
|
-
|
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
|
-
|
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
|
-
|
216
|
-
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
|
217
239
|
g.draw_image(@src, nil, 0, 0)
|
218
|
-
|
219
|
-
|
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
|
-
|
228
|
-
|
229
|
-
|
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
|
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
|
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
|
-
|
250
|
-
|
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
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
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
|
-
|
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
|
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 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
|