rmagick 2.15.0 → 2.15.1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of rmagick might be problematic. Click here for more details.

@@ -69,6 +69,15 @@ subdirectory of the installation directory for any error messages. These message
69
69
  typically contain enough additional information for you to be able to diagnose
70
70
  the problem. Also see "this FAQ":https://web.archive.org/web/20140512193354/http://rmagick.rubyforge.org/install-faq.html#libmagick.
71
71
 
72
+ On OS X with Homebrew, try [re]installing pkg-config:
73
+
74
+ <pre>
75
+ brew uninstall pkg-config
76
+ brew install pkg-config
77
+ brew unlink pkg-config
78
+ brew link pkg-config
79
+ </pre>
80
+
72
81
  h4. Cannot open shared object file
73
82
 
74
83
  If you get a message like this:
@@ -93,6 +102,13 @@ LD_LIBRARY_PATH variable like this:
93
102
  On Linux, see @ld(1)@ and @ld.so(8)@ for more information. On other operating
94
103
  systems, see the documentation for the dynamic loading facility.
95
104
 
105
+ h4. Segmentation fault
106
+
107
+ Default stack size of your operating system might be too small. Try removing the limit with this command:
108
+
109
+ <pre>
110
+ ulimit -s unlimited
111
+ </pre>
96
112
 
97
113
  h2(#upgrade). Upgrading
98
114
 
@@ -19,3 +19,8 @@ esac
19
19
  sudo make install
20
20
  cd ..
21
21
  sudo ldconfig
22
+
23
+ if [[ $TRAVIS_RUBY_VERSION =~ ^1.8 ]]; then
24
+ echo "Set the stack size to unlimited to avoid segfault for Ruby 1.8"
25
+ ulimit -s unlimited
26
+ fi
@@ -816,6 +816,11 @@ images_from_imagelist(VALUE imagelist)
816
816
 
817
817
  t = rb_ary_entry(images, x);
818
818
  image = rm_check_destroyed(t);
819
+ // avoid a loop in this linked imagelist, issue #202
820
+ if (head == image || GetPreviousImageInList(image) != NULL)
821
+ {
822
+ image = rm_clone_image(image);
823
+ }
819
824
  AppendImageToList(&head, image);
820
825
  }
821
826
 
@@ -1,5 +1,5 @@
1
1
  module Magick
2
- VERSION = '2.15.0'
2
+ VERSION = '2.15.1'
3
3
  MIN_RUBY_VERSION = '1.8.5'
4
4
  MIN_IM_VERSION = '6.4.9'
5
5
  end
@@ -42,1907 +42,1907 @@ module Magick
42
42
  end
43
43
  end
44
44
 
45
- # Geometry class and related enum constants
46
- class GeometryValue < Enum
47
- # no methods
48
- end
49
-
50
- PercentGeometry = GeometryValue.new(:PercentGeometry, 1).freeze
51
- AspectGeometry = GeometryValue.new(:AspectGeometry, 2).freeze
52
- LessGeometry = GeometryValue.new(:LessGeometry, 3).freeze
53
- GreaterGeometry = GeometryValue.new(:GreaterGeometry, 4).freeze
54
- AreaGeometry = GeometryValue.new(:AreaGeometry, 5).freeze
55
- MinimumGeometry = GeometryValue.new(:MinimumGeometry, 6).freeze
56
-
57
- class Geometry
58
- FLAGS = ['', '%', '!', '<', '>', '@', '^']
59
- RFLAGS = { '%' => PercentGeometry,
60
- '!' => AspectGeometry,
61
- '<' => LessGeometry,
62
- '>' => GreaterGeometry,
63
- '@' => AreaGeometry,
64
- '^' => MinimumGeometry }
65
-
66
- attr_accessor :width, :height, :x, :y, :flag
67
-
68
- def initialize(width=nil, height=nil, x=nil, y=nil, flag=nil)
69
- fail(ArgumentError, "width set to #{width}") if width.is_a? GeometryValue
70
- fail(ArgumentError, "height set to #{height}") if height.is_a? GeometryValue
71
- fail(ArgumentError, "x set to #{x}") if x.is_a? GeometryValue
72
- fail(ArgumentError, "y set to #{y}") if y.is_a? GeometryValue
73
-
74
- # Support floating-point width and height arguments so Geometry
75
- # objects can be used to specify Image#density= arguments.
76
- if width.nil?
77
- @width = 0
78
- elsif width.to_f >= 0.0
79
- @width = width.to_f
80
- else
81
- Kernel.raise ArgumentError, "width must be >= 0: #{width}"
82
- end
83
- if height.nil?
84
- @height = 0
85
- elsif height.to_f >= 0.0
86
- @height = height.to_f
87
- else
88
- Kernel.raise ArgumentError, "height must be >= 0: #{height}"
89
- end
90
-
91
- @x = x.to_i
92
- @y = y.to_i
93
- @flag = flag
94
- end
95
-
96
- # Construct an object from a geometry string
97
- W = /(\d+\.\d+%?)|(\d*%?)/
98
- H = W
99
- X = /(?:([-+]\d+))?/
100
- Y = X
101
- RE = /\A#{W}x?#{H}#{X}#{Y}([!<>@\^]?)\Z/
102
-
103
- def self.from_s(str)
104
- m = RE.match(str)
105
- if m
106
- width = (m[1] || m[2]).to_f
107
- height = (m[3] || m[4]).to_f
108
- x = m[5].to_i
109
- y = m[6].to_i
110
- flag = RFLAGS[m[7]]
111
- else
112
- Kernel.raise ArgumentError, 'invalid geometry format'
113
- end
114
- if str['%']
115
- flag = PercentGeometry
116
- end
117
- Geometry.new(width, height, x, y, flag)
118
- end
119
-
120
- # Convert object to a geometry string
121
- def to_s
122
- str = ''
123
- if @width > 0
124
- fmt = @width.truncate == @width ? '%d' : '%.2f'
125
- str << sprintf(fmt, @width)
126
- str << '%' if @flag == PercentGeometry
127
- end
128
-
129
- if (@width > 0 && @flag != PercentGeometry) || (@height > 0)
130
- str << 'x'
131
- end
132
-
133
- if @height > 0
134
- fmt = @height.truncate == @height ? '%d' : '%.2f'
135
- str << sprintf(fmt, @height)
136
- str << '%' if @flag == PercentGeometry
137
- end
138
- str << sprintf('%+d%+d', @x, @y) if @x != 0 || @y != 0
139
- if @flag != PercentGeometry
140
- str << FLAGS[@flag.to_i]
141
- end
142
- str
143
- end
45
+ # Geometry class and related enum constants
46
+ class GeometryValue < Enum
47
+ # no methods
48
+ end
49
+
50
+ PercentGeometry = GeometryValue.new(:PercentGeometry, 1).freeze
51
+ AspectGeometry = GeometryValue.new(:AspectGeometry, 2).freeze
52
+ LessGeometry = GeometryValue.new(:LessGeometry, 3).freeze
53
+ GreaterGeometry = GeometryValue.new(:GreaterGeometry, 4).freeze
54
+ AreaGeometry = GeometryValue.new(:AreaGeometry, 5).freeze
55
+ MinimumGeometry = GeometryValue.new(:MinimumGeometry, 6).freeze
56
+
57
+ class Geometry
58
+ FLAGS = ['', '%', '!', '<', '>', '@', '^']
59
+ RFLAGS = { '%' => PercentGeometry,
60
+ '!' => AspectGeometry,
61
+ '<' => LessGeometry,
62
+ '>' => GreaterGeometry,
63
+ '@' => AreaGeometry,
64
+ '^' => MinimumGeometry }
65
+
66
+ attr_accessor :width, :height, :x, :y, :flag
67
+
68
+ def initialize(width=nil, height=nil, x=nil, y=nil, flag=nil)
69
+ fail(ArgumentError, "width set to #{width}") if width.is_a? GeometryValue
70
+ fail(ArgumentError, "height set to #{height}") if height.is_a? GeometryValue
71
+ fail(ArgumentError, "x set to #{x}") if x.is_a? GeometryValue
72
+ fail(ArgumentError, "y set to #{y}") if y.is_a? GeometryValue
73
+
74
+ # Support floating-point width and height arguments so Geometry
75
+ # objects can be used to specify Image#density= arguments.
76
+ if width.nil?
77
+ @width = 0
78
+ elsif width.to_f >= 0.0
79
+ @width = width.to_f
80
+ else
81
+ Kernel.raise ArgumentError, "width must be >= 0: #{width}"
82
+ end
83
+ if height.nil?
84
+ @height = 0
85
+ elsif height.to_f >= 0.0
86
+ @height = height.to_f
87
+ else
88
+ Kernel.raise ArgumentError, "height must be >= 0: #{height}"
89
+ end
90
+
91
+ @x = x.to_i
92
+ @y = y.to_i
93
+ @flag = flag
144
94
  end
145
95
 
