rmagick 2.13.3 → 2.13.4

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.

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