gd2-ffij 0.0.2

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 +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