rgd2-ffij 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. data/.gitignore +5 -0
  2. data/COPYING +340 -0
  3. data/COPYRIGHT +17 -0
  4. data/README +34 -0
  5. data/Rakefile +32 -0
  6. data/gd2-ffij.gemspec +96 -0
  7. data/lib/gd2-ffij.rb +215 -0
  8. data/lib/gd2/canvas.rb +422 -0
  9. data/lib/gd2/color.rb +240 -0
  10. data/lib/gd2/ffi_struct.rb +76 -0
  11. data/lib/gd2/font.rb +343 -0
  12. data/lib/gd2/image.rb +795 -0
  13. data/lib/gd2/palette.rb +253 -0
  14. data/test/canvas_test.rb +186 -0
  15. data/test/image_test.rb +149 -0
  16. data/test/images/test.bmp +0 -0
  17. data/test/images/test.gd +0 -0
  18. data/test/images/test.gd2 +0 -0
  19. data/test/images/test.gif +0 -0
  20. data/test/images/test.jpg +0 -0
  21. data/test/images/test.png +0 -0
  22. data/test/images/test.wbmp +0 -0
  23. data/test/images/test.xbm +686 -0
  24. data/test/images/test.xcf +0 -0
  25. data/test/images/test.xpm +261 -0
  26. data/test/images/test_arc.gd2 +0 -0
  27. data/test/images/test_canvas_filled_polygon.gd2 +0 -0
  28. data/test/images/test_canvas_filled_rectangle.gd2 +0 -0
  29. data/test/images/test_canvas_line.gd2 +0 -0
  30. data/test/images/test_canvas_move_to_and_line_to.gd2 +0 -0
  31. data/test/images/test_canvas_polygon.gd2 +0 -0
  32. data/test/images/test_canvas_rectangle.gd2 +0 -0
  33. data/test/images/test_circle.gd2 +0 -0
  34. data/test/images/test_color.gd2 +0 -0
  35. data/test/images/test_color.png +0 -0
  36. data/test/images/test_color.xcf +0 -0
  37. data/test/images/test_color_indexed.gd2 +0 -0
  38. data/test/images/test_color_sharpened.gd2 +0 -0
  39. data/test/images/test_cropped.gd2 +0 -0
  40. data/test/images/test_ellipse.gd2 +0 -0
  41. data/test/images/test_fill.gd2 +0 -0
  42. data/test/images/test_fill_to.gd2 +0 -0
  43. data/test/images/test_filled_circle.gd2 +0 -0
  44. data/test/images/test_filled_ellipse.gd2 +0 -0
  45. data/test/images/test_filled_wedge.gd2 +0 -0
  46. data/test/images/test_polar_transform.gd2 +0 -0
  47. data/test/images/test_resampled.gd2 +0 -0
  48. data/test/images/test_resized.gd2 +0 -0
  49. data/test/images/test_rotated_180.gd2 +0 -0
  50. data/test/images/test_text.gd2 +0 -0
  51. data/test/images/test_text_circle.gd2 +0 -0
  52. data/test/images/test_wedge.gd2 +0 -0
  53. data/test/test_helper.rb +13 -0
  54. data/vendor/fonts/ttf/DejaVuSans.ttf +0 -0
  55. data/vendor/fonts/ttf/LICENSE +99 -0
  56. metadata +123 -0
