gd2-ffij 0.0.2

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