rgd2-ffij 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
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