146
- class Draw
147
- # Thse hashes are used to map Magick constant
148
- # values to the strings used in the primitives.
149
- ALIGN_TYPE_NAMES = {
150
- LeftAlign.to_i => 'left',
151
- RightAlign.to_i => 'right',
152
- CenterAlign.to_i => 'center'
153
- }.freeze
154
- ANCHOR_TYPE_NAMES = {
155
- StartAnchor.to_i => 'start',
156
- MiddleAnchor.to_i => 'middle',
157
- EndAnchor.to_i => 'end'
158
- }.freeze
159
- DECORATION_TYPE_NAMES = {
160
- NoDecoration.to_i => 'none',
161
- UnderlineDecoration.to_i => 'underline',
162
- OverlineDecoration.to_i => 'overline',
163
- LineThroughDecoration.to_i => 'line-through'
164
- }.freeze
165
- FONT_WEIGHT_NAMES = {
166
- AnyWeight.to_i => 'all',
167
- NormalWeight.to_i => 'normal',
168
- BoldWeight.to_i => 'bold',
169
- BolderWeight.to_i => 'bolder',
170
- LighterWeight.to_i => 'lighter',
171
- }.freeze
172
- GRAVITY_NAMES = {
173
- NorthWestGravity.to_i => 'northwest',
174
- NorthGravity.to_i => 'north',
175
- NorthEastGravity.to_i => 'northeast',
176
- WestGravity.to_i => 'west',
177
- CenterGravity.to_i => 'center',
178
- EastGravity.to_i => 'east',
179
- SouthWestGravity.to_i => 'southwest',
180
- SouthGravity.to_i => 'south',
181
- SouthEastGravity.to_i => 'southeast'
182
- }.freeze
183
- PAINT_METHOD_NAMES = {
184
- PointMethod.to_i => 'point',
185
- ReplaceMethod.to_i => 'replace',
186
- FloodfillMethod.to_i => 'floodfill',
187
- FillToBorderMethod.to_i => 'filltoborder',
188
- ResetMethod.to_i => 'reset'
189
- }.freeze
190
- STRETCH_TYPE_NAMES = {
191
- NormalStretch.to_i => 'normal',
192
- UltraCondensedStretch.to_i => 'ultra-condensed',
193
- ExtraCondensedStretch.to_i => 'extra-condensed',
194
- CondensedStretch.to_i => 'condensed',
195
- SemiCondensedStretch.to_i => 'semi-condensed',
196
- SemiExpandedStretch.to_i => 'semi-expanded',
197
- ExpandedStretch.to_i => 'expanded',
198
- ExtraExpandedStretch.to_i => 'extra-expanded',
199
- UltraExpandedStretch.to_i => 'ultra-expanded',
200
- AnyStretch.to_i => 'all'
201
- }.freeze
202
- STYLE_TYPE_NAMES = {
203
- NormalStyle.to_i => 'normal',
204
- ItalicStyle.to_i => 'italic',
205
- ObliqueStyle.to_i => 'oblique',
206
- AnyStyle.to_i => 'all'
207
- }.freeze
208
-
209
- private
210
-
211
- def enquote(str)
212
- if str.length > 2 && /\A(?:\"[^\"]+\"|\'[^\']+\'|\{[^\}]+\})\z/.match(str)
213
- return str
214
- else
215
- return '"' + str + '"'
216
- end
217
- end
218
-
219
- public
220
-
221
- # Apply coordinate transformations to support scaling (s), rotation (r),
222
- # and translation (t). Angles are specified in radians.
223
- def affine(sx, rx, ry, sy, tx, ty)
224
- primitive 'affine ' + sprintf('%g,%g,%g,%g,%g,%g', sx, rx, ry, sy, tx, ty)
225
- end
226
-
227
- # Draw an arc.
228
- def arc(startX, startY, endX, endY, startDegrees, endDegrees)
229
- primitive 'arc ' + sprintf('%g,%g %g,%g %g,%g',
230
- startX, startY, endX, endY, startDegrees, endDegrees)
231
- end
232
-
233
- # Draw a bezier curve.
234
- def bezier(*points)
235
- if points.length == 0
236
- Kernel.raise ArgumentError, 'no points specified'
237
- elsif points.length.odd?
238
- Kernel.raise ArgumentError, 'odd number of arguments specified'
239
- end
240
- primitive 'bezier ' + points.join(',')
241
- end
242
-
243
- # Draw a circle
244
- def circle(originX, originY, perimX, perimY)
245
- primitive 'circle ' + sprintf('%g,%g %g,%g', originX, originY, perimX, perimY)
246
- end
247
-
248
- # Invoke a clip-path defined by def_clip_path.
249
- def clip_path(name)
250
- primitive "clip-path #{name}"
251
- end
252
-
253
- # Define the clipping rule.
254
- def clip_rule(rule)
255
- unless ['evenodd', 'nonzero'].include?(rule.downcase)
256
- Kernel.raise ArgumentError, "Unknown clipping rule #{rule}"
257
- end
258
- primitive "clip-rule #{rule}"
259
- end
260
-
261
- # Define the clip units
262
- def clip_units(unit)
263
- unless ['userspace', 'userspaceonuse', 'objectboundingbox'].include?(unit.downcase)
264
- Kernel.raise ArgumentError, "Unknown clip unit #{unit}"
265
- end
266
- primitive "clip-units #{unit}"
267
- end
268
-
269
- # Set color in image according to specified colorization rule. Rule is one of
270
- # point, replace, floodfill, filltoborder,reset
271
- def color(x, y, method)
272
- unless PAINT_METHOD_NAMES.has_key?(method.to_i)
273
- Kernel.raise ArgumentError, "Unknown PaintMethod: #{method}"
274
- end
275
- primitive "color #{x},#{y},#{PAINT_METHOD_NAMES[method.to_i]}"
276
- end
277
-
278
- # Specify EITHER the text decoration (none, underline, overline,
279
- # line-through) OR the text solid background color (any color name or spec)
280
- def decorate(decoration)
281
- if DECORATION_TYPE_NAMES.has_key?(decoration.to_i)
282
- primitive "decorate #{DECORATION_TYPE_NAMES[decoration.to_i]}"
283
- else
284
- primitive "decorate #{enquote(decoration)}"
285
- end
286
- end
287
-
288
- # Define a clip-path. A clip-path is a sequence of primitives
289
- # bracketed by the "push clip-path <name>" and "pop clip-path"
290
- # primitives. Upon advice from the IM guys, we also bracket
291
- # the clip-path primitives with "push(pop) defs" and "push
292
- # (pop) graphic-context".
293
- def define_clip_path(name)
294
- push('defs')
295
- push('clip-path', name)
296
- push('graphic-context')
297
- yield
298
- ensure
299
- pop('graphic-context')
300
- pop('clip-path')
301
- pop('defs')
302
- end
303
-
304
- # Draw an ellipse
305
- def ellipse(originX, originY, width, height, arcStart, arcEnd)
306
- primitive 'ellipse ' + sprintf('%g,%g %g,%g %g,%g',
307
- originX, originY, width, height, arcStart, arcEnd)
308
- end
309
-
310
- # Let anything through, but the only defined argument
311
- # is "UTF-8". All others are apparently ignored.
312
- def encoding(encoding)
313
- primitive "encoding #{encoding}"
314
- end
315
-
316
- # Specify object fill, a color name or pattern name
317
- def fill(colorspec)
318
- primitive "fill #{enquote(colorspec)}"
319
- end
320
- alias_method :fill_color, :fill
321
- alias_method :fill_pattern, :fill
322
-
323
- # Specify fill opacity (use "xx%" to indicate percentage)
324
- def fill_opacity(opacity)
325
- primitive "fill-opacity #{opacity}"
326
- end
327
-
328
- def fill_rule(rule)
329
- unless ['evenodd', 'nonzero'].include?(rule.downcase)
330
- Kernel.raise ArgumentError, "Unknown fill rule #{rule}"
331
- end
332
- primitive "fill-rule #{rule}"
333
- end
334
-
335
- # Specify text drawing font
336
- def font(name)
337
- primitive "font \'#{name}\'"
338
- end
339
-
340
- def font_family(name)
341
- primitive "font-family \'#{name}\'"
342
- end
343
-
344
- def font_stretch(stretch)
345
- unless STRETCH_TYPE_NAMES.has_key?(stretch.to_i)
346
- Kernel.raise ArgumentError, 'Unknown stretch type'
347
- end
348
- primitive "font-stretch #{STRETCH_TYPE_NAMES[stretch.to_i]}"
349
- end
350
-
351
- def font_style(style)
352
- unless STYLE_TYPE_NAMES.has_key?(style.to_i)
353
- Kernel.raise ArgumentError, 'Unknown style type'
354
- end
355
- primitive "font-style #{STYLE_TYPE_NAMES[style.to_i]}"
356
- end
357
-
358
- # The font weight argument can be either a font weight
359
- # constant or [100,200,...,900]
360
- def font_weight(weight)
361
- if FONT_WEIGHT_NAMES.has_key?(weight.to_i)
362
- primitive "font-weight #{FONT_WEIGHT_NAMES[weight.to_i]}"
363
- else
364
- primitive "font-weight #{weight}"
365
- end
366
- end
367
-
368
- # Specify the text positioning gravity, one of:
369
- # NorthWest, North, NorthEast, West, Center, East, SouthWest, South, SouthEast
370
- def gravity(grav)
371
- unless GRAVITY_NAMES.has_key?(grav.to_i)
372
- Kernel.raise ArgumentError, 'Unknown text positioning gravity'
373
- end
374
- primitive "gravity #{GRAVITY_NAMES[grav.to_i]}"
375
- end
376
-
377
- # IM 6.5.5-8 and later
378
- def interline_spacing(space)
379
- begin
380
- Float(space)
381
- rescue ArgumentError
382
- Kernel.raise ArgumentError, 'invalid value for interline_spacing'
383
- rescue TypeError
384
- Kernel.raise TypeError, "can't convert #{space.class} into Float"
385
- end
386
- primitive "interline-spacing #{space}"
387
- end
388
-
389
- # IM 6.4.8-3 and later
390
- def interword_spacing(space)
391
- begin
392
- Float(space)
393
- rescue ArgumentError
394
- Kernel.raise ArgumentError, 'invalid value for interword_spacing'
395
- rescue TypeError
396
- Kernel.raise TypeError, "can't convert #{space.class} into Float"
397
- end
398
- primitive "interword-spacing #{space}"
399
- end
400
-
401
- # IM 6.4.8-3 and later
402
- def kerning(space)
403
- begin
404
- Float(space)
405
- rescue ArgumentError
406
- Kernel.raise ArgumentError, 'invalid value for kerning'
407
- rescue TypeError
408
- Kernel.raise TypeError, "can't convert #{space.class} into Float"
409
- end
410
- primitive "kerning #{space}"
411
- end
412
-
413
- # Draw a line
414
- def line(startX, startY, endX, endY)
415
- primitive 'line ' + sprintf('%g,%g %g,%g', startX, startY, endX, endY)
416
- end
417
-
418
- # Set matte (make transparent) in image according to the specified
419
- # colorization rule
420
- def matte(x, y, method)
421
- unless PAINT_METHOD_NAMES.has_key?(method.to_i)
422
- Kernel.raise ArgumentError, 'Unknown paint method'
423
- end
424
- primitive "matte #{x},#{y} #{PAINT_METHOD_NAMES[method.to_i]}"
96
+ # Construct an object from a geometry string
97
+ W = /(\d+\.\d+%?)|(\d*%?)/
98
+ H = W
99
+ X = /(?:([-+]\d+))?/
100
+ Y = X
101
+ RE = /\A#{W}x?#{H}#{X}#{Y}([!<>@\^]?)\Z/
102
+
103
+ def self.from_s(str)
104
+ m = RE.match(str)
105
+ if m
106
+ width = (m[1] || m[2]).to_f
107
+ height = (m[3] || m[4]).to_f
108
+ x = m[5].to_i
109
+ y = m[6].to_i
110
+ flag = RFLAGS[m[7]]
111
+ else
112
+ Kernel.raise ArgumentError, 'invalid geometry format'
113
+ end
114
+ if str['%']
115
+ flag = PercentGeometry
116
+ end
117
+ Geometry.new(width, height, x, y, flag)
118
+ end
119
+
120
+ # Convert object to a geometry string
121
+ def to_s
122
+ str = ''
123
+ if @width > 0
124
+ fmt = @width.truncate == @width ? '%d' : '%.2f'
125
+ str << sprintf(fmt, @width)
126
+ str << '%' if @flag == PercentGeometry
127
+ end
128
+
129
+ if (@width > 0 && @flag != PercentGeometry) || (@height > 0)
130
+ str << 'x'
131
+ end
132
+
133
+ if @height > 0
134
+ fmt = @height.truncate == @height ? '%d' : '%.2f'
135
+ str << sprintf(fmt, @height)
136
+ str << '%' if @flag == PercentGeometry
137
+ end
138
+ str << sprintf('%+d%+d', @x, @y) if @x != 0 || @y != 0
139
+ if @flag != PercentGeometry
140
+ str << FLAGS[@flag.to_i]
141
+ end
142
+ str
143
+ end
144
+ end
145
+
146
+ class Draw
147
+ # Thse hashes are used to map Magick constant
148
+ # values to the strings used in the primitives.
149
+ ALIGN_TYPE_NAMES = {
150
+ LeftAlign.to_i => 'left',
151
+ RightAlign.to_i => 'right',
152
+ CenterAlign.to_i => 'center'
153
+ }.freeze
154
+ ANCHOR_TYPE_NAMES = {
155
+ StartAnchor.to_i => 'start',
156
+ MiddleAnchor.to_i => 'middle',
157
+ EndAnchor.to_i => 'end'
158
+ }.freeze
159
+ DECORATION_TYPE_NAMES = {
160
+ NoDecoration.to_i => 'none',
161
+ UnderlineDecoration.to_i => 'underline',
162
+ OverlineDecoration.to_i => 'overline',
163
+ LineThroughDecoration.to_i => 'line-through'
164
+ }.freeze
165
+ FONT_WEIGHT_NAMES = {
166
+ AnyWeight.to_i => 'all',
167
+ NormalWeight.to_i => 'normal',
168
+ BoldWeight.to_i => 'bold',
169
+ BolderWeight.to_i => 'bolder',
170
+ LighterWeight.to_i => 'lighter',
171
+ }.freeze
172
+ GRAVITY_NAMES = {
173
+ NorthWestGravity.to_i => 'northwest',
174
+ NorthGravity.to_i => 'north',
175
+ NorthEastGravity.to_i => 'northeast',
176
+ WestGravity.to_i => 'west',
177
+ CenterGravity.to_i => 'center',
178
+ EastGravity.to_i => 'east',
179
+ SouthWestGravity.to_i => 'southwest',
180
+ SouthGravity.to_i => 'south',
181
+ SouthEastGravity.to_i => 'southeast'
182
+ }.freeze
183
+ PAINT_METHOD_NAMES = {
184
+ PointMethod.to_i => 'point',
185
+ ReplaceMethod.to_i => 'replace',
186
+ FloodfillMethod.to_i => 'floodfill',
187
+ FillToBorderMethod.to_i => 'filltoborder',
188
+ ResetMethod.to_i => 'reset'
189
+ }.freeze
190
+ STRETCH_TYPE_NAMES = {
191
+ NormalStretch.to_i => 'normal',
192
+ UltraCondensedStretch.to_i => 'ultra-condensed',
193
+ ExtraCondensedStretch.to_i => 'extra-condensed',
194
+ CondensedStretch.to_i => 'condensed',
195
+ SemiCondensedStretch.to_i => 'semi-condensed',
196
+ SemiExpandedStretch.to_i => 'semi-expanded',
197
+ ExpandedStretch.to_i => 'expanded',
198
+ ExtraExpandedStretch.to_i => 'extra-expanded',
199
+ UltraExpandedStretch.to_i => 'ultra-expanded',
200
+ AnyStretch.to_i => 'all'
201
+ }.freeze
202
+ STYLE_TYPE_NAMES = {
203
+ NormalStyle.to_i => 'normal',
204
+ ItalicStyle.to_i => 'italic',
205
+ ObliqueStyle.to_i => 'oblique',
206
+ AnyStyle.to_i => 'all'
207
+ }.freeze
208
+
209
+ private
210
+
211
+ def enquote(str)
212
+ if str.length > 2 && /\A(?:\"[^\"]+\"|\'[^\']+\'|\{[^\}]+\})\z/.match(str)
213
+ return str
214
+ else
215
+ return '"' + str + '"'
216
+ end
217
+ end
218
+
219
+ public
220
+
221
+ # Apply coordinate transformations to support scaling (s), rotation (r),
222
+ # and translation (t). Angles are specified in radians.
223
+ def affine(sx, rx, ry, sy, tx, ty)
224
+ primitive 'affine ' + sprintf('%g,%g,%g,%g,%g,%g', sx, rx, ry, sy, tx, ty)
225
+ end
226
+
227
+ # Draw an arc.
228
+ def arc(startX, startY, endX, endY, startDegrees, endDegrees)
229
+ primitive 'arc ' + sprintf('%g,%g %g,%g %g,%g',
230
+ startX, startY, endX, endY, startDegrees, endDegrees)
231
+ end
232
+
233
+ # Draw a bezier curve.
234
+ def bezier(*points)
235
+ if points.length == 0
236
+ Kernel.raise ArgumentError, 'no points specified'
237
+ elsif points.length.odd?
238
+ Kernel.raise ArgumentError, 'odd number of arguments specified'
239
+ end
240
+ primitive 'bezier ' + points.join(',')
241
+ end
242
+
243
+ # Draw a circle
244
+ def circle(originX, originY, perimX, perimY)
245
+ primitive 'circle ' + sprintf('%g,%g %g,%g', originX, originY, perimX, perimY)
246
+ end
247
+
248
+ # Invoke a clip-path defined by def_clip_path.
249
+ def clip_path(name)
250
+ primitive "clip-path #{name}"
251
+ end
252
+
253
+ # Define the clipping rule.
254
+ def clip_rule(rule)
255
+ unless ['evenodd', 'nonzero'].include?(rule.downcase)
256
+ Kernel.raise ArgumentError, "Unknown clipping rule #{rule}"
257
+ end
258
+ primitive "clip-rule #{rule}"
259
+ end
260
+
261
+ # Define the clip units
262
+ def clip_units(unit)
263
+ unless ['userspace', 'userspaceonuse', 'objectboundingbox'].include?(unit.downcase)
264
+ Kernel.raise ArgumentError, "Unknown clip unit #{unit}"
265
+ end
266
+ primitive "clip-units #{unit}"
267
+ end
268
+
269
+ # Set color in image according to specified colorization rule. Rule is one of
270
+ # point, replace, floodfill, filltoborder,reset
271
+ def color(x, y, method)
272
+ unless PAINT_METHOD_NAMES.has_key?(method.to_i)
273
+ Kernel.raise ArgumentError, "Unknown PaintMethod: #{method}"
274
+ end
275
+ primitive "color #{x},#{y},#{PAINT_METHOD_NAMES[method.to_i]}"
276
+ end
277
+
278
+ # Specify EITHER the text decoration (none, underline, overline,
279
+ # line-through) OR the text solid background color (any color name or spec)
280
+ def decorate(decoration)
281
+ if DECORATION_TYPE_NAMES.has_key?(decoration.to_i)
282
+ primitive "decorate #{DECORATION_TYPE_NAMES[decoration.to_i]}"
283
+ else
284
+ primitive "decorate #{enquote(decoration)}"
285
+ end
286
+ end
287
+
288
+ # Define a clip-path. A clip-path is a sequence of primitives
289
+ # bracketed by the "push clip-path <name>" and "pop clip-path"
290
+ # primitives. Upon advice from the IM guys, we also bracket
291
+ # the clip-path primitives with "push(pop) defs" and "push
292
+ # (pop) graphic-context".
293
+ def define_clip_path(name)
294
+ push('defs')
295
+ push('clip-path', name)
296
+ push('graphic-context')
297
+ yield
298
+ ensure
299
+ pop('graphic-context')
300
+ pop('clip-path')
301
+ pop('defs')
302
+ end
303
+
304
+ # Draw an ellipse
305
+ def ellipse(originX, originY, width, height, arcStart, arcEnd)
306
+ primitive 'ellipse ' + sprintf('%g,%g %g,%g %g,%g',
307
+ originX, originY, width, height, arcStart, arcEnd)
308
+ end
309
+
310
+ # Let anything through, but the only defined argument
311
+ # is "UTF-8". All others are apparently ignored.
312
+ def encoding(encoding)
313
+ primitive "encoding #{encoding}"
314
+ end
315
+
316
+ # Specify object fill, a color name or pattern name
317
+ def fill(colorspec)
318
+ primitive "fill #{enquote(colorspec)}"
319
+ end
320
+ alias_method :fill_color, :fill
321
+ alias_method :fill_pattern, :fill
322
+
323
+ # Specify fill opacity (use "xx%" to indicate percentage)
324
+ def fill_opacity(opacity)
325
+ primitive "fill-opacity #{opacity}"
326
+ end
327
+
328
+ def fill_rule(rule)
329
+ unless ['evenodd', 'nonzero'].include?(rule.downcase)
330
+ Kernel.raise ArgumentError, "Unknown fill rule #{rule}"
331
+ end
332
+ primitive "fill-rule #{rule}"
333
+ end
334
+
335
+ # Specify text drawing font
336
+ def font(name)
337
+ primitive "font \'#{name}\'"
338
+ end
339
+
340
+ def font_family(name)
341
+ primitive "font-family \'#{name}\'"
342
+ end
343
+
344
+ def font_stretch(stretch)
345
+ unless STRETCH_TYPE_NAMES.has_key?(stretch.to_i)
346
+ Kernel.raise ArgumentError, 'Unknown stretch type'
347
+ end
348
+ primitive "font-stretch #{STRETCH_TYPE_NAMES[stretch.to_i]}"
349
+ end
350
+
351
+ def font_style(style)
352
+ unless STYLE_TYPE_NAMES.has_key?(style.to_i)
353
+ Kernel.raise ArgumentError, 'Unknown style type'
354
+ end
355
+ primitive "font-style #{STYLE_TYPE_NAMES[style.to_i]}"
356
+ end
357
+
358
+ # The font weight argument can be either a font weight
359
+ # constant or [100,200,...,900]
360
+ def font_weight(weight)
361
+ if FONT_WEIGHT_NAMES.has_key?(weight.to_i)
362
+ primitive "font-weight #{FONT_WEIGHT_NAMES[weight.to_i]}"
363
+ else
364
+ primitive "font-weight #{weight}"
365
+ end
366
+ end
367
+
368
+ # Specify the text positioning gravity, one of:
369
+ # NorthWest, North, NorthEast, West, Center, East, SouthWest, South, SouthEast
370
+ def gravity(grav)
371
+ unless GRAVITY_NAMES.has_key?(grav.to_i)
372
+ Kernel.raise ArgumentError, 'Unknown text positioning gravity'
373
+ end
374
+ primitive "gravity #{GRAVITY_NAMES[grav.to_i]}"
375
+ end
376
+
377
+ # IM 6.5.5-8 and later
378
+ def interline_spacing(space)
379
+ begin
380
+ Float(space)
381
+ rescue ArgumentError
382
+ Kernel.raise ArgumentError, 'invalid value for interline_spacing'
383
+ rescue TypeError
384
+ Kernel.raise TypeError, "can't convert #{space.class} into Float"
385
+ end
386
+ primitive "interline-spacing #{space}"
387
+ end
388
+
389
+ # IM 6.4.8-3 and later
390
+ def interword_spacing(space)
391
+ begin
392
+ Float(space)
393
+ rescue ArgumentError
394
+ Kernel.raise ArgumentError, 'invalid value for interword_spacing'
395
+ rescue TypeError
396
+ Kernel.raise TypeError, "can't convert #{space.class} into Float"
397
+ end
398
+ primitive "interword-spacing #{space}"
399
+ end
400
+
401
+ # IM 6.4.8-3 and later
402
+ def kerning(space)
403
+ begin
404
+ Float(space)
405
+ rescue ArgumentError
406
+ Kernel.raise ArgumentError, 'invalid value for kerning'
407
+ rescue TypeError
408
+ Kernel.raise TypeError, "can't convert #{space.class} into Float"
409
+ end
410
+ primitive "kerning #{space}"
411
+ end
412
+
413
+ # Draw a line
414
+ def line(startX, startY, endX, endY)
415
+ primitive 'line ' + sprintf('%g,%g %g,%g', startX, startY, endX, endY)
416
+ end
417
+
418
+ # Set matte (make transparent) in image according to the specified
419
+ # colorization rule
420
+ def matte(x, y, method)
421
+ unless PAINT_METHOD_NAMES.has_key?(method.to_i)
422
+ Kernel.raise ArgumentError, 'Unknown paint method'
423
+ end
424
+ primitive "matte #{x},#{y} #{PAINT_METHOD_NAMES[method.to_i]}"
425
+ end
426
+
427
+ # Specify drawing fill and stroke opacities. If the value is a string
428
+ # ending with a %, the number will be multiplied by 0.01.
429
+ def opacity(opacity)
430
+ if (Numeric === opacity)
431
+ if opacity < 0 || opacity > 1.0
432
+ Kernel.raise ArgumentError, 'opacity must be >= 0 and <= 1.0'
425
433
  end
426
-
427
- # Specify drawing fill and stroke opacities. If the value is a string
428
- # ending with a %, the number will be multiplied by 0.01.
429
- def opacity(opacity)
430
- if (Numeric === opacity)
431
- if opacity < 0 || opacity > 1.0
432
- Kernel.raise ArgumentError, 'opacity must be >= 0 and <= 1.0'
433
- end
434
+ end
435
+ primitive "opacity #{opacity}"
436
+ end
437
+
438
+ # Draw using SVG-compatible path drawing commands. Note that the
439
+ # primitive requires that the commands be surrounded by quotes or
440
+ # apostrophes. Here we simply use apostrophes.
441
+ def path(cmds)
442
+ primitive "path '" + cmds + "'"
443
+ end
444
+
445
+ # Define a pattern. In the block, call primitive methods to
446
+ # draw the pattern. Reference the pattern by using its name
447
+ # as the argument to the 'fill' or 'stroke' methods
448
+ def pattern(name, x, y, width, height)
449
+ push('defs')
450
+ push("pattern #{name} #{x} #{y} #{width} #{height}")
451
+ push('graphic-context')
452
+ yield
453
+ ensure
454
+ pop('graphic-context')
455
+ pop('pattern')
456
+ pop('defs')
457
+ end
458
+
459
+ # Set point to fill color.
460
+ def point(x, y)
461
+ primitive "point #{x},#{y}"
462
+ end
463
+
464
+ # Specify the font size in points. Yes, the primitive is "font-size" but
465
+ # in other places this value is called the "pointsize". Give it both names.
466
+ def pointsize(points)
467
+ primitive "font-size #{points}"
468
+ end
469
+ alias_method :font_size, :pointsize
470
+
471
+ # Draw a polygon
472
+ def polygon(*points)
473
+ if points.length == 0
474
+ Kernel.raise ArgumentError, 'no points specified'
475
+ elsif points.length.odd?
476
+ Kernel.raise ArgumentError, 'odd number of points specified'
477
+ end
478
+ primitive 'polygon ' + points.join(',')
479
+ end
480
+
481
+ # Draw a polyline
482
+ def polyline(*points)
483
+ if points.length == 0
484
+ Kernel.raise ArgumentError, 'no points specified'
485
+ elsif points.length.odd?
486
+ Kernel.raise ArgumentError, 'odd number of points specified'
487
+ end
488
+ primitive 'polyline ' + points.join(',')
489
+ end
490
+
491
+ # Return to the previously-saved set of whatever
492
+ # pop('graphic-context') (the default if no arguments)
493
+ # pop('defs')
494
+ # pop('gradient')
495
+ # pop('pattern')
496
+
497
+ def pop(*what)
498
+ if what.length == 0
499
+ primitive 'pop graphic-context'
500
+ else
501
+ # to_s allows a Symbol to be used instead of a String
502
+ primitive 'pop ' + what.map {|w| w.to_s}.join(' ')
503
+ end
504
+ end
505
+
506
+ # Push the current set of drawing options. Also you can use
507
+ # push('graphic-context') (the default if no arguments)
508
+ # push('defs')
509
+ # push('gradient')
510
+ # push('pattern')
511
+ def push(*what)
512
+ if what.length == 0
513
+ primitive 'push graphic-context'
514
+ else
515
+ # to_s allows a Symbol to be used instead of a String
516
+ primitive 'push ' + what.map {|w| w.to_s}.join(' ')
517
+ end
518
+ end
519
+
520
+ # Draw a rectangle
521
+ def rectangle(upper_left_x, upper_left_y, lower_right_x, lower_right_y)
522
+ primitive 'rectangle ' + sprintf('%g,%g %g,%g',
523
+ upper_left_x, upper_left_y, lower_right_x, lower_right_y)
524
+ end
525
+
526
+ # Specify coordinate space rotation. "angle" is measured in degrees
527
+ def rotate(angle)
528
+ primitive "rotate #{angle}"
529
+ end
530
+
531
+ # Draw a rectangle with rounded corners
532
+ def roundrectangle(center_x, center_y, width, height, corner_width, corner_height)
533
+ primitive 'roundrectangle ' + sprintf('%g,%g,%g,%g,%g,%g',
534
+ center_x, center_y, width, height, corner_width, corner_height)
535
+ end
536
+
537
+ # Specify scaling to be applied to coordinate space on subsequent drawing commands.
538
+ def scale(x, y)
539
+ primitive "scale #{x},#{y}"
540
+ end
541
+
542
+ def skewx(angle)
543
+ primitive "skewX #{angle}"
544
+ end
545
+
546
+ def skewy(angle)
547
+ primitive "skewY #{angle}"
548
+ end
549
+
550
+ # Specify the object stroke, a color name or pattern name.
551
+ def stroke(colorspec)
552
+ primitive "stroke #{enquote(colorspec)}"
553
+ end
554
+ alias_method :stroke_color, :stroke
555
+ alias_method :stroke_pattern, :stroke
556
+
557
+ # Specify if stroke should be antialiased or not
558
+ def stroke_antialias(bool)
559
+ bool = bool ? '1' : '0'
560
+ primitive "stroke-antialias #{bool}"
561
+ end
562
+
563
+ # Specify a stroke dash pattern
564
+ def stroke_dasharray(*list)
565
+ if list.length == 0
566
+ primitive 'stroke-dasharray none'
567
+ else
568
+ list.each do |x|
569
+ if x <= 0
570
+ Kernel.raise ArgumentError, "dash array elements must be > 0 (#{x} given)"
434
571
  end
435
- primitive "opacity #{opacity}"
436
572
  end
437
-
438
- # Draw using SVG-compatible path drawing commands. Note that the
439
- # primitive requires that the commands be surrounded by quotes or
440
- # apostrophes. Here we simply use apostrophes.
441
- def path(cmds)
442
- primitive "path '" + cmds + "'"
443
- end
444
-
445
- # Define a pattern. In the block, call primitive methods to
446
- # draw the pattern. Reference the pattern by using its name
447
- # as the argument to the 'fill' or 'stroke' methods
448
- def pattern(name, x, y, width, height)
449
- push('defs')
450
- push("pattern #{name} #{x} #{y} #{width} #{height}")
451
- push('graphic-context')
452
- yield
573
+ primitive "stroke-dasharray #{list.join(',')}"
574
+ end
575
+ end
576
+
577
+ # Specify the initial offset in the dash pattern
578
+ def stroke_dashoffset(value=0)
579
+ primitive "stroke-dashoffset #{value}"
580
+ end
581
+
582
+ def stroke_linecap(value)
583
+ unless ['butt', 'round', 'square'].include?(value.downcase)
584
+ Kernel.raise ArgumentError, "Unknown linecap type: #{value}"
585
+ end
586
+ primitive "stroke-linecap #{value}"
587
+ end
588
+
589
+ def stroke_linejoin(value)
590
+ unless ['round', 'miter', 'bevel'].include?(value.downcase)
591
+ Kernel.raise ArgumentError, "Unknown linejoin type: #{value}"
592
+ end
593
+ primitive "stroke-linejoin #{value}"
594
+ end
595
+
596
+ def stroke_miterlimit(value)
597
+ if value < 1
598
+ Kernel.raise ArgumentError, 'miterlimit must be >= 1'
599
+ end
600
+ primitive "stroke-miterlimit #{value}"
601
+ end
602
+
603
+ # Specify opacity of stroke drawing color
604
+ # (use "xx%" to indicate percentage)
605
+ def stroke_opacity(value)
606
+ primitive "stroke-opacity #{value}"
607
+ end
608
+
609
+ # Specify stroke (outline) width in pixels.
610
+ def stroke_width(pixels)
611
+ primitive "stroke-width #{pixels}"
612
+ end
613
+
614
+ # Draw text at position x,y. Add quotes to text that is not already quoted.
615
+ def text(x, y, text)
616
+ if text.to_s.empty?
617
+ Kernel.raise ArgumentError, 'missing text argument'
618
+ end
619
+ if text.length > 2 && /\A(?:\"[^\"]+\"|\'[^\']+\'|\{[^\}]+\})\z/.match(text)
620
+ ; # text already quoted
621
+ elsif !text['\'']
622
+ text = '\''+text+'\''
623
+ elsif !text['"']
624
+ text = '"'+text+'"'
625
+ elsif !(text['{'] || text['}'])
626
+ text = '{'+text+'}'
627
+ else
628
+ # escape existing braces, surround with braces
629
+ text = '{' + text.gsub(/[}]/) { |b| '\\' + b } + '}'
630
+ end
631
+ primitive "text #{x},#{y} #{text}"
632
+ end
633
+
634
+ # Specify text alignment relative to a given point
635
+ def text_align(alignment)
636
+ unless ALIGN_TYPE_NAMES.has_key?(alignment.to_i)
637
+ Kernel.raise ArgumentError, "Unknown alignment constant: #{alignment}"
638
+ end
639
+ primitive "text-align #{ALIGN_TYPE_NAMES[alignment.to_i]}"
640
+ end
641
+
642
+ # SVG-compatible version of text_align
643
+ def text_anchor(anchor)
644
+ unless ANCHOR_TYPE_NAMES.has_key?(anchor.to_i)
645
+ Kernel.raise ArgumentError, "Unknown anchor constant: #{anchor}"
646
+ end
647
+ primitive "text-anchor #{ANCHOR_TYPE_NAMES[anchor.to_i]}"
648
+ end
649
+
650
+ # Specify if rendered text is to be antialiased.
651
+ def text_antialias(boolean)
652
+ boolean = boolean ? '1' : '0'
653
+ primitive "text-antialias #{boolean}"
654
+ end
655
+
656
+ # Specify color underneath text
657
+ def text_undercolor(color)
658
+ primitive "text-undercolor #{enquote(color)}"
659
+ end
660
+
661
+ # Specify center of coordinate space to use for subsequent drawing
662
+ # commands.
663
+ def translate(x, y)
664
+ primitive "translate #{x},#{y}"
665
+ end
666
+ end # class Magick::Draw
667
+
668
+ # Define IPTC record number:dataset tags for use with Image#get_iptc_dataset
669
+ module IPTC
670
+ module Envelope
671
+ Model_Version = '1:00'
672
+ Destination = '1:05'
673
+ File_Format = '1:20'
674
+ File_Format_Version = '1:22'
675
+ Service_Identifier = '1:30'
676
+ Envelope_Number = '1:40'
677
+ Product_ID = '1:50'
678
+ Envelope_Priority = '1:60'
679
+ Date_Sent = '1:70'
680
+ Time_Sent = '1:80'
681
+ Coded_Character_Set = '1:90'
682
+ UNO = '1:100'
683
+ Unique_Name_of_Object = '1:100'
684
+ ARM_Identifier = '1:120'
685
+ ARM_Version = '1:122'
686
+ end
687
+
688
+ module Application
689
+ Record_Version = '2:00'
690
+ Object_Type_Reference = '2:03'
691
+ Object_Name = '2:05'
692
+ Title = '2:05'
693
+ Edit_Status = '2:07'
694
+ Editorial_Update = '2:08'
695
+ Urgency = '2:10'
696
+ Subject_Reference = '2:12'
697
+ Category = '2:15'
698
+ Supplemental_Category = '2:20'
699
+ Fixture_Identifier = '2:22'
700
+ Keywords = '2:25'
701
+ Content_Location_Code = '2:26'
702
+ Content_Location_Name = '2:27'
703
+ Release_Date = '2:30'
704
+ Release_Time = '2:35'
705
+ Expiration_Date = '2:37'
706
+ Expiration_Time = '2:35'
707
+ Special_Instructions = '2:40'
708
+ Action_Advised = '2:42'
709
+ Reference_Service = '2:45'
710
+ Reference_Date = '2:47'
711
+ Reference_Number = '2:50'
712
+ Date_Created = '2:55'
713
+ Time_Created = '2:60'
714
+ Digital_Creation_Date = '2:62'
715
+ Digital_Creation_Time = '2:63'
716
+ Originating_Program = '2:65'
717
+ Program_Version = '2:70'
718
+ Object_Cycle = '2:75'
719
+ By_Line = '2:80'
720
+ Author = '2:80'
721
+ By_Line_Title = '2:85'
722
+ Author_Position = '2:85'
723
+ City = '2:90'
724
+ Sub_Location = '2:92'
725
+ Province = '2:95'
726
+ State = '2:95'
727
+ Country_Primary_Location_Code = '2:100'
728
+ Country_Primary_Location_Name = '2:101'
729
+ Original_Transmission_Reference = '2:103'
730
+ Headline = '2:105'
731
+ Credit = '2:110'
732
+ Source = '2:115'
733
+ Copyright_Notice = '2:116'
734
+ Contact = '2:118'
735
+ Abstract = '2:120'
736
+ Caption = '2:120'
737
+ Editor = '2:122'
738
+ Caption_Writer = '2:122'
739
+ Rasterized_Caption = '2:125'
740
+ Image_Type = '2:130'
741
+ Image_Orientation = '2:131'
742
+ Language_Identifier = '2:135'
743
+ Audio_Type = '2:150'
744
+ Audio_Sampling_Rate = '2:151'
745
+ Audio_Sampling_Resolution = '2:152'
746
+ Audio_Duration = '2:153'
747
+ Audio_Outcue = '2:154'
748
+ ObjectData_Preview_File_Format = '2:200'
749
+ ObjectData_Preview_File_Format_Version = '2:201'
750
+ ObjectData_Preview_Data = '2:202'
751
+ end
752
+
753
+ module Pre_ObjectData_Descriptor
754
+ Size_Mode = '7:10'
755
+ Max_Subfile_Size = '7:20'
756
+ ObjectData_Size_Announced = '7:90'
757
+ Maximum_ObjectData_Size = '7:95'
758
+ end
759
+
760
+ module ObjectData
761
+ Subfile = '8:10'
762
+ end
763
+
764
+ module Post_ObjectData_Descriptor
765
+ Confirmed_ObjectData_Size = '9:10'
766
+ end
767
+
768
+ # Make all constants above immutable
769
+ constants.each do |record|
770
+ rec = const_get(record)
771
+ rec.constants.each { |ds| rec.const_get(ds).freeze }
772
+ end
773
+ end # module Magick::IPTC
774
+
775
+ # Ruby-level Magick::Image methods
776
+ class Image
777
+ include Comparable
778
+
779
+ alias_method :affinity, :remap
780
+
781
+ # Provide an alternate version of Draw#annotate, for folks who
782
+ # want to find it in this class.
783
+ def annotate(draw, width, height, x, y, text, &block)
784
+ check_destroyed
785
+ draw.annotate(self, width, height, x, y, text, &block)
786
+ self
787
+ end
788
+
789
+ # Set the color at x,y
790
+ def color_point(x, y, fill)
791
+ f = copy
792
+ f.pixel_color(x, y, fill)
793
+ f
794
+ end
795
+
796
+ # Set all pixels that have the same color as the pixel at x,y and
797
+ # are neighbors to the fill color
798
+ def color_floodfill(x, y, fill)
799
+ target = pixel_color(x, y)
800
+ color_flood_fill(target, fill, x, y, Magick::FloodfillMethod)
801
+ end
802
+
803
+ # Set all pixels that are neighbors of x,y and are not the border color
804
+ # to the fill color
805
+ def color_fill_to_border(x, y, fill)
806
+ color_flood_fill(border_color, fill, x, y, Magick::FillToBorderMethod)
807
+ end
808
+
809
+ # Set all pixels to the fill color. Very similar to Image#erase!
810
+ # Accepts either String or Pixel arguments
811
+ def color_reset!(fill)
812
+ save = background_color
813
+ # Change the background color _outside_ the begin block
814
+ # so that if this object is frozen the exeception will be
815
+ # raised before we have to handle it explicitly.
816
+ self.background_color = fill
817
+ begin
818
+ erase!
453
819
  ensure
454
- pop('graphic-context')
455
- pop('pattern')
456
- pop('defs')
457
- end
458
-
459
- # Set point to fill color.
460
- def point(x, y)
461
- primitive "point #{x},#{y}"
462
- end
463
-
464
- # Specify the font size in points. Yes, the primitive is "font-size" but
465
- # in other places this value is called the "pointsize". Give it both names.
466
- def pointsize(points)
467
- primitive "font-size #{points}"
468
- end
469
- alias_method :font_size, :pointsize
470
-
471
- # Draw a polygon
472
- def polygon(*points)
473
- if points.length == 0
474
- Kernel.raise ArgumentError, 'no points specified'
475
- elsif points.length.odd?
476
- Kernel.raise ArgumentError, 'odd number of points specified'
477
- end
478
- primitive 'polygon ' + points.join(',')
479
- end
480
-
481
- # Draw a polyline
482
- def polyline(*points)
483
- if points.length == 0
484
- Kernel.raise ArgumentError, 'no points specified'
485
- elsif points.length.odd?
486
- Kernel.raise ArgumentError, 'odd number of points specified'
487
- end
488
- primitive 'polyline ' + points.join(',')
489
- end
490
-
491
- # Return to the previously-saved set of whatever
492
- # pop('graphic-context') (the default if no arguments)
493
- # pop('defs')
494
- # pop('gradient')
495
- # pop('pattern')
496
-
497
- def pop(*what)
498
- if what.length == 0
499
- primitive 'pop graphic-context'
500
- else
501
- # to_s allows a Symbol to be used instead of a String
502
- primitive 'pop ' + what.map {|w| w.to_s}.join(' ')
503
- end
504
- end
505
-
506
- # Push the current set of drawing options. Also you can use
507
- # push('graphic-context') (the default if no arguments)
508
- # push('defs')
509
- # push('gradient')
510
- # push('pattern')
511
- def push(*what)
512
- if what.length == 0
513
- primitive 'push graphic-context'
514
- else
515
- # to_s allows a Symbol to be used instead of a String
516
- primitive 'push ' + what.map {|w| w.to_s}.join(' ')
517
- end
518
- end
519
-
520
- # Draw a rectangle
521
- def rectangle(upper_left_x, upper_left_y, lower_right_x, lower_right_y)
522
- primitive 'rectangle ' + sprintf('%g,%g %g,%g',
523
- upper_left_x, upper_left_y, lower_right_x, lower_right_y)
524
- end
525
-
526
- # Specify coordinate space rotation. "angle" is measured in degrees
527
- def rotate(angle)
528
- primitive "rotate #{angle}"
529
- end
530
-
531
- # Draw a rectangle with rounded corners
532
- def roundrectangle(center_x, center_y, width, height, corner_width, corner_height)
533
- primitive 'roundrectangle ' + sprintf('%g,%g,%g,%g,%g,%g',
534
- center_x, center_y, width, height, corner_width, corner_height)
535
- end
536
-
537
- # Specify scaling to be applied to coordinate space on subsequent drawing commands.
538
- def scale(x, y)
539
- primitive "scale #{x},#{y}"
540
- end
541
-
542
- def skewx(angle)
543
- primitive "skewX #{angle}"
544
- end
545
-
546
- def skewy(angle)
547
- primitive "skewY #{angle}"
548
- end
549
-
550
- # Specify the object stroke, a color name or pattern name.
551
- def stroke(colorspec)
552
- primitive "stroke #{enquote(colorspec)}"
553
- end
554
- alias_method :stroke_color, :stroke
555
- alias_method :stroke_pattern, :stroke
556
-
557
- # Specify if stroke should be antialiased or not
558
- def stroke_antialias(bool)
559
- bool = bool ? '1' : '0'
560
- primitive "stroke-antialias #{bool}"
561
- end
562
-
563
- # Specify a stroke dash pattern
564
- def stroke_dasharray(*list)
565
- if list.length == 0
566
- primitive 'stroke-dasharray none'
567
- else
568
- list.each do |x|
569
- if x <= 0
570
- Kernel.raise ArgumentError, "dash array elements must be > 0 (#{x} given)"
571
- end
572
- end
573
- primitive "stroke-dasharray #{list.join(',')}"
574
- end
575
- end
576
-
577
- # Specify the initial offset in the dash pattern
578
- def stroke_dashoffset(value=0)
579
- primitive "stroke-dashoffset #{value}"
580
- end
581
-
582
- def stroke_linecap(value)
583
- unless ['butt', 'round', 'square'].include?(value.downcase)
584
- Kernel.raise ArgumentError, "Unknown linecap type: #{value}"
585
- end
586
- primitive "stroke-linecap #{value}"
587
- end
588
-
589
- def stroke_linejoin(value)
590
- unless ['round', 'miter', 'bevel'].include?(value.downcase)
591
- Kernel.raise ArgumentError, "Unknown linejoin type: #{value}"
592
- end
593
- primitive "stroke-linejoin #{value}"
594
- end
595
-
596
- def stroke_miterlimit(value)
597
- if value < 1
598
- Kernel.raise ArgumentError, 'miterlimit must be >= 1'
599
- end
600
- primitive "stroke-miterlimit #{value}"
601
- end
602
-
603
- # Specify opacity of stroke drawing color
604
- # (use "xx%" to indicate percentage)
605
- def stroke_opacity(value)
606
- primitive "stroke-opacity #{value}"
607
- end
608
-
609
- # Specify stroke (outline) width in pixels.
610
- def stroke_width(pixels)
611
- primitive "stroke-width #{pixels}"
612
- end
613
-
614
- # Draw text at position x,y. Add quotes to text that is not already quoted.
615
- def text(x, y, text)
616
- if text.to_s.empty?
617
- Kernel.raise ArgumentError, 'missing text argument'
618
- end
619
- if text.length > 2 && /\A(?:\"[^\"]+\"|\'[^\']+\'|\{[^\}]+\})\z/.match(text)
620
- ; # text already quoted
621
- elsif !text['\'']
622
- text = '\''+text+'\''
623
- elsif !text['"']
624
- text = '"'+text+'"'
625
- elsif !(text['{'] || text['}'])
626
- text = '{'+text+'}'
627
- else
628
- # escape existing braces, surround with braces
629
- text = '{' + text.gsub(/[}]/) { |b| '\\' + b } + '}'
630
- end
631
- primitive "text #{x},#{y} #{text}"
632
- end
633
-
634
- # Specify text alignment relative to a given point
635
- def text_align(alignment)
636
- unless ALIGN_TYPE_NAMES.has_key?(alignment.to_i)
637
- Kernel.raise ArgumentError, "Unknown alignment constant: #{alignment}"
638
- end
639
- primitive "text-align #{ALIGN_TYPE_NAMES[alignment.to_i]}"
640
- end
641
-
642
- # SVG-compatible version of text_align
643
- def text_anchor(anchor)
644
- unless ANCHOR_TYPE_NAMES.has_key?(anchor.to_i)
645
- Kernel.raise ArgumentError, "Unknown anchor constant: #{anchor}"
646
- end
647
- primitive "text-anchor #{ANCHOR_TYPE_NAMES[anchor.to_i]}"
648
- end
649
-
650
- # Specify if rendered text is to be antialiased.
651
- def text_antialias(boolean)
652
- boolean = boolean ? '1' : '0'
653
- primitive "text-antialias #{boolean}"
654
- end
655
-
656
- # Specify color underneath text
657
- def text_undercolor(color)
658
- primitive "text-undercolor #{enquote(color)}"
659
- end
660
-
661
- # Specify center of coordinate space to use for subsequent drawing
662
- # commands.
663
- def translate(x, y)
664
- primitive "translate #{x},#{y}"
665
- end
666
- end # class Magick::Draw
667
-
668
- # Define IPTC record number:dataset tags for use with Image#get_iptc_dataset
669
- module IPTC
670
- module Envelope
671
- Model_Version = '1:00'
672
- Destination = '1:05'
673
- File_Format = '1:20'
674
- File_Format_Version = '1:22'
675
- Service_Identifier = '1:30'
676
- Envelope_Number = '1:40'
677
- Product_ID = '1:50'
678
- Envelope_Priority = '1:60'
679
- Date_Sent = '1:70'
680
- Time_Sent = '1:80'
681
- Coded_Character_Set = '1:90'
682
- UNO = '1:100'
683
- Unique_Name_of_Object = '1:100'
684
- ARM_Identifier = '1:120'
685
- ARM_Version = '1:122'
686
- end
687
-
688
- module Application
689
- Record_Version = '2:00'
690
- Object_Type_Reference = '2:03'
691
- Object_Name = '2:05'
692
- Title = '2:05'
693
- Edit_Status = '2:07'
694
- Editorial_Update = '2:08'
695
- Urgency = '2:10'
696
- Subject_Reference = '2:12'
697
- Category = '2:15'
698
- Supplemental_Category = '2:20'
699
- Fixture_Identifier = '2:22'
700
- Keywords = '2:25'
701
- Content_Location_Code = '2:26'
702
- Content_Location_Name = '2:27'
703
- Release_Date = '2:30'
704
- Release_Time = '2:35'
705
- Expiration_Date = '2:37'
706
- Expiration_Time = '2:35'
707
- Special_Instructions = '2:40'
708
- Action_Advised = '2:42'
709
- Reference_Service = '2:45'
710
- Reference_Date = '2:47'
711
- Reference_Number = '2:50'
712
- Date_Created = '2:55'
713
- Time_Created = '2:60'
714
- Digital_Creation_Date = '2:62'
715
- Digital_Creation_Time = '2:63'
716
- Originating_Program = '2:65'
717
- Program_Version = '2:70'
718
- Object_Cycle = '2:75'
719
- By_Line = '2:80'
720
- Author = '2:80'
721
- By_Line_Title = '2:85'
722
- Author_Position = '2:85'
723
- City = '2:90'
724
- Sub_Location = '2:92'
725
- Province = '2:95'
726
- State = '2:95'
727
- Country_Primary_Location_Code = '2:100'
728
- Country_Primary_Location_Name = '2:101'
729
- Original_Transmission_Reference = '2:103'
730
- Headline = '2:105'
731
- Credit = '2:110'
732
- Source = '2:115'
733
- Copyright_Notice = '2:116'
734
- Contact = '2:118'
735
- Abstract = '2:120'
736
- Caption = '2:120'
737
- Editor = '2:122'
738
- Caption_Writer = '2:122'
739
- Rasterized_Caption = '2:125'
740
- Image_Type = '2:130'
741
- Image_Orientation = '2:131'
742
- Language_Identifier = '2:135'
743
- Audio_Type = '2:150'
744
- Audio_Sampling_Rate = '2:151'
745
- Audio_Sampling_Resolution = '2:152'
746
- Audio_Duration = '2:153'
747
- Audio_Outcue = '2:154'
748
- ObjectData_Preview_File_Format = '2:200'
749
- ObjectData_Preview_File_Format_Version = '2:201'
750
- ObjectData_Preview_Data = '2:202'
751
- end
752
-
753
- module Pre_ObjectData_Descriptor
754
- Size_Mode = '7:10'
755
- Max_Subfile_Size = '7:20'
756
- ObjectData_Size_Announced = '7:90'
757
- Maximum_ObjectData_Size = '7:95'
758
- end
759
-
760
- module ObjectData
761
- Subfile = '8:10'
762
- end
763
-
764
- module Post_ObjectData_Descriptor
765
- Confirmed_ObjectData_Size = '9:10'
766
- end
767
-
768
- # Make all constants above immutable
769
- constants.each do |record|
770
- rec = const_get(record)
771
- rec.constants.each { |ds| rec.const_get(ds).freeze }
772
- end
773
- end # module Magick::IPTC
774
-
775
- # Ruby-level Magick::Image methods
776
- class Image
777
- include Comparable
778
-
779
- alias_method :affinity, :remap
780
-
781
- # Provide an alternate version of Draw#annotate, for folks who
782
- # want to find it in this class.
783
- def annotate(draw, width, height, x, y, text, &block)
784
- check_destroyed
785
- draw.annotate(self, width, height, x, y, text, &block)
786
- self
787
- end
788
-
789
- # Set the color at x,y
790
- def color_point(x, y, fill)
791
- f = copy
792
- f.pixel_color(x, y, fill)
793
- f
794
- end
795
-
796
- # Set all pixels that have the same color as the pixel at x,y and
797
- # are neighbors to the fill color
798
- def color_floodfill(x, y, fill)
799
- target = pixel_color(x, y)
800
- color_flood_fill(target, fill, x, y, Magick::FloodfillMethod)
801
- end
802
-
803
- # Set all pixels that are neighbors of x,y and are not the border color
804
- # to the fill color
805
- def color_fill_to_border(x, y, fill)
806
- color_flood_fill(border_color, fill, x, y, Magick::FillToBorderMethod)
807
- end
808
-
809
- # Set all pixels to the fill color. Very similar to Image#erase!
810
- # Accepts either String or Pixel arguments
811
- def color_reset!(fill)
812
- save = background_color
813
- # Change the background color _outside_ the begin block
814
- # so that if this object is frozen the exeception will be
815
- # raised before we have to handle it explicitly.
816
- self.background_color = fill
817
- begin
818
- erase!
819
- ensure
820
- self.background_color = save
821
- end
822
- self
823
- end
824
-
825
- # Used by ImageList methods - see ImageList#cur_image
826
- def cur_image
827
- self
828
- end
829
-
830
- # Thanks to Russell Norris!
831
- def each_pixel
832
- get_pixels(0, 0, columns, rows).each_with_index do |p, n|
833
- yield(p, n%columns, n/columns)
834
- end
835
- self
836
- end
837
-
838
- # Retrieve EXIF data by entry or all. If one or more entry names specified,
839
- # return the values associated with the entries. If no entries specified,
840
- # return all entries and values. The return value is an array of [name,value]
841
- # arrays.
842
- def get_exif_by_entry(*entry)
843
- ary = []
844
- if entry.length == 0
845
- exif_data = self['EXIF:*']
846
- if exif_data
847
- exif_data.split("\n").each { |exif| ary.push(exif.split('=')) }
848
- end
849
- else
850
- get_exif_by_entry # ensure properties is populated with exif data
851
- entry.each do |name|
852
- rval = self["EXIF:#{name}"]
853
- ary.push([name, rval])
854
- end
855
- end
856
- ary
857
- end
858
-
859
- # Retrieve EXIF data by tag number or all tag/value pairs. The return value is a hash.
860
- def get_exif_by_number(*tag)
861
- hash = {}
862
- if tag.length == 0
863
- exif_data = self['EXIF:!']
864
- if exif_data
865
- exif_data.split("\n").each do |exif|
866
- tag, value = exif.split('=')
867
- tag = tag[1,4].hex
868
- hash[tag] = value
869
- end
870
- end
871
- else
872
- get_exif_by_number # ensure properties is populated with exif data
873
- tag.each do |num|
874
- rval = self['#%04X' % num.to_i]
875
- hash[num] = rval == 'unknown' ? nil : rval
876
- end
877
- end
878
- hash
879
- end
880
-
881
- # Retrieve IPTC information by record number:dataset tag constant defined in
882
- # Magick::IPTC, above.
883
- def get_iptc_dataset(ds)
884
- self['IPTC:'+ds]
885
- end
886
-
887
- # Iterate over IPTC record number:dataset tags, yield for each non-nil dataset
888
- def each_iptc_dataset
889
- Magick::IPTC.constants.each do |record|
890
- rec = Magick::IPTC.const_get(record)
891
- rec.constants.each do |dataset|
892
- data_field = get_iptc_dataset(rec.const_get(dataset))
893
- yield(dataset, data_field) unless data_field.nil?
894
- end
895
- end
896
- nil
897
- end
898
-
899
- # Patches problematic change to the order of arguments in 1.11.0.
900
- # Before this release, the order was
901
- # black_point, gamma, white_point
902
- # RMagick 1.11.0 changed this to
903
- # black_point, white_point, gamma
904
- # This fix tries to determine if the arguments are in the old order and
905
- # if so, swaps the gamma and white_point arguments. Then it calls
906
- # level2, which simply accepts the arguments as given.
907
-
908
- # Inspect the gamma and white point values and swap them if they
909
- # look like they're in the old order.
910
-
911
- # (Thanks to Al Evans for the suggestion.)
912
- def level(black_point=0.0, white_point=nil, gamma=nil)
913
- black_point = Float(black_point)
914
-
915
- white_point ||= Magick::QuantumRange - black_point
916
- white_point = Float(white_point)
917
-
918
- gamma_arg = gamma
919
- gamma ||= 1.0
920
- gamma = Float(gamma)
921
-
922
- if gamma.abs > 10.0 || white_point.abs <= 10.0 || white_point.abs < gamma.abs
923
- gamma, white_point = white_point, gamma
924
- unless gamma_arg
925
- white_point = Magick::QuantumRange - black_point
926
- end
927
- end
928
-
929
- level2(black_point, white_point, gamma)
930
- end
931
-
932
- # These four methods are equivalent to the Draw#matte method
933
- # with the "Point", "Replace", "Floodfill", "FilltoBorder", and
934
- # "Replace" arguments, respectively.
935
-
936
- # Make the pixel at (x,y) transparent.
937
- def matte_point(x, y)
938
- f = copy
939
- f.opacity = OpaqueOpacity unless f.matte
940
- pixel = f.pixel_color(x,y)
941
- pixel.opacity = TransparentOpacity
942
- f.pixel_color(x, y, pixel)
943
- f
944
- end
945
-
946
- # Make transparent all pixels that are the same color as the
947
- # pixel at (x, y).
948
- def matte_replace(x, y)
949
- f = copy
950
- f.opacity = OpaqueOpacity unless f.matte
951
- target = f.pixel_color(x, y)
952
- f.transparent(target)
953
- end
954
-
955
- # Make transparent any pixel that matches the color of the pixel
956
- # at (x,y) and is a neighbor.
957
- def matte_floodfill(x, y)
958
- f = copy
959
- f.opacity = OpaqueOpacity unless f.matte
960
- target = f.pixel_color(x, y)
961
- f.matte_flood_fill(target, TransparentOpacity,
962
- x, y, FloodfillMethod)
963
- end
964
-
965
- # Make transparent any neighbor pixel that is not the border color.
966
- def matte_fill_to_border(x, y)
967
- f = copy
968
- f.opacity = Magick::OpaqueOpacity unless f.matte
969
- f.matte_flood_fill(border_color, TransparentOpacity,
970
- x, y, FillToBorderMethod)
971
- end
972
-
973
- # Make all pixels transparent.
974
- def matte_reset!
975
- self.opacity = Magick::TransparentOpacity
976
- self
977
- end
978
-
979
- # Force an image to exact dimensions without changing the aspect ratio.
980
- # Resize and crop if necessary. (Thanks to Jerett Taylor!)
981
- def resize_to_fill(ncols, nrows=nil, gravity=CenterGravity)
982
- copy.resize_to_fill!(ncols, nrows, gravity)
983
- end
984
-
985
- def resize_to_fill!(ncols, nrows=nil, gravity=CenterGravity)
986
- nrows ||= ncols
987
- if ncols != columns || nrows != rows
988
- scale = [ncols/columns.to_f, nrows/rows.to_f].max
989
- resize!(scale*columns+0.5, scale*rows+0.5)
990
- end
991
- crop!(gravity, ncols, nrows, true) if ncols != columns || nrows != rows
992
- self
993
- end
994
-
995
- # Preserve aliases used < RMagick 2.0.1
996
- alias_method :crop_resized, :resize_to_fill
997
- alias_method :crop_resized!, :resize_to_fill!
998
-
999
- # Convenience method to resize retaining the aspect ratio.
1000
- # (Thanks to Robert Manni!)
1001
- def resize_to_fit(cols, rows=nil)
1002
- rows ||= cols
1003
- change_geometry(Geometry.new(cols, rows)) do |ncols, nrows|
1004
- resize(ncols, nrows)
1005
- end
1006
- end
1007
-
1008
- def resize_to_fit!(cols, rows=nil)
1009
- rows ||= cols
1010
- change_geometry(Geometry.new(cols, rows)) do |ncols, nrows|
1011
- resize!(ncols, nrows)
1012
- end
1013
- end
1014
-
1015
- # Replace matching neighboring pixels with texture pixels
1016
- def texture_floodfill(x, y, texture)
1017
- target = pixel_color(x, y)
1018
- texture_flood_fill(target, texture, x, y, FloodfillMethod)
1019
- end
1020
-
1021
- # Replace neighboring pixels to border color with texture pixels
1022
- def texture_fill_to_border(x, y, texture)
1023
- texture_flood_fill(border_color, texture, x, y, FillToBorderMethod)
1024
- end
1025
-
1026
- # Construct a view. If a block is present, yield and pass the view
1027
- # object, otherwise return the view object.
1028
- def view(x, y, width, height)
1029
- view = View.new(self, x, y, width, height)
1030
-
1031
- if block_given?
1032
- begin
1033
- yield(view)
1034
- ensure
1035
- view.sync
1036
- end
1037
- return nil
1038
- else
1039
- return view
1040
- end
1041
- end
1042
-
1043
- # Magick::Image::View class
1044
- class View
1045
- attr_reader :x, :y, :width, :height
1046
- attr_accessor :dirty
1047
-
1048
- def initialize(img, x, y, width, height)
1049
- img.check_destroyed
1050
- if width <= 0 || height <= 0
1051
- Kernel.raise ArgumentError, "invalid geometry (#{width}x#{height}+#{x}+#{y})"
1052
- end
1053
- if x < 0 || y < 0 || (x+width) > img.columns || (y+height) > img.rows
1054
- Kernel.raise RangeError, "geometry (#{width}x#{height}+#{x}+#{y}) exceeds image boundary"
1055
- end
1056
- @view = img.get_pixels(x, y, width, height)
1057
- @img = img
1058
- @x = x
1059
- @y = y
1060
- @width = width
1061
- @height = height
1062
- @dirty = false
1063
- end
1064
-
1065
- def [](*args)
1066
- rows = Rows.new(@view, @width, @height, args)
1067
- rows.add_observer(self)
1068
- rows
1069
- end
1070
-
1071
- # Store changed pixels back to image
1072
- def sync(force=false)
1073
- @img.store_pixels(x, y, width, height, @view) if @dirty || force
1074
- @dirty || force
1075
- end
1076
-
1077
- # Get update from Rows - if @dirty ever becomes
1078
- # true, don't change it back to false!
1079
- def update(rows)
1080
- @dirty = true
1081
- rows.delete_observer(self) # No need to tell us again.
1082
- nil
1083
- end
1084
-
1085
- # Magick::Image::View::Pixels
1086
- # Defines channel attribute getters/setters
1087
- class Pixels < Array
1088
- include Observable
1089
-
1090
- # Define a getter and a setter for each channel.
1091
- [:red, :green, :blue, :opacity].each do |c|
1092
- module_eval <<-END_EVAL
1093
- def #{c}
1094
- return collect { |p| p.#{c} }
1095
- end
1096
- def #{c}=(v)
1097
- each { |p| p.#{c} = v }
1098
- changed
1099
- notify_observers(self)
1100
- nil
1101
- end
1102
- END_EVAL
1103
- end
1104
- end # class Magick::Image::View::Pixels
1105
-
1106
- # Magick::Image::View::Rows
1107
- class Rows
1108
- include Observable
1109
-
1110
- def initialize(view, width, height, rows)
1111
- @view = view
1112
- @width = width
1113
- @height = height
1114
- @rows = rows
1115
- end
1116
-
1117
- def [](*args)
1118
- cols(args)
1119
-
1120
- # Both View::Pixels and Magick::Pixel implement Observable
1121
- if @unique
1122
- pixels = @view[@rows[0]*@width + @cols[0]]
1123
- pixels.add_observer(self)
1124
- else
1125
- pixels = View::Pixels.new
1126
- each do |x|
1127
- p = @view[x]
1128
- p.add_observer(self)
1129
- pixels << p
1130
- end
1131
- end
1132
- pixels
1133
- end
1134
-
1135
- def []=(*args)
1136
- rv = args.delete_at(-1) # get rvalue
1137
- unless rv.is_a?(Pixel) # must be a Pixel or a color name
1138
- begin
1139
- rv = Pixel.from_color(rv)
1140
- rescue TypeError
1141
- Kernel.raise TypeError, "cannot convert #{rv.class} into Pixel"
1142
- end
1143
- end
1144
- cols(args)
1145
- each { |x| @view[x] = rv.dup }
1146
- changed
1147
- notify_observers(self)
1148
- nil
1149
- end
1150
-
1151
- # A pixel has been modified. Tell the view.
1152
- def update(pixel)
1153
- changed
1154
- notify_observers(self)
1155
- pixel.delete_observer(self) # Don't need to hear again.
1156
- nil
1157
- end
1158
-
1159
- private
1160
-
1161
- def cols(*args)
1162
- @cols = args[0] # remove the outermost array
1163
- @unique = false
1164
-
1165
- # Convert @rows to an Enumerable object
1166
- case @rows.length
1167
- when 0 # Create a Range for all the rows
1168
- @rows = Range.new(0, @height, true)
1169
- when 1 # Range, Array, or a single integer
1170
- # if the single element is already an Enumerable
1171
- # object, get it.
1172
- if @rows.first.respond_to? :each
1173
- @rows = @rows.first
1174
- else
1175
- @rows = Integer(@rows.first)
1176
- if @rows < 0
1177
- @rows += @height
1178
- end
1179
- if @rows < 0 || @rows > @height-1
1180
- Kernel.raise IndexError, "index [#{@rows}] out of range"
1181
- end
1182
- # Convert back to an array
1183
- @rows = Array.new(1, @rows)
1184
- @unique = true
1185
- end
1186
- when 2
1187
- # A pair of integers representing the starting column and the number of columns
1188
- start = Integer(@rows[0])
1189
- length = Integer(@rows[1])
1190
-
1191
- # Negative start -> start from last row
1192
- if start < 0
1193
- start += @height
1194
- end
1195
-
1196
- if start > @height || start < 0 || length < 0
1197
- Kernel.raise IndexError, "index [#{@rows.first}] out of range"
1198
- else
1199
- if start + length > @height
1200
- length = @height - length
1201
- length = [length, 0].max
1202
- end
1203
- end
1204
- # Create a Range for the specified set of rows
1205
- @rows = Range.new(start, start+length, true)
1206
- end
1207
-
1208
- case @cols.length
1209
- when 0 # all rows
1210
- @cols = Range.new(0, @width, true) # convert to range
1211
- @unique = false
1212
- when 1 # Range, Array, or a single integer
1213
- # if the single element is already an Enumerable
1214
- # object, get it.
1215
- if @cols.first.respond_to? :each
1216
- @cols = @cols.first
1217
- @unique = false
1218
- else
1219
- @cols = Integer(@cols.first)
1220
- if @cols < 0
1221
- @cols += @width
1222
- end
1223
- if @cols < 0 || @cols > @width-1
1224
- Kernel.raise IndexError, "index [#{@cols}] out of range"
1225
- end
1226
- # Convert back to array
1227
- @cols = Array.new(1, @cols)
1228
- @unique &&= true
1229
- end
1230
- when 2
1231
- # A pair of integers representing the starting column and the number of columns
1232
- start = Integer(@cols[0])
1233
- length = Integer(@cols[1])
1234
-
1235
- # Negative start -> start from last row
1236
- if start < 0
1237
- start += @width
1238
- end
1239
-
1240
- if start > @width || start < 0 || length < 0
1241
- ; #nop
1242
- else
1243
- if start + length > @width
1244
- length = @width - length
1245
- length = [length, 0].max
1246
- end
1247
- end
1248
- # Create a Range for the specified set of columns
1249
- @cols = Range.new(start, start+length, true)
1250
- @unique = false
1251
- end
1252
- end
1253
-
1254
- # iterator called from subscript methods
1255
- def each
1256
- maxrows = @height - 1
1257
- maxcols = @width - 1
1258
-
1259
- @rows.each do |j|
1260
- if j > maxrows
1261
- Kernel.raise IndexError, "index [#{j}] out of range"
1262
- end
1263
- @cols.each do |i|
1264
- if i > maxcols
1265
- Kernel.raise IndexError, "index [#{i}] out of range"
1266
- end
1267
- yield j*@width + i
1268
- end
1269
- end
1270
- nil # useless return value
1271
- end
1272
- end # class Magick::Image::View::Rows
1273
- end # class Magick::Image::View
1274
- end # class Magick::Image
1275
-
1276
- class ImageList
1277
- include Comparable
1278
- include Enumerable
1279
- attr_reader :scene
1280
-
1281
- private
1282
-
1283
- def get_current
1284
- return @images[@scene].__id__ rescue nil
1285
- end
1286
-
1287
- protected
1288
-
1289
- def is_an_image(obj)
1290
- unless obj.is_a? Magick::Image
1291
- Kernel.raise ArgumentError, "Magick::Image required (#{obj.class} given)"
1292
- end
1293
- true
1294
- end
1295
-
1296
- # Ensure array is always an array of Magick::Image objects
1297
- def is_an_image_array(ary)
1298
- unless ary.respond_to? :each
1299
- Kernel.raise ArgumentError, "Magick::ImageList or array of Magick::Images required (#{ary.class} given)"
1300
- end
1301
- ary.each { |obj| is_an_image obj }
1302
- true
1303
- end
1304
-
1305
- # Find old current image, update scene number
1306
- # current is the id of the old current image.
1307
- def set_current(current)
1308
- if length == 0
1309
- self.scene = nil
1310
- return
1311
- # Don't bother looking for current image
1312
- elsif scene.nil? || scene >= length
1313
- self.scene = length - 1
1314
- return
1315
- elsif !current.nil?
1316
- # Find last instance of "current" in the list.
1317
- # If "current" isn't in the list, set current to last image.
1318
- self.scene = length - 1
1319
- each_with_index do |f,i|
1320
- if f.__id__ == current
1321
- self.scene = i
1322
- end
1323
- end
1324
- return
1325
- end
1326
- self.scene = length - 1
1327
- end
1328
-
1329
- public
1330
-
1331
- # Allow scene to be set to nil
1332
- def scene=(n)
1333
- if n.nil?
1334
- Kernel.raise IndexError, 'scene number out of bounds' unless @images.length == 0
1335
- @scene = nil
1336
- return @scene
1337
- elsif @images.length == 0
1338
- Kernel.raise IndexError, 'scene number out of bounds'
1339
- end
1340
-
1341
- n = Integer(n)
1342
- if n < 0 || n > length - 1
1343
- Kernel.raise IndexError, 'scene number out of bounds'
1344
- end
1345
- @scene = n
1346
- @scene
1347
- end
1348
-
1349
- # All the binary operators work the same way.
1350
- # 'other' should be either an ImageList or an Array
1351
- %w{& + - |}.each do |op|
1352
- module_eval <<-END_BINOPS
1353
- def #{op}(other)
1354
- ilist = self.class.new
1355
- begin
1356
- a = other #{op} @images
1357
- rescue TypeError
1358
- Kernel.raise ArgumentError, "Magick::ImageList expected, got " + other.class.to_s
1359
- end
1360
- current = get_current()
1361
- a.each do |image|
1362
- is_an_image image
1363
- ilist << image
1364
- end
1365
- ilist.set_current current
1366
- return ilist
1367
- end
1368
- END_BINOPS
1369
- end
1370
-
1371
- def *(n)
1372
- unless n.is_a? Integer
1373
- Kernel.raise ArgumentError, "Integer required (#{n.class} given)"
1374
- end
1375
- current = get_current
1376
- ilist = self.class.new
1377
- (@images * n).each {|image| ilist << image}
1378
- ilist.set_current current
1379
- ilist
1380
- end
1381
-
1382
- def <<(obj)
1383
- is_an_image obj
1384
- @images << obj
1385
- @scene = @images.length - 1
1386
- self
1387
- end
1388
-
1389
- # Compare ImageLists
1390
- # Compare each image in turn until the result of a comparison
1391
- # is not 0. If all comparisons return 0, then
1392
- # return if A.scene != B.scene
1393
- # return A.length <=> B.length
1394
- def <=>(other)
1395
- unless other.is_a? self.class
1396
- Kernel.raise TypeError, "#{self.class} required (#{other.class} given)"
1397
- end
1398
- size = [length, other.length].min
1399
- size.times do |x|
1400
- r = self[x] <=> other[x]
1401
- return r unless r == 0
1402
- end
1403
- if @scene.nil? && other.scene.nil?
1404
- return 0
1405
- elsif @scene.nil? && !other.scene.nil?
1406
- Kernel.raise TypeError, "cannot convert nil into #{other.scene.class}"
1407
- elsif ! @scene.nil? && other.scene.nil?
1408
- Kernel.raise TypeError, "cannot convert nil into #{scene.class}"
1409
- end
1410
- r = scene <=> other.scene
1411
- return r unless r == 0
1412
- length <=> other.length
1413
- end
1414
-
1415
- def [](*args)
1416
- a = @images[*args]
1417
- if a.respond_to?(:each)
1418
- ilist = self.class.new
1419
- a.each {|image| ilist << image}
1420
- a = ilist
1421
- end
1422
- a
1423
- end
1424
-
1425
- def []=(*args)
1426
- obj = @images.[]=(*args)
1427
- if obj && obj.respond_to?(:each)
1428
- is_an_image_array(obj)
1429
- set_current obj.last.__id__
1430
- elsif obj
1431
- is_an_image(obj)
1432
- set_current obj.__id__
1433
- else
1434
- set_current nil
1435
- end
1436
- obj
1437
- end
1438
-
1439
- [:at, :each, :each_index, :empty?, :fetch,
1440
- :first, :hash, :include?, :index, :length, :rindex, :sort!].each do |mth|
1441
- module_eval <<-END_SIMPLE_DELEGATES
1442
- def #{mth}(*args, &block)
1443
- @images.#{mth}(*args, &block)
1444
- end
1445
- END_SIMPLE_DELEGATES
1446
- end
1447
- alias_method :size, :length
1448
-
1449
- # Array#nitems is not available in 1.9
1450
- if Array.instance_methods.include?('nitems')
1451
- def nitems
1452
- @images.nitems
1453
- end
1454
- end
1455
-
1456
- def clear
1457
- @scene = nil
1458
- @images.clear
1459
- end
1460
-
1461
- def clone
1462
- ditto = dup
1463
- ditto.freeze if frozen?
1464
- ditto
1465
- end
1466
-
1467
- # override Enumerable#collect
1468
- def collect(&block)
1469
- current = get_current
1470
- a = @images.collect(&block)
1471
- ilist = self.class.new
1472
- a.each {|image| ilist << image}
1473
- ilist.set_current current
1474
- ilist
1475
- end
1476
-
1477
- def collect!(&block)
1478
- @images.collect!(&block)
1479
- is_an_image_array @images
1480
- self
1481
- end
1482
-
1483
- # Make a deep copy
1484
- def copy
1485
- ditto = self.class.new
1486
- @images.each { |f| ditto << f.copy }
1487
- ditto.scene = @scene
1488
- ditto.taint if tainted?
1489
- ditto
1490
- end
1491
-
1492
- # Return the current image
1493
- def cur_image
1494
- unless @scene
1495
- Kernel.raise IndexError, 'no images in this list'
1496
- end
1497
- @images[@scene]
1498
- end
1499
-
1500
- # ImageList#map took over the "map" name. Use alternatives.
1501
- alias_method :__map__, :collect
1502
- alias_method :map!, :collect!
1503
- alias_method :__map__!, :collect!
1504
-
1505
- # ImageMagic used affinity in 6.4.3, switch to remap in 6.4.4.
1506
- alias_method :affinity, :remap
1507
-
1508
- def compact
1509
- current = get_current
1510
- ilist = self.class.new
1511
- a = @images.compact
1512
- a.each {|image| ilist << image}
1513
- ilist.set_current current
1514
- ilist
1515
- end
1516
-
1517
- def compact!
1518
- current = get_current
1519
- a = @images.compact! # returns nil if no changes were made
1520
- set_current current
1521
- a.nil? ? nil : self
820
+ self.background_color = save
821
+ end
822
+ self
823
+ end
824
+
825
+ # Used by ImageList methods - see ImageList#cur_image
826
+ def cur_image
827
+ self
828
+ end
829
+
830
+ # Thanks to Russell Norris!
831
+ def each_pixel
832
+ get_pixels(0, 0, columns, rows).each_with_index do |p, n|
833
+ yield(p, n%columns, n/columns)
834
+ end
835
+ self
836
+ end
837
+
838
+ # Retrieve EXIF data by entry or all. If one or more entry names specified,
839
+ # return the values associated with the entries. If no entries specified,
840
+ # return all entries and values. The return value is an array of [name,value]
841
+ # arrays.
842
+ def get_exif_by_entry(*entry)
843
+ ary = []
844
+ if entry.length == 0
845
+ exif_data = self['EXIF:*']
846
+ if exif_data
847
+ exif_data.split("\n").each { |exif| ary.push(exif.split('=')) }
1522
848
  end
1523
-
1524
- def concat(other)
1525
- is_an_image_array other
1526
- other.each {|image| @images << image}
1527
- @scene = length-1
1528
- self
849
+ else
850
+ get_exif_by_entry # ensure properties is populated with exif data
851
+ entry.each do |name|
852
+ rval = self["EXIF:#{name}"]
853
+ ary.push([name, rval])
1529
854
  end
1530
-
1531
- # Set same delay for all images
1532
- def delay=(d)
1533
- if Integer(d) < 0
1534
- fail ArgumentError, 'delay must be greater than or equal to 0'
855
+ end
856
+ ary
857
+ end
858
+
859
+ # Retrieve EXIF data by tag number or all tag/value pairs. The return value is a hash.
860
+ def get_exif_by_number(*tag)
861
+ hash = {}
862
+ if tag.length == 0
863
+ exif_data = self['EXIF:!']
864
+ if exif_data
865
+ exif_data.split("\n").each do |exif|
866
+ tag, value = exif.split('=')
867
+ tag = tag[1,4].hex
868
+ hash[tag] = value
1535
869
  end
1536
- @images.each { |f| f.delay = Integer(d) }
1537
870
  end
1538
-
1539
- def delete(obj, &block)
1540
- is_an_image obj
1541
- current = get_current
1542
- a = @images.delete(obj, &block)
1543
- set_current current
1544
- a
1545
- end
1546
-
1547
- def delete_at(ndx)
1548
- current = get_current
1549
- a = @images.delete_at(ndx)
1550
- set_current current
1551
- a
1552
- end
1553
-
1554
- def delete_if(&block)
1555
- current = get_current
1556
- @images.delete_if(&block)
1557
- set_current current
1558
- self
871
+ else
872
+ get_exif_by_number # ensure properties is populated with exif data
873
+ tag.each do |num|
874
+ rval = self['#%04X' % num.to_i]
875
+ hash[num] = rval == 'unknown' ? nil : rval
1559
876
  end
1560
-
1561
- def dup
1562
- ditto = self.class.new
1563
- @images.each {|img| ditto << img}
1564
- ditto.scene = @scene
1565
- ditto.taint if tainted?
1566
- ditto
877
+ end
878
+ hash
879
+ end
880
+
881
+ # Retrieve IPTC information by record number:dataset tag constant defined in
882
+ # Magick::IPTC, above.
883
+ def get_iptc_dataset(ds)
884
+ self['IPTC:'+ds]
885
+ end
886
+
887
+ # Iterate over IPTC record number:dataset tags, yield for each non-nil dataset
888
+ def each_iptc_dataset
889
+ Magick::IPTC.constants.each do |record|
890
+ rec = Magick::IPTC.const_get(record)
891
+ rec.constants.each do |dataset|
892
+ data_field = get_iptc_dataset(rec.const_get(dataset))
893
+ yield(dataset, data_field) unless data_field.nil?
1567
894
  end
1568
-
1569
- def eql?(other)
1570
- is_an_image_array other
1571
- eql = other.eql?(@images)
1572
- begin # "other" is another ImageList
1573
- eql &&= @scene == other.scene
1574
- rescue NoMethodError
1575
- # "other" is a plain Array
1576
- end
1577
- eql
895
+ end
896
+ nil
897
+ end
898
+
899
+ # Patches problematic change to the order of arguments in 1.11.0.
900
+ # Before this release, the order was
901
+ # black_point, gamma, white_point
902
+ # RMagick 1.11.0 changed this to
903
+ # black_point, white_point, gamma
904
+ # This fix tries to determine if the arguments are in the old order and
905
+ # if so, swaps the gamma and white_point arguments. Then it calls
906
+ # level2, which simply accepts the arguments as given.
907
+
908
+ # Inspect the gamma and white point values and swap them if they
909
+ # look like they're in the old order.
910
+
911
+ # (Thanks to Al Evans for the suggestion.)
912
+ def level(black_point=0.0, white_point=nil, gamma=nil)
913
+ black_point = Float(black_point)
914
+
915
+ white_point ||= Magick::QuantumRange - black_point
916
+ white_point = Float(white_point)
917
+
918
+ gamma_arg = gamma
919
+ gamma ||= 1.0
920
+ gamma = Float(gamma)
921
+
922
+ if gamma.abs > 10.0 || white_point.abs <= 10.0 || white_point.abs < gamma.abs
923
+ gamma, white_point = white_point, gamma
924
+ unless gamma_arg
925
+ white_point = Magick::QuantumRange - black_point
1578
926
  end
1579
-
1580
- def fill(*args, &block)
1581
- is_an_image args[0] unless block_given?
1582
- current = get_current
1583
- @images.fill(*args, &block)
1584
- is_an_image_array self
1585
- set_current current
1586
- self
927
+ end
928
+
929
+ level2(black_point, white_point, gamma)
930
+ end
931
+
932
+ # These four methods are equivalent to the Draw#matte method
933
+ # with the "Point", "Replace", "Floodfill", "FilltoBorder", and
934
+ # "Replace" arguments, respectively.
935
+
936
+ # Make the pixel at (x,y) transparent.
937
+ def matte_point(x, y)
938
+ f = copy
939
+ f.opacity = OpaqueOpacity unless f.matte
940
+ pixel = f.pixel_color(x,y)
941
+ pixel.opacity = TransparentOpacity
942
+ f.pixel_color(x, y, pixel)
943
+ f
944
+ end
945
+
946
+ # Make transparent all pixels that are the same color as the
947
+ # pixel at (x, y).
948
+ def matte_replace(x, y)
949
+ f = copy
950
+ f.opacity = OpaqueOpacity unless f.matte
951
+ target = f.pixel_color(x, y)
952
+ f.transparent(target)
953
+ end
954
+
955
+ # Make transparent any pixel that matches the color of the pixel
956
+ # at (x,y) and is a neighbor.
957
+ def matte_floodfill(x, y)
958
+ f = copy
959
+ f.opacity = OpaqueOpacity unless f.matte
960
+ target = f.pixel_color(x, y)
961
+ f.matte_flood_fill(target, TransparentOpacity,
962
+ x, y, FloodfillMethod)
963
+ end
964
+
965
+ # Make transparent any neighbor pixel that is not the border color.
966
+ def matte_fill_to_border(x, y)
967
+ f = copy
968
+ f.opacity = Magick::OpaqueOpacity unless f.matte
969
+ f.matte_flood_fill(border_color, TransparentOpacity,
970
+ x, y, FillToBorderMethod)
971
+ end
972
+
973
+ # Make all pixels transparent.
974
+ def matte_reset!
975
+ self.opacity = Magick::TransparentOpacity
976
+ self
977
+ end
978
+
979
+ # Force an image to exact dimensions without changing the aspect ratio.
980
+ # Resize and crop if necessary. (Thanks to Jerett Taylor!)
981
+ def resize_to_fill(ncols, nrows=nil, gravity=CenterGravity)
982
+ copy.resize_to_fill!(ncols, nrows, gravity)
983
+ end
984
+
985
+ def resize_to_fill!(ncols, nrows=nil, gravity=CenterGravity)
986
+ nrows ||= ncols
987
+ if ncols != columns || nrows != rows
988
+ scale = [ncols/columns.to_f, nrows/rows.to_f].max
989
+ resize!(scale*columns+0.5, scale*rows+0.5)
990
+ end
991
+ crop!(gravity, ncols, nrows, true) if ncols != columns || nrows != rows
992
+ self
993
+ end
994
+
995
+ # Preserve aliases used < RMagick 2.0.1
996
+ alias_method :crop_resized, :resize_to_fill
997
+ alias_method :crop_resized!, :resize_to_fill!
998
+
999
+ # Convenience method to resize retaining the aspect ratio.
1000
+ # (Thanks to Robert Manni!)
1001
+ def resize_to_fit(cols, rows=nil)
1002
+ rows ||= cols
1003
+ change_geometry(Geometry.new(cols, rows)) do |ncols, nrows|
1004
+ resize(ncols, nrows)
1005
+ end
1006
+ end
1007
+
1008
+ def resize_to_fit!(cols, rows=nil)
1009
+ rows ||= cols
1010
+ change_geometry(Geometry.new(cols, rows)) do |ncols, nrows|
1011
+ resize!(ncols, nrows)
1012
+ end
1013
+ end
1014
+
1015
+ # Replace matching neighboring pixels with texture pixels
1016
+ def texture_floodfill(x, y, texture)
1017
+ target = pixel_color(x, y)
1018
+ texture_flood_fill(target, texture, x, y, FloodfillMethod)
1019
+ end
1020
+
1021
+ # Replace neighboring pixels to border color with texture pixels
1022
+ def texture_fill_to_border(x, y, texture)
1023
+ texture_flood_fill(border_color, texture, x, y, FillToBorderMethod)
1024
+ end
1025
+
1026
+ # Construct a view. If a block is present, yield and pass the view
1027
+ # object, otherwise return the view object.
1028
+ def view(x, y, width, height)
1029
+ view = View.new(self, x, y, width, height)
1030
+
1031
+ if block_given?
1032
+ begin
1033
+ yield(view)
1034
+ ensure
1035
+ view.sync
1587
1036
  end
1588
-
1589
- # Override Enumerable's find_all
1590
- def find_all(&block)
1591
- current = get_current
1592
- a = @images.find_all(&block)
1593
- ilist = self.class.new
1594
- a.each {|image| ilist << image}
1595
- ilist.set_current current
1596
- ilist
1037
+ return nil
1038
+ else
1039
+ return view
1040
+ end
1041
+ end
1042
+
1043
+ # Magick::Image::View class
1044
+ class View
1045
+ attr_reader :x, :y, :width, :height
1046
+ attr_accessor :dirty
1047
+
1048
+ def initialize(img, x, y, width, height)
1049
+ img.check_destroyed
1050
+ if width <= 0 || height <= 0
1051
+ Kernel.raise ArgumentError, "invalid geometry (#{width}x#{height}+#{x}+#{y})"
1597
1052
  end
1598
- alias_method :select, :find_all
1599
-
1600
- def from_blob(*blobs, &block)
1601
- if (blobs.length == 0)
1602
- Kernel.raise ArgumentError, 'no blobs given'
1603
- end
1604
- blobs.each do |b|
1605
- Magick::Image.from_blob(b, &block).each { |n| @images << n }
1606
- end
1607
- @scene = length - 1
1608
- self
1053
+ if x < 0 || y < 0 || (x+width) > img.columns || (y+height) > img.rows
1054
+ Kernel.raise RangeError, "geometry (#{width}x#{height}+#{x}+#{y}) exceeds image boundary"
1609
1055
  end
1610
-
1611
- # Initialize new instances
1612
- def initialize(*filenames, &block)
1613
- @images = []
1614
- @scene = nil
1615
- filenames.each do |f|
1616
- Magick::Image.read(f, &block).each { |n| @images << n }
1056
+ @view = img.get_pixels(x, y, width, height)
1057
+ @img = img
1058
+ @x = x
1059
+ @y = y
1060
+ @width = width
1061
+ @height = height
1062
+ @dirty = false
1063
+ end
1064
+
1065
+ def [](*args)
1066
+ rows = Rows.new(@view, @width, @height, args)
1067
+ rows.add_observer(self)
1068
+ rows
1069
+ end
1070
+
1071
+ # Store changed pixels back to image
1072
+ def sync(force=false)
1073
+ @img.store_pixels(x, y, width, height, @view) if @dirty || force
1074
+ @dirty || force
1075
+ end
1076
+
1077
+ # Get update from Rows - if @dirty ever becomes
1078
+ # true, don't change it back to false!
1079
+ def update(rows)
1080
+ @dirty = true
1081
+ rows.delete_observer(self) # No need to tell us again.
1082
+ nil
1083
+ end
1084
+
1085
+ # Magick::Image::View::Pixels
1086
+ # Defines channel attribute getters/setters
1087
+ class Pixels < Array
1088
+ include Observable
1089
+
1090
+ # Define a getter and a setter for each channel.
1091
+ [:red, :green, :blue, :opacity].each do |c|
1092
+ module_eval <<-END_EVAL
1093
+ def #{c}
1094
+ return collect { |p| p.#{c} }
1617
1095
  end
1618
- if length > 0
1619
- @scene = length - 1 # last image in array
1096
+ def #{c}=(v)
1097
+ each { |p| p.#{c} = v }
1098
+ changed
1099
+ notify_observers(self)
1100
+ nil
1620
1101
  end
1621
- self
1622
- end
1623
-
1624
- def insert(index, *args)
1625
- args.each {|image| is_an_image image}
1626
- current = get_current
1627
- @images.insert(index, *args)
1628
- set_current current
1629
- self
1630
- end
1631
-
1632
- # Call inspect for all the images
1633
- def inspect
1634
- img = []
1635
- @images.each {|image| img << image.inspect }
1636
- img = '[' + img.join(",\n") + "]\nscene=#{@scene}"
1102
+ END_EVAL
1637
1103
  end
1638
-
1639
- # Set the number of iterations of an animated GIF
1640
- def iterations=(n)
1641
- n = Integer(n)
1642
- if n < 0 || n > 65535
1643
- Kernel.raise ArgumentError, 'iterations must be between 0 and 65535'
1644
- end
1645
- @images.each {|f| f.iterations=n}
1646
- self
1104
+ end # class Magick::Image::View::Pixels
1105
+
1106
+ # Magick::Image::View::Rows
1107
+ class Rows
1108
+ include Observable
1109
+
1110
+ def initialize(view, width, height, rows)
1111
+ @view = view
1112
+ @width = width
1113
+ @height = height
1114
+ @rows = rows
1647
1115
  end
1648
-
1649
- def last(*args)
1650
- if args.length == 0
1651
- a = @images.last
1116
+
1117
+ def [](*args)
1118
+ cols(args)
1119
+
1120
+ # Both View::Pixels and Magick::Pixel implement Observable
1121
+ if @unique
1122
+ pixels = @view[@rows[0]*@width + @cols[0]]
1123
+ pixels.add_observer(self)
1652
1124
  else
1653
- a = @images.last(*args)
1654
- ilist = self.class.new
1655
- a.each {|img| ilist << img}
1656
- @scene = a.length - 1
1657
- a = ilist
1125
+ pixels = View::Pixels.new
1126
+ each do |x|
1127
+ p = @view[x]
1128
+ p.add_observer(self)
1129
+ pixels << p
1130
+ end
1658
1131
  end
1659
- a
1660
- end
1661
-
1662
- # Custom marshal/unmarshal for Ruby 1.8.
1663
- def marshal_dump
1664
- ary = [@scene]
1665
- @images.each {|i| ary << Marshal.dump(i)}
1666
- ary
1132
+ pixels
1667
1133
  end
1668
-
1669
- def marshal_load(ary)
1670
- @scene = ary.shift
1671
- @images = []
1672
- ary.each {|a| @images << Marshal.load(a)}
1673
- end
1674
-
1675
- # The ImageList class supports the Magick::Image class methods by simply sending
1676
- # the method to the current image. If the method isn't explicitly supported,
1677
- # send it to the current image in the array. If there are no images, send
1678
- # it up the line. Catch a NameError and emit a useful message.
1679
- def method_missing(methID, *args, &block)
1680
- if @scene
1681
- @images[@scene].send(methID, *args, &block)
1682
- else
1683
- super
1134
+
1135
+ def []=(*args)
1136
+ rv = args.delete_at(-1) # get rvalue
1137
+ unless rv.is_a?(Pixel) # must be a Pixel or a color name
1138
+ begin
1139
+ rv = Pixel.from_color(rv)
1140
+ rescue TypeError
1141
+ Kernel.raise TypeError, "cannot convert #{rv.class} into Pixel"
1142
+ end
1684
1143
  end
1685
- rescue NoMethodError
1686
- Kernel.raise NoMethodError, "undefined method `#{methID.id2name}' for #{self.class}"
1687
- rescue Exception
1688
- $ERROR_POSITION.delete_if { |s| /:in `send'$/.match(s) || /:in `method_missing'$/.match(s) }
1689
- Kernel.raise
1690
- end
1691
-
1692
- # Create a new image and add it to the end
1693
- def new_image(cols, rows, *fill, &info_blk)
1694
- self << Magick::Image.new(cols, rows, *fill, &info_blk)
1144
+ cols(args)
1145
+ each { |x| @view[x] = rv.dup }
1146
+ changed
1147
+ notify_observers(self)
1148
+ nil
1695
1149
  end
1696
-
1697
- def partition(&block)
1698
- a = @images.partition(&block)
1699
- t = self.class.new
1700
- a[0].each { |img| t << img}
1701
- t.set_current nil
1702
- f = self.class.new
1703
- a[1].each { |img| f << img}
1704
- f.set_current nil
1705
- [t, f]
1150
+
1151
+ # A pixel has been modified. Tell the view.
1152
+ def update(pixel)
1153
+ changed
1154
+ notify_observers(self)
1155
+ pixel.delete_observer(self) # Don't need to hear again.
1156
+ nil
1706
1157
  end
1707
-
1708
- # Ping files and concatenate the new images
1709
- def ping(*files, &block)
1710
- if (files.length == 0)
1711
- Kernel.raise ArgumentError, 'no files given'
1712
- end
1713
- files.each do |f|
1714
- Magick::Image.ping(f, &block).each { |n| @images << n }
1158
+
1159
+ private
1160
+
1161
+ def cols(*args)
1162
+ @cols = args[0] # remove the outermost array
1163
+ @unique = false
1164
+
1165
+ # Convert @rows to an Enumerable object
1166
+ case @rows.length
1167
+ when 0 # Create a Range for all the rows
1168
+ @rows = Range.new(0, @height, true)
1169
+ when 1 # Range, Array, or a single integer
1170
+ # if the single element is already an Enumerable
1171
+ # object, get it.
1172
+ if @rows.first.respond_to? :each
1173
+ @rows = @rows.first
1174
+ else
1175
+ @rows = Integer(@rows.first)
1176
+ if @rows < 0
1177
+ @rows += @height
1178
+ end
1179
+ if @rows < 0 || @rows > @height-1
1180
+ Kernel.raise IndexError, "index [#{@rows}] out of range"
1181
+ end
1182
+ # Convert back to an array
1183
+ @rows = Array.new(1, @rows)
1184
+ @unique = true
1715
1185
  end
1716
- @scene = length - 1
1717
- self
1718
- end
1719
-
1720
- def pop
1721
- current = get_current
1722
- a = @images.pop # can return nil
1723
- set_current current
1724
- a
1725
- end
1726
-
1727
- def push(*objs)
1728
- objs.each do |image|
1729
- is_an_image image
1730
- @images << image
1731
- end
1732
- @scene = length - 1
1733
- self
1734
- end
1735
-
1736
- # Read files and concatenate the new images
1737
- def read(*files, &block)
1738
- if (files.length == 0)
1739
- Kernel.raise ArgumentError, 'no files given'
1740
- end
1741
- files.each do |f|
1742
- Magick::Image.read(f, &block).each { |n| @images << n }
1186
+ when 2
1187
+ # A pair of integers representing the starting column and the number of columns
1188
+ start = Integer(@rows[0])
1189
+ length = Integer(@rows[1])
1190
+
1191
+ # Negative start -> start from last row
1192
+ if start < 0
1193
+ start += @height
1743
1194
  end
1744
- @scene = length - 1
1745
- self
1746
- end
1747
-
1748
- # override Enumerable's reject
1749
- def reject(&block)
1750
- current = get_current
1751
- ilist = self.class.new
1752
- a = @images.reject(&block)
1753
- a.each {|image| ilist << image}
1754
- ilist.set_current current
1755
- ilist
1756
- end
1757
-
1758
- def reject!(&block)
1759
- current = get_current
1760
- a = @images.reject!(&block)
1761
- @images = a unless a.nil?
1762
- set_current current
1763
- a.nil? ? nil : self
1764
- end
1765
-
1766
- def replace(other)
1767
- is_an_image_array other
1768
- current = get_current
1769
- @images.clear
1770
- other.each {|image| @images << image}
1771
- @scene = length == 0 ? nil : 0
1772
- set_current current
1773
- self
1774
- end
1775
-
1776
- # Ensure respond_to? answers correctly when we are delegating to Image
1777
- alias_method :__respond_to__?, :respond_to?
1778
- def respond_to?(methID, priv=false)
1779
- return true if __respond_to__?(methID, priv)
1780
- if @scene
1781
- @images[@scene].respond_to?(methID, priv)
1195
+
1196
+ if start > @height || start < 0 || length < 0
1197
+ Kernel.raise IndexError, "index [#{@rows.first}] out of range"
1782
1198
  else
1783
- super
1199
+ if start + length > @height
1200
+ length = @height - length
1201
+ length = [length, 0].max
1202
+ end
1784
1203
  end
1785
- end
1786
-
1787
- def reverse
1788
- current = get_current
1789
- a = self.class.new
1790
- @images.reverse_each {|image| a << image}
1791
- a.set_current current
1792
- a
1793
- end
1794
-
1795
- def reverse!
1796
- current = get_current
1797
- @images.reverse!
1798
- set_current current
1799
- self
1800
- end
1801
-
1802
- def reverse_each
1803
- @images.reverse_each {|image| yield(image)}
1804
- self
1805
- end
1806
-
1807
- def shift
1808
- current = get_current
1809
- a = @images.shift
1810
- set_current current
1811
- a
1812
- end
1813
-
1814
- def slice(*args)
1815
- current = get_current
1816
- slice = @images.slice(*args)
1817
- if slice
1818
- ilist = self.class.new
1819
- if slice.respond_to?(:each)
1820
- slice.each {|image| ilist << image}
1821
- else
1822
- ilist << slice
1823
- end
1204
+ # Create a Range for the specified set of rows
1205
+ @rows = Range.new(start, start+length, true)
1206
+ end
1207
+
1208
+ case @cols.length
1209
+ when 0 # all rows
1210
+ @cols = Range.new(0, @width, true) # convert to range
1211
+ @unique = false
1212
+ when 1 # Range, Array, or a single integer
1213
+ # if the single element is already an Enumerable
1214
+ # object, get it.
1215
+ if @cols.first.respond_to? :each
1216
+ @cols = @cols.first
1217
+ @unique = false
1824
1218
  else
1825
- ilist = nil
1219
+ @cols = Integer(@cols.first)
1220
+ if @cols < 0
1221
+ @cols += @width
1222
+ end
1223
+ if @cols < 0 || @cols > @width-1
1224
+ Kernel.raise IndexError, "index [#{@cols}] out of range"
1225
+ end
1226
+ # Convert back to array
1227
+ @cols = Array.new(1, @cols)
1228
+ @unique &&= true
1826
1229
  end
1827
- ilist
1828
- end
1829
-
1830
- def slice!(*args)
1831
- current = get_current
1832
- a = @images.slice!(*args)
1833
- set_current current
1834
- a
1835
- end
1836
-
1837
- def ticks_per_second=(t)
1838
- if Integer(t) < 0
1839
- Kernel.raise ArgumentError, 'ticks_per_second must be greater than or equal to 0'
1230
+ when 2
1231
+ # A pair of integers representing the starting column and the number of columns
1232
+ start = Integer(@cols[0])
1233
+ length = Integer(@cols[1])
1234
+
1235
+ # Negative start -> start from last row
1236
+ if start < 0
1237
+ start += @width
1238
+ end
1239
+
1240
+ if start > @width || start < 0 || length < 0
1241
+ ; #nop
1242
+ else
1243
+ if start + length > @width
1244
+ length = @width - length
1245
+ length = [length, 0].max
1246
+ end
1247
+ end
1248
+ # Create a Range for the specified set of columns
1249
+ @cols = Range.new(start, start+length, true)
1250
+ @unique = false
1840
1251
  end
1841
- @images.each { |f| f.ticks_per_second = Integer(t) }
1842
- end
1843
-
1844
- def to_a
1845
- a = []
1846
- @images.each {|image| a << image}
1847
- a
1848
- end
1849
-
1850
- def uniq
1851
- current = get_current
1852
- a = self.class.new
1853
- @images.uniq.each {|image| a << image}
1854
- a.set_current current
1855
- a
1856
1252
  end
1857
-
1858
- def uniq!(*args)
1859
- current = get_current
1860
- a = @images.uniq!
1861
- set_current current
1862
- a.nil? ? nil : self
1863
- end
1864
-
1865
- # @scene -> new object
1866
- def unshift(obj)
1867
- is_an_image obj
1868
- @images.unshift(obj)
1869
- @scene = 0
1870
- self
1871
- end
1872
-
1873
- def values_at(*args)
1874
- a = @images.values_at(*args)
1875
- a = self.class.new
1876
- @images.values_at(*args).each {|image| a << image}
1877
- a.scene = a.length - 1
1878
- a
1253
+
1254
+ # iterator called from subscript methods
1255
+ def each
1256
+ maxrows = @height - 1
1257
+ maxcols = @width - 1
1258
+
1259
+ @rows.each do |j|
1260
+ if j > maxrows
1261
+ Kernel.raise IndexError, "index [#{j}] out of range"
1262
+ end
1263
+ @cols.each do |i|
1264
+ if i > maxcols
1265
+ Kernel.raise IndexError, "index [#{i}] out of range"
1266
+ end
1267
+ yield j*@width + i
1268
+ end
1269
+ end
1270
+ nil # useless return value
1879
1271
  end
1880
- alias_method :indexes, :values_at
1881
- alias_method :indices, :values_at
1882
- end # Magick::ImageList
1272
+ end # class Magick::Image::View::Rows
1273
+ end # class Magick::Image::View
1274
+ end # class Magick::Image
1883
1275
 
1884
- # Collects non-specific optional method arguments
1885
- class OptionalMethodArguments
1886
- def initialize(img)
1887
- @img = img
1276
+ class ImageList
1277
+ include Comparable
1278
+ include Enumerable
1279
+ attr_reader :scene
1280
+
1281
+ private
1282
+
1283
+ def get_current
1284
+ return @images[@scene].__id__ rescue nil
1285
+ end
1286
+
1287
+ protected
1288
+
1289
+ def is_an_image(obj)
1290
+ unless obj.is_a? Magick::Image
1291
+ Kernel.raise ArgumentError, "Magick::Image required (#{obj.class} given)"
1292
+ end
1293
+ true
1294
+ end
1295
+
1296
+ # Ensure array is always an array of Magick::Image objects
1297
+ def is_an_image_array(ary)
1298
+ unless ary.respond_to? :each
1299
+ Kernel.raise ArgumentError, "Magick::ImageList or array of Magick::Images required (#{ary.class} given)"
1888
1300
  end
1889
-
1890
- # miscellaneous options like -verbose
1891
- def method_missing(mth, val)
1892
- @img.define(mth.to_s.tr('_', '-'), val)
1301
+ ary.each { |obj| is_an_image obj }
1302
+ true
1303
+ end
1304
+
1305
+ # Find old current image, update scene number
1306
+ # current is the id of the old current image.
1307
+ def set_current(current)
1308
+ if length == 0
1309
+ self.scene = nil
1310
+ return
1311
+ # Don't bother looking for current image
1312
+ elsif scene.nil? || scene >= length
1313
+ self.scene = length - 1
1314
+ return
1315
+ elsif !current.nil?
1316
+ # Find last instance of "current" in the list.
1317
+ # If "current" isn't in the list, set current to last image.
1318
+ self.scene = length - 1
1319
+ each_with_index do |f,i|
1320
+ if f.__id__ == current
1321
+ self.scene = i
1322
+ end
1893
1323
  end
1894
-
1895
- # set(key, val) corresponds to -set option:key val
1896
- def define(key, val = nil)
1897
- @img.define(key, val)
1324
+ return
1325
+ end
1326
+ self.scene = length - 1
1327
+ end
1328
+
1329
+ public
1330
+
1331
+ # Allow scene to be set to nil
1332
+ def scene=(n)
1333
+ if n.nil?
1334
+ Kernel.raise IndexError, 'scene number out of bounds' unless @images.length == 0
1335
+ @scene = nil
1336
+ return @scene
1337
+ elsif @images.length == 0
1338
+ Kernel.raise IndexError, 'scene number out of bounds'
1339
+ end
1340
+
1341
+ n = Integer(n)
1342
+ if n < 0 || n > length - 1
1343
+ Kernel.raise IndexError, 'scene number out of bounds'
1344
+ end
1345
+ @scene = n
1346
+ @scene
1347
+ end
1348
+
1349
+ # All the binary operators work the same way.
1350
+ # 'other' should be either an ImageList or an Array
1351
+ %w{& + - |}.each do |op|
1352
+ module_eval <<-END_BINOPS
1353
+ def #{op}(other)
1354
+ ilist = self.class.new
1355
+ begin
1356
+ a = other #{op} @images
1357
+ rescue TypeError
1358
+ Kernel.raise ArgumentError, "Magick::ImageList expected, got " + other.class.to_s
1359
+ end
1360
+ current = get_current()
1361
+ a.each do |image|
1362
+ is_an_image image
1363
+ ilist << image
1364
+ end
1365
+ ilist.set_current current
1366
+ return ilist
1898
1367
  end
1899
-
1900
- # accepts Pixel object or color name
1901
- def highlight_color=(color)
1902
- color = @img.to_color(color) if color.respond_to?(:to_color)
1903
- @img.define('highlight-color', color)
1368
+ END_BINOPS
1369
+ end
1370
+
1371
+ def *(n)
1372
+ unless n.is_a? Integer
1373
+ Kernel.raise ArgumentError, "Integer required (#{n.class} given)"
1374
+ end
1375
+ current = get_current
1376
+ ilist = self.class.new
1377
+ (@images * n).each {|image| ilist << image}
1378
+ ilist.set_current current
1379
+ ilist
1380
+ end
1381
+
1382
+ def <<(obj)
1383
+ is_an_image obj
1384
+ @images << obj
1385
+ @scene = @images.length - 1
1386
+ self
1387
+ end
1388
+
1389
+ # Compare ImageLists
1390
+ # Compare each image in turn until the result of a comparison
1391
+ # is not 0. If all comparisons return 0, then
1392
+ # return if A.scene != B.scene
1393
+ # return A.length <=> B.length
1394
+ def <=>(other)
1395
+ unless other.is_a? self.class
1396
+ Kernel.raise TypeError, "#{self.class} required (#{other.class} given)"
1397
+ end
1398
+ size = [length, other.length].min
1399
+ size.times do |x|
1400
+ r = self[x] <=> other[x]
1401
+ return r unless r == 0
1402
+ end
1403
+ if @scene.nil? && other.scene.nil?
1404
+ return 0
1405
+ elsif @scene.nil? && !other.scene.nil?
1406
+ Kernel.raise TypeError, "cannot convert nil into #{other.scene.class}"
1407
+ elsif ! @scene.nil? && other.scene.nil?
1408
+ Kernel.raise TypeError, "cannot convert nil into #{scene.class}"
1409
+ end
1410
+ r = scene <=> other.scene
1411
+ return r unless r == 0
1412
+ length <=> other.length
1413
+ end
1414
+
1415
+ def [](*args)
1416
+ a = @images[*args]
1417
+ if a.respond_to?(:each)
1418
+ ilist = self.class.new
1419
+ a.each {|image| ilist << image}
1420
+ a = ilist
1421
+ end
1422
+ a
1423
+ end
1424
+
1425
+ def []=(*args)
1426
+ obj = @images.[]=(*args)
1427
+ if obj && obj.respond_to?(:each)
1428
+ is_an_image_array(obj)
1429
+ set_current obj.last.__id__
1430
+ elsif obj
1431
+ is_an_image(obj)
1432
+ set_current obj.__id__
1433
+ else
1434
+ set_current nil
1435
+ end
1436
+ obj
1437
+ end
1438
+
1439
+ [:at, :each, :each_index, :empty?, :fetch,
1440
+ :first, :hash, :include?, :index, :length, :rindex, :sort!].each do |mth|
1441
+ module_eval <<-END_SIMPLE_DELEGATES
1442
+ def #{mth}(*args, &block)
1443
+ @images.#{mth}(*args, &block)
1904
1444
  end
1905
-
1906
- # accepts Pixel object or color name
1907
- def lowlight_color=(color)
1908
- color = @img.to_color(color) if color.respond_to?(:to_color)
1909
- @img.define('lowlight-color', color)
1445
+ END_SIMPLE_DELEGATES
1446
+ end
1447
+ alias_method :size, :length
1448
+
1449
+ # Array#nitems is not available in 1.9
1450
+ if Array.instance_methods.include?('nitems')
1451
+ def nitems
1452
+ @images.nitems
1453
+ end
1454
+ end
1455
+
1456
+ def clear
1457
+ @scene = nil
1458
+ @images.clear
1459
+ end
1460
+
1461
+ def clone
1462
+ ditto = dup
1463
+ ditto.freeze if frozen?
1464
+ ditto
1465
+ end
1466
+
1467
+ # override Enumerable#collect
1468
+ def collect(&block)
1469
+ current = get_current
1470
+ a = @images.collect(&block)
1471
+ ilist = self.class.new
1472
+ a.each {|image| ilist << image}
1473
+ ilist.set_current current
1474
+ ilist
1475
+ end
1476
+
1477
+ def collect!(&block)
1478
+ @images.collect!(&block)
1479
+ is_an_image_array @images
1480
+ self
1481
+ end
1482
+
1483
+ # Make a deep copy
1484
+ def copy
1485
+ ditto = self.class.new
1486
+ @images.each { |f| ditto << f.copy }
1487
+ ditto.scene = @scene
1488
+ ditto.taint if tainted?
1489
+ ditto
1490
+ end
1491
+
1492
+ # Return the current image
1493
+ def cur_image
1494
+ unless @scene
1495
+ Kernel.raise IndexError, 'no images in this list'
1496
+ end
1497
+ @images[@scene]
1498
+ end
1499
+
1500
+ # ImageList#map took over the "map" name. Use alternatives.
1501
+ alias_method :__map__, :collect
1502
+ alias_method :map!, :collect!
1503
+ alias_method :__map__!, :collect!
1504
+
1505
+ # ImageMagic used affinity in 6.4.3, switch to remap in 6.4.4.
1506
+ alias_method :affinity, :remap
1507
+
1508
+ def compact
1509
+ current = get_current
1510
+ ilist = self.class.new
1511
+ a = @images.compact
1512
+ a.each {|image| ilist << image}
1513
+ ilist.set_current current
1514
+ ilist
1515
+ end
1516
+
1517
+ def compact!
1518
+ current = get_current
1519
+ a = @images.compact! # returns nil if no changes were made
1520
+ set_current current
1521
+ a.nil? ? nil : self
1522
+ end
1523
+
1524
+ def concat(other)
1525
+ is_an_image_array other
1526
+ other.each {|image| @images << image}
1527
+ @scene = length-1
1528
+ self
1529
+ end
1530
+
1531
+ # Set same delay for all images
1532
+ def delay=(d)
1533
+ if Integer(d) < 0
1534
+ fail ArgumentError, 'delay must be greater than or equal to 0'
1535
+ end
1536
+ @images.each { |f| f.delay = Integer(d) }
1537
+ end
1538
+
1539
+ def delete(obj, &block)
1540
+ is_an_image obj
1541
+ current = get_current
1542
+ a = @images.delete(obj, &block)
1543
+ set_current current
1544
+ a
1545
+ end
1546
+
1547
+ def delete_at(ndx)
1548
+ current = get_current
1549
+ a = @images.delete_at(ndx)
1550
+ set_current current
1551
+ a
1552
+ end
1553
+
1554
+ def delete_if(&block)
1555
+ current = get_current
1556
+ @images.delete_if(&block)
1557
+ set_current current
1558
+ self
1559
+ end
1560
+
1561
+ def dup
1562
+ ditto = self.class.new
1563
+ @images.each {|img| ditto << img}
1564
+ ditto.scene = @scene
1565
+ ditto.taint if tainted?
1566
+ ditto
1567
+ end
1568
+
1569
+ def eql?(other)
1570
+ is_an_image_array other
1571
+ eql = other.eql?(@images)
1572
+ begin # "other" is another ImageList
1573
+ eql &&= @scene == other.scene
1574
+ rescue NoMethodError
1575
+ # "other" is a plain Array
1576
+ end
1577
+ eql
1578
+ end
1579
+
1580
+ def fill(*args, &block)
1581
+ is_an_image args[0] unless block_given?
1582
+ current = get_current
1583
+ @images.fill(*args, &block)
1584
+ is_an_image_array self
1585
+ set_current current
1586
+ self
1587
+ end
1588
+
1589
+ # Override Enumerable's find_all
1590
+ def find_all(&block)
1591
+ current = get_current
1592
+ a = @images.find_all(&block)
1593
+ ilist = self.class.new
1594
+ a.each {|image| ilist << image}
1595
+ ilist.set_current current
1596
+ ilist
1597
+ end
1598
+ alias_method :select, :find_all
1599
+
1600
+ def from_blob(*blobs, &block)
1601
+ if (blobs.length == 0)
1602
+ Kernel.raise ArgumentError, 'no blobs given'
1603
+ end
1604
+ blobs.each do |b|
1605
+ Magick::Image.from_blob(b, &block).each { |n| @images << n }
1606
+ end
1607
+ @scene = length - 1
1608
+ self
1609
+ end
1610
+
1611
+ # Initialize new instances
1612
+ def initialize(*filenames, &block)
1613
+ @images = []
1614
+ @scene = nil
1615
+ filenames.each do |f|
1616
+ Magick::Image.read(f, &block).each { |n| @images << n }
1617
+ end
1618
+ if length > 0
1619
+ @scene = length - 1 # last image in array
1620
+ end
1621
+ self
1622
+ end
1623
+
1624
+ def insert(index, *args)
1625
+ args.each {|image| is_an_image image}
1626
+ current = get_current
1627
+ @images.insert(index, *args)
1628
+ set_current current
1629
+ self
1630
+ end
1631
+
1632
+ # Call inspect for all the images
1633
+ def inspect
1634
+ img = []
1635
+ @images.each {|image| img << image.inspect }
1636
+ img = '[' + img.join(",\n") + "]\nscene=#{@scene}"
1637
+ end
1638
+
1639
+ # Set the number of iterations of an animated GIF
1640
+ def iterations=(n)
1641
+ n = Integer(n)
1642
+ if n < 0 || n > 65535
1643
+ Kernel.raise ArgumentError, 'iterations must be between 0 and 65535'
1644
+ end
1645
+ @images.each {|f| f.iterations=n}
1646
+ self
1647
+ end
1648
+
1649
+ def last(*args)
1650
+ if args.length == 0
1651
+ a = @images.last
1652
+ else
1653
+ a = @images.last(*args)
1654
+ ilist = self.class.new
1655
+ a.each {|img| ilist << img}
1656
+ @scene = a.length - 1
1657
+ a = ilist
1658
+ end
1659
+ a
1660
+ end
1661
+
1662
+ # Custom marshal/unmarshal for Ruby 1.8.
1663
+ def marshal_dump
1664
+ ary = [@scene]
1665
+ @images.each {|i| ary << Marshal.dump(i)}
1666
+ ary
1667
+ end
1668
+
1669
+ def marshal_load(ary)
1670
+ @scene = ary.shift
1671
+ @images = []
1672
+ ary.each {|a| @images << Marshal.load(a)}
1673
+ end
1674
+
1675
+ # The ImageList class supports the Magick::Image class methods by simply sending
1676
+ # the method to the current image. If the method isn't explicitly supported,
1677
+ # send it to the current image in the array. If there are no images, send
1678
+ # it up the line. Catch a NameError and emit a useful message.
1679
+ def method_missing(methID, *args, &block)
1680
+ if @scene
1681
+ @images[@scene].send(methID, *args, &block)
1682
+ else
1683
+ super
1684
+ end
1685
+ rescue NoMethodError
1686
+ Kernel.raise NoMethodError, "undefined method `#{methID.id2name}' for #{self.class}"
1687
+ rescue Exception
1688
+ $ERROR_POSITION.delete_if { |s| /:in `send'$/.match(s) || /:in `method_missing'$/.match(s) }
1689
+ Kernel.raise
1690
+ end
1691
+
1692
+ # Create a new image and add it to the end
1693
+ def new_image(cols, rows, *fill, &info_blk)
1694
+ self << Magick::Image.new(cols, rows, *fill, &info_blk)
1695
+ end
1696
+
1697
+ def partition(&block)
1698
+ a = @images.partition(&block)
1699
+ t = self.class.new
1700
+ a[0].each { |img| t << img}
1701
+ t.set_current nil
1702
+ f = self.class.new
1703
+ a[1].each { |img| f << img}
1704
+ f.set_current nil
1705
+ [t, f]
1706
+ end
1707
+
1708
+ # Ping files and concatenate the new images
1709
+ def ping(*files, &block)
1710
+ if (files.length == 0)
1711
+ Kernel.raise ArgumentError, 'no files given'
1712
+ end
1713
+ files.each do |f|
1714
+ Magick::Image.ping(f, &block).each { |n| @images << n }
1715
+ end
1716
+ @scene = length - 1
1717
+ self
1718
+ end
1719
+
1720
+ def pop
1721
+ current = get_current
1722
+ a = @images.pop # can return nil
1723
+ set_current current
1724
+ a
1725
+ end
1726
+
1727
+ def push(*objs)
1728
+ objs.each do |image|
1729
+ is_an_image image
1730
+ @images << image
1731
+ end
1732
+ @scene = length - 1
1733
+ self
1734
+ end
1735
+
1736
+ # Read files and concatenate the new images
1737
+ def read(*files, &block)
1738
+ if (files.length == 0)
1739
+ Kernel.raise ArgumentError, 'no files given'
1740
+ end
1741
+ files.each do |f|
1742
+ Magick::Image.read(f, &block).each { |n| @images << n }
1743
+ end
1744
+ @scene = length - 1
1745
+ self
1746
+ end
1747
+
1748
+ # override Enumerable's reject
1749
+ def reject(&block)
1750
+ current = get_current
1751
+ ilist = self.class.new
1752
+ a = @images.reject(&block)
1753
+ a.each {|image| ilist << image}
1754
+ ilist.set_current current
1755
+ ilist
1756
+ end
1757
+
1758
+ def reject!(&block)
1759
+ current = get_current
1760
+ a = @images.reject!(&block)
1761
+ @images = a unless a.nil?
1762
+ set_current current
1763
+ a.nil? ? nil : self
1764
+ end
1765
+
1766
+ def replace(other)
1767
+ is_an_image_array other
1768
+ current = get_current
1769
+ @images.clear
1770
+ other.each {|image| @images << image}
1771
+ @scene = length == 0 ? nil : 0
1772
+ set_current current
1773
+ self
1774
+ end
1775
+
1776
+ # Ensure respond_to? answers correctly when we are delegating to Image
1777
+ alias_method :__respond_to__?, :respond_to?
1778
+ def respond_to?(methID, priv=false)
1779
+ return true if __respond_to__?(methID, priv)
1780
+ if @scene
1781
+ @images[@scene].respond_to?(methID, priv)
1782
+ else
1783
+ super
1784
+ end
1785
+ end
1786
+
1787
+ def reverse
1788
+ current = get_current
1789
+ a = self.class.new
1790
+ @images.reverse_each {|image| a << image}
1791
+ a.set_current current
1792
+ a
1793
+ end
1794
+
1795
+ def reverse!
1796
+ current = get_current
1797
+ @images.reverse!
1798
+ set_current current
1799
+ self
1800
+ end
1801
+
1802
+ def reverse_each
1803
+ @images.reverse_each {|image| yield(image)}
1804
+ self
1805
+ end
1806
+
1807
+ def shift
1808
+ current = get_current
1809
+ a = @images.shift
1810
+ set_current current
1811
+ a
1812
+ end
1813
+
1814
+ def slice(*args)
1815
+ current = get_current
1816
+ slice = @images.slice(*args)
1817
+ if slice
1818
+ ilist = self.class.new
1819
+ if slice.respond_to?(:each)
1820
+ slice.each {|image| ilist << image}
1821
+ else
1822
+ ilist << slice
1910
1823
  end
1824
+ else
1825
+ ilist = nil
1826
+ end
1827
+ ilist
1911
1828
  end
1912
1829
 
1913
- # Example fill class. Fills the image with the specified background
1914
- # color, then crosshatches with the specified crosshatch color.
1915
- # @dist is the number of pixels between hatch lines.
1916
- # See Magick::Draw examples.
1917
- class HatchFill
1918
- def initialize(bgcolor, hatchcolor='white', dist=10)
1919
- @bgcolor = bgcolor
1920
- @hatchpixel = Pixel.from_color(hatchcolor)
1921
- @dist = dist
1922
- end
1923
-
1924
- def fill(img) # required
1925
- img.background_color = @bgcolor
1926
- img.erase! # sets image to background color
1927
- pixels = Array.new([img.rows, img.columns].max, @hatchpixel)
1928
- @dist.step((img.columns-1)/@dist*@dist, @dist) do |x|
1929
- img.store_pixels(x,0,1,img.rows,pixels)
1930
- end
1931
- @dist.step((img.rows-1)/@dist*@dist, @dist) do |y|
1932
- img.store_pixels(0,y,img.columns,1,pixels)
1933
- end
1934
- end
1830
+ def slice!(*args)
1831
+ current = get_current
1832
+ a = @images.slice!(*args)
1833
+ set_current current
1834
+ a
1835
+ end
1836
+
1837
+ def ticks_per_second=(t)
1838
+ if Integer(t) < 0
1839
+ Kernel.raise ArgumentError, 'ticks_per_second must be greater than or equal to 0'
1840
+ end
1841
+ @images.each { |f| f.ticks_per_second = Integer(t) }
1842
+ end
1843
+
1844
+ def to_a
1845
+ a = []
1846
+ @images.each {|image| a << image}
1847
+ a
1848
+ end
1849
+
1850
+ def uniq
1851
+ current = get_current
1852
+ a = self.class.new
1853
+ @images.uniq.each {|image| a << image}
1854
+ a.set_current current
1855
+ a
1856
+ end
1857
+
1858
+ def uniq!(*args)
1859
+ current = get_current
1860
+ a = @images.uniq!
1861
+ set_current current
1862
+ a.nil? ? nil : self
1863
+ end
1864
+
1865
+ # @scene -> new object
1866
+ def unshift(obj)
1867
+ is_an_image obj
1868
+ @images.unshift(obj)
1869
+ @scene = 0
1870
+ self
1871
+ end
1872
+
1873
+ def values_at(*args)
1874
+ a = @images.values_at(*args)
1875
+ a = self.class.new
1876
+ @images.values_at(*args).each {|image| a << image}
1877
+ a.scene = a.length - 1
1878
+ a
1879
+ end
1880
+ alias_method :indexes, :values_at
1881
+ alias_method :indices, :values_at
1882
+ end # Magick::ImageList
1883
+
1884
+ # Collects non-specific optional method arguments
1885
+ class OptionalMethodArguments
1886
+ def initialize(img)
1887
+ @img = img
1888
+ end
1889
+
1890
+ # miscellaneous options like -verbose
1891
+ def method_missing(mth, val)
1892
+ @img.define(mth.to_s.tr('_', '-'), val)
1935
1893
  end
1936
1894
 
1937
- # Fill class with solid monochromatic color
1938
- class SolidFill
1939
- def initialize(bgcolor)
1940
- @bgcolor = bgcolor
1895
+ # set(key, val) corresponds to -set option:key val
1896
+ def define(key, val = nil)
1897
+ @img.define(key, val)
1898
+ end
1899
+
1900
+ # accepts Pixel object or color name
1901
+ def highlight_color=(color)
1902
+ color = @img.to_color(color) if color.respond_to?(:to_color)
1903
+ @img.define('highlight-color', color)
1904
+ end
1905
+
1906
+ # accepts Pixel object or color name
1907
+ def lowlight_color=(color)
1908
+ color = @img.to_color(color) if color.respond_to?(:to_color)
1909
+ @img.define('lowlight-color', color)
1910
+ end
1911
+ end
1912
+
1913
+ # Example fill class. Fills the image with the specified background
1914
+ # color, then crosshatches with the specified crosshatch color.
1915
+ # @dist is the number of pixels between hatch lines.
1916
+ # See Magick::Draw examples.
1917
+ class HatchFill
1918
+ def initialize(bgcolor, hatchcolor='white', dist=10)
1919
+ @bgcolor = bgcolor
1920
+ @hatchpixel = Pixel.from_color(hatchcolor)
1921
+ @dist = dist
1922
+ end
1923
+
1924
+ def fill(img) # required
1925
+ img.background_color = @bgcolor
1926
+ img.erase! # sets image to background color
1927
+ pixels = Array.new([img.rows, img.columns].max, @hatchpixel)
1928
+ @dist.step((img.columns-1)/@dist*@dist, @dist) do |x|
1929
+ img.store_pixels(x,0,1,img.rows,pixels)
1930
+ end
1931
+ @dist.step((img.rows-1)/@dist*@dist, @dist) do |y|
1932
+ img.store_pixels(0,y,img.columns,1,pixels)
1941
1933
  end
1942
-
1943
- def fill(img)
1944
- img.background_color = @bgcolor
1945
- img.erase!
1946
- end
1947
1934
  end
1935
+ end
1936
+
1937
+ # Fill class with solid monochromatic color
1938
+ class SolidFill
1939
+ def initialize(bgcolor)
1940
+ @bgcolor = bgcolor
1941
+ end
1942
+
1943
+ def fill(img)
1944
+ img.background_color = @bgcolor
1945
+ img.erase!
1946
+ end
1947
+ end
1948
1948
  end # Magick