@@ -0,0 +1,795 @@
1
+ #
2
+ # Ruby/GD2 -- Ruby binding for gd 2 graphics library
3
+ #
4
+ # Copyright © 2005-2006 Robert Leslie, 2010 J Smith
5
+ #
6
+ # This file is part of Ruby/GD2.
7
+ #
8
+ # Ruby/GD2 is free software; you can redistribute it and/or modify it under
9
+ # the terms of the GNU General Public License as published by the Free
10
+ # Software Foundation; either version 2 of the License, or (at your option)
11
+ # any later version.
12
+ #
13
+ # This program is distributed in the hope that it will be useful, but
14
+ # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
15
+ # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
16
+ # for more details.
17
+ #
18
+ # You should have received a copy of the GNU General Public License along
19
+ # with this program; if not, write to the Free Software Foundation, Inc.,
20
+ # 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
21
+ #
22
+
23
+ module GD2
24
+ #
25
+ # = Introduction
26
+ #
27
+ # Image is the abstract base class for Image::IndexedColor and
28
+ # Image::TrueColor.
29
+ #
30
+ # == Creating and Importing
31
+ #
32
+ # Image objects are created either as a blank array of pixels:
33
+ #
34
+ # image = Image::IndexedColor.new(width, height)
35
+ # image = Image::TrueColor.new(width, height)
36
+ #
37
+ # or by loading image data from a file or a string containing one of the
38
+ # supported image formats:
39
+ #
40
+ # image = Image.load(file)
41
+ # image = Image.load(string)
42
+ #
43
+ # or by importing image data from a file given by its pathname:
44
+ #
45
+ # image = Image.import(filename)
46
+ #
47
+ # == Exporting
48
+ #
49
+ # After manipulating an image, it can be exported to a string in one of the
50
+ # supported image formats:
51
+ #
52
+ # image.jpeg(quality = nil)
53
+ # image.png(level = nil)
54
+ # image.gif
55
+ # image.wbmp(fgcolor)
56
+ # image.gd
57
+ # image.gd2(fmt = FMT_COMPRESSED)
58
+ #
59
+ # or to a file in a format determined by the filename extension:
60
+ #
61
+ # image.export(filename, options = {})
62
+ #
63
+ class Image
64
+ class UnrecognizedImageTypeError < StandardError; end
65
+
66
+ attr_reader :image_ptr #:nodoc:
67
+
68
+ # The Palette object for this image
69
+ attr_reader :palette
70
+
71
+ include Enumerable
72
+
73
+ # Create a new image of the specified dimensions. The default image class
74
+ # is Image::TrueColor; call this method on Image::IndexedColor instead if
75
+ # a palette image is desired.
76
+ def self.new(w, h)
77
+ image = (self == Image) ?
78
+ TrueColor.new(w, h) : allocate.init_with_size(w, h)
79
+
80
+ block_given? ? yield(image) : image
81
+ end
82
+
83
+ class << self
84
+ alias [] new
85
+ end
86
+
87
+ # Load an image from a file or a string. The image type is detected
88
+ # automatically (JPEG, PNG, GIF, WBMP, or GD2). The resulting image will be
89
+ # either of class Image::TrueColor or Image::IndexedColor.
90
+ def self.load(src)
91
+ src = src.force_encoding("BINARY") if src.respond_to? :force_encoding
92
+ case src
93
+ when File
94
+ pos = src.pos
95
+ magic = src.read(4)
96
+ src.pos = pos
97
+ data = src.read
98
+ data = data.force_encoding("BINARY") if data.respond_to? :force_encoding
99
+ args = [ data.length, data ]
100
+ when String
101
+ magic = src
102
+ args = [ src.length, src ]
103
+ else
104
+ raise TypeError, 'Unexpected argument type'
105
+ end
106
+
107
+ create = {
108
+ :jpeg => :gdImageCreateFromJpegPtr,
109
+ :png => :gdImageCreateFromPngPtr,
110
+ :gif => :gdImageCreateFromGifPtr,
111
+ :wbmp => :gdImageCreateFromWBMPPtr,
112
+ :gd2 => :gdImageCreateFromGd2Ptr
113
+ }
114
+
115
+ type = data_type(magic) or
116
+ raise UnrecognizedImageTypeError, 'Image data format is not recognized'
117
+ ptr = GD2FFI.send(create[type], *args)
118
+ raise LibraryError unless ptr
119
+
120
+ ptr = FFIStruct::ImagePtr.new(ptr)
121
+
122
+ image = (image_true_color?(ptr) ?
123
+ TrueColor : IndexedColor).allocate.init_with_image(ptr)
124
+
125
+ block_given? ? yield(image) : image
126
+ end
127
+
128
+ def self.data_type(str)
129
+ ct = 0
130
+ mgc = ""
131
+ str.each_byte do |byte|
132
+ break if ct == 4
133
+ mgc << byte.to_s
134
+ ct += 1
135
+ end
136
+ case mgc
137
+ when "255216255224"
138
+ :jpeg
139
+ when "137807871"
140
+ :png
141
+ when "71737056"
142
+ :gif
143
+ when "001300"
144
+ :wbmp
145
+ when "103100500"
146
+ :gd2
147
+ end
148
+ end
149
+ private_class_method :data_type
150
+
151
+ # Import an image from a file with the given +filename+. The :format option
152
+ # or the file extension is used to determine the image type (jpeg, png,
153
+ # gif, wbmp, gd, gd2, xbm, or xpm). The resulting image will be either of
154
+ # class Image::TrueColor or Image::IndexedColor.
155
+ #
156
+ # If the file format is gd2, it is optionally possible to extract only a
157
+ # part of the image. Use options :x, :y, :width, and :height to specify the
158
+ # part of the image to import.
159
+ def self.import(filename, options = {})
160
+ raise Errno::ENOENT.new(filename) unless File.exists?(filename)
161
+
162
+ unless format = options.delete(:format)
163
+ md = filename.match(/\.([^.]+)\z/)
164
+ format = md ? md[1].downcase : nil
165
+ end
166
+ format = format.to_sym if format
167
+
168
+ ptr = # TODO: implement xpm and xbm imports
169
+ #if format == :xpm
170
+ #raise ArgumentError, "Unexpected options #{options.inspect}" unless options.empty?
171
+ #GD2FFI.send(:gdImageCreateFromXpm, filename)
172
+ #elsif format == :xbm
173
+ #GD2FFI.send(:gdImageCreateFromXbm, filename)
174
+ if format == :gd2 && !options.empty?
175
+ x, y, width, height =
176
+ options.delete(:x) || 0, options.delete(:y) || 0,
177
+ options.delete(:width) || options.delete(:w),
178
+ options.delete(:height) || options.delete(:h)
179
+ raise ArgumentError, "Unexpected options #{options.inspect}" unless
180
+ options.empty?
181
+ raise ArgumentError, 'Missing required option :width' if width.nil?
182
+ raise ArgumentError, 'Missing required option :height' if height.nil?
183
+ # TODO:
184
+ ptr = File.open(filename, 'rb') do |file|
185
+ GD2FFI.send(:gdImageCreateFromGd2Part, file, x.to_i, y.to_i, width.to_i, height.to_i)
186
+ end
187
+ else
188
+ raise ArgumentError, "Unexpected options #{options.inspect}" unless
189
+ options.empty?
190
+ create_sym = {
191
+ :jpeg => :gdImageCreateFromJpegPtr,
192
+ :jpg => :gdImageCreateFromJpegPtr,
193
+ :png => :gdImageCreateFromPngPtr,
194
+ :gif => :gdImageCreateFromGifPtr,
195
+ :wbmp => :gdImageCreateFromWBMPPtr,
196
+ :gd => :gdImageCreateFromGdPtr,
197
+ :gd2 => :gdImageCreateFromGd2Ptr
198
+ }[format]
199
+ raise UnrecognizedImageTypeError,
200
+ 'Format (or file extension) is not recognized' unless create_sym
201
+
202
+ file = File.read(filename)
203
+ file = file.force_encoding("BINARY") if file.respond_to? :force_encoding
204
+ file_ptr = FFI::MemoryPointer.new(file.size, 1, false)
205
+ file_ptr.put_bytes(0, file)
206
+
207
+ GD2FFI.send(create_sym, file.size, file_ptr)
208
+ end
209
+ raise LibraryError unless ptr
210
+
211
+ ptr = FFIStruct::ImagePtr.new(ptr)
212
+
213
+ image = (image_true_color?(ptr) ?
214
+ TrueColor : IndexedColor).allocate.init_with_image(ptr)
215
+
216
+ block_given? ? yield(image) : image
217
+ end
218
+
219
+ def self.image_true_color?(ptr)
220
+ not ptr[:trueColor].zero?
221
+ end
222
+ private_class_method :image_true_color?
223
+
224
+ def self.create_image_ptr(sx, sy, alpha_blending = true) #:nodoc:
225
+ ptr = FFIStruct::ImagePtr.new(GD2FFI.send(create_image_sym, sx.to_i, sy.to_i))
226
+ GD2FFI.send(:gdImageAlphaBlending, ptr, alpha_blending ? 1 : 0)
227
+ ptr
228
+ end
229
+
230
+ def init_with_size(sx, sy) #:nodoc:
231
+ init_with_image self.class.create_image_ptr(sx, sy)
232
+ end
233
+
234
+ def init_with_image(ptr) #:nodoc:
235
+ @image_ptr = if ptr.is_a?(FFIStruct::ImagePtr)
236
+ ptr
237
+ else
238
+ FFIStruct::ImagePtr.new(ptr)
239
+ end
240
+
241
+ @palette = self.class.palette_class.new(self) unless
242
+ @palette && @palette.image == self
243
+ self
244
+ end
245
+
246
+ def inspect #:nodoc:
247
+ "#<#{self.class} #{size.inspect}>"
248
+ end
249
+
250
+ # Duplicate this image, copying all pixels to a new image. Contrast with
251
+ # Image#clone which produces a shallow copy and shares internal pixel data.
252
+ def dup
253
+ self.class.superclass.load(gd2(FMT_RAW))
254
+ end
255
+
256
+ # Compare this image with another image. Returns 0 if the images are
257
+ # identical, otherwise a bit field indicating the differences. See the
258
+ # GD2::CMP_* constants for individual bit flags.
259
+ def compare(other)
260
+ GD2FFI.send(:gdImageCompare, image_ptr, other.image_ptr)
261
+ end
262
+
263
+ # Compare this image with another image. Returns *false* if the images are
264
+ # not identical.
265
+ def ==(other)
266
+ (compare(other) & CMP_IMAGE).zero?
267
+ end
268
+
269
+ # Return true if this image is a TrueColor image.
270
+ def true_color?
271
+ kind_of?(TrueColor)
272
+ # self.class.image_true_color?(image_ptr)
273
+ end
274
+
275
+ # Return the width of this image, in pixels.
276
+ def width
277
+ image_ptr[:sx]
278
+ end
279
+ alias w width
280
+
281
+ # Return the height of this image, in pixels.
282
+ def height
283
+ image_ptr[:sy]
284
+ end
285
+ alias h height
286
+
287
+ # Return the size of this image as an array [_width_, _height_], in pixels.
288
+ def size
289
+ [width, height]
290
+ end
291
+
292
+ # Return the aspect ratio of this image, as a floating point ratio of the
293
+ # width to the height.
294
+ def aspect
295
+ width.to_f / height
296
+ end
297
+
298
+ # Return the pixel value at image location (+x+, +y+).
299
+ def get_pixel(x, y)
300
+ GD2FFI.send(:gdImageGetPixel, @image_ptr, x.to_i, y.to_i)
301
+ end
302
+ alias pixel get_pixel
303
+
304
+ # Set the pixel value at image location (+x+, +y+).
305
+ def set_pixel(x, y, value)
306
+ GD2FFI.send(:gdImageSetPixel, @image_ptr, x.to_i, y.to_i, value.to_i)
307
+ nil
308
+ end
309
+
310
+ # Return the color of the pixel at image location (+x+, +y+).
311
+ def [](x, y)
312
+ pixel2color(get_pixel(x, y))
313
+ end
314
+
315
+ # Set the color of the pixel at image location (+x+, +y+).
316
+ def []=(x, y, color)
317
+ set_pixel(x, y, color2pixel(color))
318
+ end
319
+
320
+ # Iterate over each row of pixels in the image, returning an array of
321
+ # pixel values.
322
+ def each
323
+ ptr = image_ptr
324
+ (0...height).each do |y|
325
+ row = (0...width).inject(Array.new(width)) do |row, x|
326
+ row[x] = get_pixel(x, y)
327
+ row
328
+ end
329
+ yield row
330
+ end
331
+ end
332
+
333
+ # Return a Color object for the given +pixel+ value.
334
+ def pixel2color(pixel)
335
+ Color.new_from_rgba(pixel)
336
+ end
337
+
338
+ # Return a pixel value for the given +color+ object.
339
+ def color2pixel(color)
340
+ color.rgba
341
+ end
342
+
343
+ # Return *true* if this image will be stored in interlaced form when output
344
+ # as PNG or JPEG.
345
+ def interlaced?
346
+ not image_ptr[:interlace].zero?
347
+ end
348
+
349
+ # Set whether this image will be stored in interlaced form when output as
350
+ # PNG or JPEG.
351
+ def interlaced=(bool)
352
+ GD2FFI.send(:gdImageInterlace, image_ptr, bool ? 1 : 0)
353
+ end
354
+
355
+ # Return *true* if colors will be alpha blended into the image when pixels
356
+ # are modified. Returns *false* if colors will be copied verbatim into the
357
+ # image without alpha blending when pixels are modified.
358
+ def alpha_blending?
359
+ not image_ptr[:alphaBlendingFlag].zero?
360
+ end
361
+
362
+ # Set whether colors should be alpha blended with existing colors when
363
+ # pixels are modified. Alpha blending is not available for IndexedColor
364
+ # images.
365
+ def alpha_blending=(bool)
366
+ GD2FFI.send(:gdImageAlphaBlending, image_ptr, bool ? 1 : 0)
367
+ end
368
+
369
+ # Return *true* if this image will be stored with full alpha channel
370
+ # information when output as PNG.
371
+ def save_alpha?
372
+ not image_ptr[:saveAlphaFlag].zero?
373
+ end
374
+
375
+ # Set whether this image will be stored with full alpha channel information
376
+ # when output as PNG.
377
+ def save_alpha=(bool)
378
+ GD2FFI.send(:gdImageSaveAlpha, image_ptr, bool ? 1 : 0)
379
+ end
380
+
381
+ # Return the transparent color for this image, or *nil* if none has been
382
+ # set.
383
+ def transparent
384
+ pixel = image_ptr[:transparent]
385
+ pixel == -1 ? nil : pixel2color(pixel)
386
+ end
387
+
388
+ # Set or unset the transparent color for this image.
389
+ def transparent=(color)
390
+ GD2FFI.send(:gdImageColorTransparent, image_ptr,
391
+ color.nil? ? -1 : color2pixel(color))
392
+ end
393
+
394
+ # Return the current clipping rectangle. Use Image#with_clipping to
395
+ # temporarily modify the clipping rectangle.
396
+ def clipping
397
+ x1 = FFI::MemoryPointer.new(:pointer)
398
+ y1 = FFI::MemoryPointer.new(:pointer)
399
+ x2 = FFI::MemoryPointer.new(:pointer)
400
+ y2 = FFI::MemoryPointer.new(:pointer)
401
+
402
+ GD2FFI.send(:gdImageGetClip, image_ptr, x1, y1, x2, y2)
403
+ [ x1.read_int, y1.read_int, x2.read_int, y2.read_int ]
404
+ end
405
+
406
+ # Temporarily set the clipping rectangle during the execution of a block.
407
+ # Pixels outside this rectangle will not be modified by drawing or copying
408
+ # operations.
409
+ def with_clipping(x1, y1, x2, y2) #:yields: image
410
+ clip = clipping
411
+ begin
412
+ p clipping
413
+ GD2FFI.send(:gdImageSetClip, image_ptr, x1.to_i, y1.to_i, x2.to_i, y2.to_i)
414
+ p clipping
415
+ yield self
416
+ self
417
+ ensure
418
+ GD2FFI.send(:gdImageSetClip, image_ptr, *clip)
419
+ end
420
+ end
421
+
422
+ # Return *true* if the current clipping rectangle excludes the given point.
423
+ def clips?(x, y)
424
+ GD2FFI.send(:gdImageBoundsSafe, image_ptr, x.to_i, y.to_i).zero?
425
+ end
426
+
427
+ # Provide a drawing environment for a block. See GD2::Canvas.
428
+ def draw #:yields: canvas
429
+ yield Canvas.new(self)
430
+ self
431
+ end
432
+
433
+ # Consolidate duplicate colors in this image, and eliminate all unused
434
+ # palette entries. This only has an effect on IndexedColor images, and
435
+ # is rather expensive. Returns the number of palette entries deallocated.
436
+ def optimize_palette
437
+ # implemented by subclass
438
+ end
439
+
440
+ # Export this image to a file with the given +filename+. The image format
441
+ # is determined by the :format option, or by the file extension (jpeg, png,
442
+ # gif, wbmp, gd, or gd2). Returns the size of the written image data.
443
+ # Additional +options+ are as arguments for the Image#jpeg, Image#png,
444
+ # Image#wbmp, or Image#gd2 methods.
445
+ def export(filename, options = {})
446
+ unless format = options.delete(:format)
447
+ md = filename.match(/\.([^.]+)\z/)
448
+ format = md ? md[1].downcase : nil
449
+ end
450
+ format = format.to_sym if format
451
+
452
+ size = FFI::MemoryPointer.new(:pointer)
453
+
454
+ case format
455
+ when :jpeg, :jpg
456
+ write_sym = :gdImageJpegPtr
457
+ args = [ size, options.delete(:quality) || -1 ]
458
+ when :png
459
+ write_sym = :gdImagePngPtrEx
460
+ args = [ size, options.delete(:level) || -1 ]
461
+ when :gif
462
+ write_sym = :gdImageGifPtr
463
+ args = [ size ]
464
+ when :wbmp
465
+ write_sym = :gdImageWBMPPtr
466
+ fgcolor = options.delete(:fgcolor)
467
+ raise ArgumentError, 'Missing required option :fgcolor' if fgcolor.nil?
468
+ args = [size, color2pixel(fgcolor)]
469
+ when :gd
470
+ write_sym = :gdImageGdPtr
471
+ args = [ size ]
472
+ when :gd2
473
+ write_sym = :gdImageGd2Ptr
474
+ args = [ options.delete(:chunk_size) || 0, options.delete(:fmt) || FMT_COMPRESSED, size ]
475
+ else
476
+ raise UnrecognizedImageTypeError,
477
+ 'Format (or file extension) is not recognized'
478
+ end
479
+
480
+ raise ArgumentError, "Unrecognized options #{options.inspect}" unless
481
+ options.empty?
482
+
483
+ File.open(filename, 'wb') do |file|
484
+ begin
485
+ img = GD2FFI.send(write_sym, image_ptr, *args)
486
+ file.write(img.get_bytes(0, size.get_int(0)))
487
+ ensure
488
+ GD2FFI.gdFree(img)
489
+ end
490
+ end
491
+ end
492
+
493
+ # Encode and return data for this image in JPEG format. The +quality+
494
+ # argument should be in the range 0–95, with higher quality values usually
495
+ # implying both higher quality and larger sizes.
496
+ def jpeg(quality = nil)
497
+ size = FFI::MemoryPointer.new(:pointer)
498
+ ptr = GD2FFI.send(:gdImageJpegPtr, image_ptr, size, quality || -1)
499
+ ptr.get_bytes(0, size.get_int(0))
500
+ ensure
501
+ GD2FFI.send(:gdFree, ptr)
502
+ end
503
+
504
+ # Encode and return data for this image in PNG format. The +level+
505
+ # argument should be in the range 0–9 indicating the level of lossless
506
+ # compression (0 = none, 1 = minimal but fast, 9 = best but slow).
507
+ def png(level = nil)
508
+ size = FFI::MemoryPointer.new(:pointer)
509
+ ptr = GD2FFI.send(:gdImagePngPtrEx, image_ptr, size, level.to_i || -1)
510
+ ptr.get_bytes(0, size.get_int(0))
511
+ ensure
512
+ GD2FFI.send(:gdFree, ptr)
513
+ end
514
+
515
+ # Encode and return data for this image in GIF format. Note that GIF only
516
+ # supports palette images; TrueColor images will be automatically converted
517
+ # to IndexedColor internally in order to create the GIF. Use
518
+ # Image#to_indexed_color to control this conversion more precisely.
519
+ def gif
520
+ size = FFI::MemoryPointer.new(:pointer)
521
+ ptr = GD2FFI.send(:gdImageGifPtr, image_ptr, size)
522
+ ptr.get_bytes(0, size.get_int(0))
523
+ ensure
524
+ GD2FFI.send(:gdFree, ptr)
525
+ end
526
+
527
+ # Encode and return data for this image in WBMP format. WBMP currently
528
+ # supports only black and white images; the specified +fgcolor+ will be
529
+ # used as the foreground color (black), and all other colors will be
530
+ # considered “background” (white).
531
+ def wbmp(fgcolor)
532
+ size = FFI::MemoryPointer.new(:pointer)
533
+ ptr = GD2FFI.send(:gdImageWBMPPtr, image_ptr, size, color2pixel(fgcolor))
534
+ ptr.get_bytes(0, size.get_int(0))
535
+ ensure
536
+ GD2FFI.send(:gdFree, ptr)
537
+ end
538
+
539
+ # Encode and return data for this image in “.gd” format. This is an
540
+ # internal format used by the gd library to quickly read and write images.
541
+ def gd
542
+ size = FFI::MemoryPointer.new(:pointer)
543
+ ptr = GD2FFI.send(:gdImageGdPtr, image_ptr, size)
544
+ ptr.get_bytes(0, size.get_int(0))
545
+ ensure
546
+ GD2FFI.send(:gdFree, ptr)
547
+ end
548
+
549
+ # Encode and return data for this image in “.gd2” format. This is an
550
+ # internal format used by the gd library to quickly read and write images.
551
+ # The specified +fmt+ may be either GD2::FMT_RAW or GD2::FMT_COMPRESSED.
552
+ def gd2(fmt = FMT_COMPRESSED, chunk_size = 0)
553
+ size = FFI::MemoryPointer.new(:pointer)
554
+ ptr = GD2FFI.send(:gdImageGd2Ptr, image_ptr, chunk_size.to_i, fmt.to_i, size)
555
+ ptr.get_bytes(0, size.get_int(0))
556
+ ensure
557
+ GD2FFI.send(:gdFree, ptr)
558
+ end
559
+
560
+ # Copy a portion of another image to this image. If +src_w+ and +src_h+ are
561
+ # specified, the indicated portion of the source image will be resized
562
+ # (and resampled) to fit the indicated dimensions of the destination.
563
+ def copy_from(other, dst_x, dst_y, src_x, src_y,
564
+ dst_w, dst_h, src_w = nil, src_h = nil)
565
+ raise ArgumentError unless src_w.nil? == src_h.nil?
566
+ if src_w
567
+ GD2FFI.send(:gdImageCopyResampled, image_ptr, other.image_ptr,
568
+ dst_x.to_i, dst_y.to_i, src_x.to_i, src_y.to_i, dst_w.to_i, dst_h.to_i, src_w.to_i, src_h.to_i)
569
+ else
570
+ GD2FFI.send(:gdImageCopy, image_ptr, other.image_ptr,
571
+ dst_x.to_i, dst_y.to_i, src_x.to_i, src_y.to_i, dst_w.to_i, dst_h.to_i)
572
+ end
573
+ self
574
+ end
575
+
576
+ # Copy a portion of another image to this image, rotating the source
577
+ # portion first by the indicated +angle+ (in radians). The +dst_x+ and
578
+ # +dst_y+ arguments indicate the _center_ of the desired destination, and
579
+ # may be floating point.
580
+ def copy_from_rotated(other, dst_x, dst_y, src_x, src_y, w, h, angle)
581
+ GD2FFI.send(:gdImageCopyRotated, image_ptr, other.image_ptr,
582
+ dst_x.to_f, dst_y.to_f, src_x.to_i, src_y.to_i, w.to_i, h.to_i, angle.to_degrees.round.to_i)
583
+ self
584
+ end
585
+
586
+ # Merge a portion of another image into this one by the amount specified
587
+ # as +pct+ (a percentage). A percentage of 1.0 is identical to
588
+ # Image#copy_from; a percentage of 0.0 is a no-op. Note that alpha
589
+ # channel information from the source image is ignored.
590
+ def merge_from(other, dst_x, dst_y, src_x, src_y, w, h, pct)
591
+ GD2FFI.send(:gdImageCopyMerge, image_ptr, other.image_ptr,
592
+ dst_x.to_i, dst_y.to_i, src_x.to_i, src_y.to_i, w.to_i, h.to_i, pct.to_percent.round.to_i)
593
+ self
594
+ end
595
+
596
+ # Rotate this image by the given +angle+ (in radians) about the given axis
597
+ # coordinates. Note that some of the edges of the image may be lost.
598
+ def rotate!(angle, axis_x = width / 2.0, axis_y = height / 2.0)
599
+ ptr = self.class.create_image_ptr(width, height, alpha_blending?)
600
+ GD2FFI.send(:gdImageCopyRotated, ptr, image_ptr,
601
+ axis_x.to_f, axis_y.to_f, 0, 0, width.to_i, height.to_i, angle.to_degrees.round.to_i)
602
+ init_with_image(ptr)
603
+ end
604
+
605
+ # Like Image#rotate! except a new image is returned.
606
+ def rotate(angle, axis_x = width / 2.0, axis_y = height / 2.0)
607
+ clone.rotate!(angle, axis_x, axis_y)
608
+ end
609
+
610
+ # Crop this image to the specified dimensions, such that (+x+, +y+) becomes
611
+ # (0, 0).
612
+ def crop!(x, y, w, h)
613
+ ptr = self.class.create_image_ptr(w, h, alpha_blending?)
614
+ GD2FFI.send(:gdImageCopy, ptr, image_ptr, 0, 0, x.to_i, y.to_i, w.to_i, h.to_i)
615
+ init_with_image(ptr)
616
+ end
617
+
618
+ # Like Image#crop! except a new image is returned.
619
+ def crop(x, y, w, h)
620
+ clone.crop!(x, y, w, h)
621
+ end
622
+
623
+ # Expand the left, top, right, and bottom borders of this image by the
624
+ # given number of pixels.
625
+ def uncrop!(x1, y1 = x1, x2 = x1, y2 = y1)
626
+ ptr = self.class.create_image_ptr(x1 + width + x2, y1 + height + y2,
627
+ alpha_blending?)
628
+ GD2FFI.send(:gdImageCopy, ptr, image_ptr, x1.to_i, y1.to_i, 0, 0, width.to_i, height.to_i)
629
+ init_with_image(ptr)
630
+ end
631
+
632
+ # Like Image#uncrop! except a new image is returned.
633
+ def uncrop(x1, y1 = x1, x2 = x1, y2 = y1)
634
+ clone.uncrop!(x1, y1, x2, y2)
635
+ end
636
+
637
+ # Resize this image to the given dimensions. If +resample+ is *true*,
638
+ # the image pixels will be resampled; otherwise they will be stretched or
639
+ # shrunk as necessary without resampling.
640
+ def resize!(w, h, resample = true)
641
+ ptr = self.class.create_image_ptr(w, h, false)
642
+ GD2FFI.send(resample ? :gdImageCopyResampled : :gdImageCopyResized,
643
+ ptr, image_ptr, 0, 0, 0, 0, w.to_i, h.to_i, width.to_i, height.to_i)
644
+ alpha_blending = alpha_blending?
645
+ init_with_image(ptr)
646
+ self.alpha_blending = alpha_blending
647
+ self
648
+ end
649
+
650
+ # Like Image#resize! except a new image is returned.
651
+ def resize(w, h, resample = true)
652
+ clone.resize!(w, h, resample)
653
+ end
654
+
655
+ # Transform this image into a new image of width and height +radius+ × 2,
656
+ # in which the X axis of the original has been remapped to θ (angle) and
657
+ # the Y axis of the original has been remapped to ρ (distance from center).
658
+ # Note that the original image must be square.
659
+ def polar_transform!(radius)
660
+ raise 'Image must be square' unless width == height
661
+ ptr = GD2FFI.send(:gdImageSquareToCircle, image_ptr, radius.to_i)
662
+ raise LibraryError unless ptr
663
+ init_with_image(ptr)
664
+ end
665
+
666
+ # Like Image#polar_transform! except a new image is returned.
667
+ def polar_transform(radius)
668
+ clone.polar_transform!(radius)
669
+ end
670
+
671
+ # Sharpen this image by +pct+ (a percentage) which can be greater than 1.0.
672
+ # Transparency/alpha channel are not altered. This has no effect on
673
+ # IndexedColor images.
674
+ def sharpen(pct)
675
+ self
676
+ end
677
+
678
+ # Return this image as a TrueColor image, creating a copy if necessary.
679
+ def to_true_color
680
+ self
681
+ end
682
+
683
+ # Return this image as an IndexedColor image, creating a copy if necessary.
684
+ # +colors+ indicates the maximum number of palette colors to use, and
685
+ # +dither+ controls whether dithering is used.
686
+ def to_indexed_color(colors = MAX_COLORS, dither = true)
687
+ ptr = GD2FFI.send(:gdImageCreatePaletteFromTrueColor,
688
+ to_true_color.image_ptr, dither ? 1 : 0, colors.to_i)
689
+ raise LibraryError unless ptr
690
+
691
+ obj = IndexedColor.allocate.init_with_image(ptr)
692
+
693
+ # fix for gd bug where image->open[] is not properly initialized
694
+ (0...obj.image_ptr[:colorsTotal]).each do |i|
695
+ obj.image_ptr[:open][i] = 0
696
+ end
697
+
698
+ obj
699
+ end
700
+ end
701
+
702
+ #
703
+ # = Description
704
+ #
705
+ # IndexedColor images select pixel colors indirectly through a palette of
706
+ # up to 256 colors. Use Image#palette to access the associated Palette
707
+ # object.
708
+ #
709
+ class Image::IndexedColor < Image
710
+ def self.create_image_sym #:nodoc:
711
+ :gdImageCreate
712
+ end
713
+
714
+ def self.palette_class #:nodoc:
715
+ Palette::IndexedColor
716
+ end
717
+
718
+ def pixel2color(pixel) #:nodoc:
719
+ palette[pixel]
720
+ end
721
+
722
+ def color2pixel(color) #:nodoc:
723
+ color.from_palette?(palette) ? color.index : palette.exact!(color).index
724
+ end
725
+
726
+ def alpha_blending? #:nodoc:
727
+ false
728
+ end
729
+
730
+ def alpha_blending=(bool) #:nodoc:
731
+ raise 'Alpha blending mode not available for indexed color images' if bool
732
+ end
733
+
734
+ def optimize_palette #:nodoc:
735
+ # first map duplicate colors to a single palette index
736
+ map, cache = palette.inject([{}, Array.new(MAX_COLORS)]) do |ary, color|
737
+ ary.at(0)[color.rgba] = color.index
738
+ ary.at(1)[color.index] = color.rgba
739
+ ary
740
+ end
741
+ each_with_index do |row, y|
742
+ row.each_with_index do |pixel, x|
743
+ set_pixel(x, y, map[cache.at(pixel)])
744
+ end
745
+ end
746
+
747
+ # now clean up the palette
748
+ palette.deallocate_unused
749
+ end
750
+
751
+ def to_true_color #:nodoc:
752
+ sz = size
753
+ obj = TrueColor.new(*sz)
754
+ obj.alpha_blending = false
755
+ obj.copy_from(self, 0, 0, 0, 0, *sz)
756
+ obj.alpha_blending = true
757
+ obj
758
+ end
759
+
760
+ def to_indexed_color(colors = MAX_COLORS, dither = true) #:nodoc:
761
+ palette.used <= colors ? self : super
762
+ end
763
+
764
+ # Like Image#merge_from except an optional final argument can be specified
765
+ # to preserve the hue of the source by converting the destination pixels to
766
+ # grey scale before the merge.
767
+ def merge_from(other, dst_x, dst_y, src_x, src_y, w, h, pct, gray = false)
768
+ return super(other, dst_x, dst_y, src_x, src_y, w, h, pct) unless gray
769
+ GD2FFI.send(:gdImageCopyMergeGray, image_ptr, other.image_ptr,
770
+ dst_x.to_i, dst_y.to_i, src_x.to_i, src_y.to_i, w.to_i, h.to_i, pct.to_percent.round.to_i)
771
+ self
772
+ end
773
+ end
774
+
775
+ #
776
+ # = Description
777
+ #
778
+ # TrueColor images represent pixel colors directly and have no palette
779
+ # limitations.
780
+ #
781
+ class Image::TrueColor < Image
782
+ def self.create_image_sym #:nodoc:
783
+ :gdImageCreateTrueColor
784
+ end
785
+
786
+ def self.palette_class #:nodoc:
787
+ Palette::TrueColor
788
+ end
789
+
790
+ def sharpen(pct) #:nodoc:
791
+ GD2FFI.send(:gdImageSharpen, image_ptr, pct.to_percent.round.to_i)
792
+ self
793
+ end
794
+ end
795
+ end