gd2 1.0

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.
@@ -0,0 +1,343 @@
1
+ #
2
+ # Ruby/GD2 -- Ruby binding for gd 2 graphics library
3
+ #
4
+ # Copyright © 2005 Robert Leslie
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
+ # = Description
26
+ #
27
+ # Font objects represent a particular font in a particular size.
28
+ #
29
+ # == Built-in Fonts
30
+ #
31
+ # The following font classes may be used without further instantiation:
32
+ #
33
+ # Font::Small
34
+ # Font::Large
35
+ # Font::MediumBold
36
+ # Font::Giant
37
+ # Font::Tiny
38
+ #
39
+ # == TrueType Fonts
40
+ #
41
+ # To use a TrueType font, first instantiate the font at a particular size:
42
+ #
43
+ # font = Font::TrueType[fontname, ptsize]
44
+ #
45
+ # Here +fontname+ may be a path to a TrueType font, or a fontconfig pattern
46
+ # if fontconfig support is enabled (see Font::TrueType.fontconfig).
47
+ #
48
+ # See Font::TrueType.new for further options.
49
+ class Font
50
+ private_class_method :new
51
+
52
+ def self.font_ptr #:nodoc:
53
+ SYM[font_sym].call[0]
54
+ end
55
+
56
+ def self.draw(image_ptr, x, y, angle, string, fg) #:nodoc:
57
+ raise ArgumentError, "Angle #{angle} not supported for #{self}" unless
58
+ angle == 0.degrees || angle == 90.degrees
59
+
60
+ SYM[angle > 0 ? :gdImageStringUp : :gdImageString].call(image_ptr,
61
+ font_ptr, x, y, string, fg)
62
+ nil
63
+ end
64
+ end
65
+
66
+ class Font::Small < Font
67
+ def self.font_sym #:nodoc:
68
+ :gdFontGetSmall
69
+ end
70
+ end
71
+
72
+ class Font::Large < Font
73
+ def self.font_sym #:nodoc:
74
+ :gdFontGetLarge
75
+ end
76
+ end
77
+
78
+ class Font::MediumBold < Font
79
+ def self.font_sym #:nodoc:
80
+ :gdFontGetMediumBold
81
+ end
82
+ end
83
+
84
+ class Font::Giant < Font
85
+ def self.font_sym #:nodoc:
86
+ :gdFontGetGiant
87
+ end
88
+ end
89
+
90
+ class Font::Tiny < Font
91
+ def self.font_sym #:nodoc:
92
+ :gdFontGetTiny
93
+ end
94
+ end
95
+
96
+ class Font::TrueType
97
+ class FontconfigError < StandardError; end
98
+ class FreeTypeError < StandardError; end
99
+
100
+ CHARMAP_UNICODE = 0
101
+ CHARMAP_SHIFT_JIS = 1
102
+ CHARMAP_BIG5 = 2
103
+
104
+ FTEX_LINESPACE = 1
105
+ FTEX_CHARMAP = 2
106
+ FTEX_RESOLUTION = 4
107
+ FTEX_DISABLE_KERNING = 8
108
+ FTEX_XSHOW = 16
109
+ FTEX_FONTPATHNAME = 32
110
+ FTEX_FONTCONFIG = 64
111
+ FTEX_RETURNFONTPATHNAME = 128
112
+
113
+ @@fontcount = 0
114
+ @@fontconfig = false
115
+
116
+ def self.register(font) #:nodoc:
117
+ Thread.critical = true
118
+
119
+ count = @@fontcount
120
+ @@fontcount += 1
121
+
122
+ if count.zero?
123
+ raise FreeTypeError, 'FreeType library failed to initialize' unless
124
+ SYM[:gdFontCacheSetup].call[0].zero?
125
+ end
126
+
127
+ ObjectSpace.define_finalizer(font, font_finalizer)
128
+ ensure
129
+ Thread.critical = false
130
+ end
131
+
132
+ def self.font_finalizer
133
+ proc { unregister }
134
+ end
135
+
136
+ def self.unregister
137
+ Thread.critical = true
138
+
139
+ @@fontcount -= 1
140
+ SYM[:gdFontCacheShutdown].call if @@fontcount.zero?
141
+ ensure
142
+ Thread.critical = false
143
+ end
144
+
145
+ private_class_method :font_finalizer, :unregister
146
+
147
+ def self.fontconfig #:nodoc:
148
+ @@fontconfig
149
+ end
150
+
151
+ # Return a boolean indicating whether fontconfig support has been enabled.
152
+ # The default is *false*.
153
+ def self.fontconfig?
154
+ fontconfig
155
+ end
156
+
157
+ # Set whether fontconfig support should be enabled. To use this, the GD
158
+ # library must have been built with fontconfig support. Raises an error if
159
+ # fontconfig support is unavailable.
160
+ def self.fontconfig=(want)
161
+ avail = !SYM[:gdFTUseFontConfig].call(want ? 1 : 0)[0].zero?
162
+ raise FontconfigError, 'Fontconfig not available' if want && !avail
163
+ @@fontconfig = want
164
+ end
165
+
166
+ class << self
167
+ alias [] new
168
+ end
169
+
170
+ # The effective path to this TrueType font
171
+ attr_reader :fontpath
172
+
173
+ # The chosen linespacing
174
+ attr_reader :linespacing
175
+
176
+ # The chosen charmap
177
+ attr_reader :charmap
178
+
179
+ # The chosen horizontal resolution hint
180
+ attr_reader :hdpi
181
+
182
+ # The chosen vertical resolution hint
183
+ attr_reader :vdpi
184
+
185
+ # Whether kerning is desired
186
+ attr_reader :kerning
187
+
188
+ # Instantiate a TrueType font given by +fontname+ (either a pathname or a
189
+ # fontconfig pattern if fontconfig is enabled) and +ptsize+ (a point size
190
+ # given as a floating point number).
191
+ #
192
+ # The possible +options+ are:
193
+ #
194
+ # - :linespacing => The desired line spacing for multiline text, expressed as
195
+ # a multiple of the font height. A line spacing of 1.0 is the minimum to
196
+ # guarantee that lines of text do not collide. The default according to GD
197
+ # is 1.05.
198
+ #
199
+ # - :charmap => Specify a preference for Unicode, Shift_JIS, or Big5
200
+ # character encoding. Use one of the constants
201
+ # Font::TrueType::CHARMAP_UNICODE, Font::TrueType::CHARMAP_SHIFT_JIS, or
202
+ # Font::TrueType::CHARMAP_BIG5.
203
+ #
204
+ # - :hdpi => The horizontal resolution hint for the rendering engine. The
205
+ # default according to GD is 96 dpi.
206
+ #
207
+ # - :vdpi => The vertical resolution hint for the rendering engine. The
208
+ # default according to GD is 96 dpi.
209
+ #
210
+ # - :dpi => A shortcut to specify both :hdpi and :vdpi.
211
+ #
212
+ # - :kerning => A boolean to specify whether kerning tables should be used,
213
+ # if fontconfig is available. The default is *true*.
214
+ #
215
+ def initialize(fontname, ptsize, options = {})
216
+ @fontname, @ptsize = fontname, ptsize.to_f
217
+ @linespacing = options.delete(:linespacing)
218
+ @linespacing = @linespacing.to_f if @linespacing
219
+ @charmap = options.delete(:charmap)
220
+ @hdpi = options.delete(:hdpi)
221
+ @vdpi = options.delete(:vdpi)
222
+ if dpi = options.delete(:dpi)
223
+ @hdpi ||= dpi
224
+ @vdpi ||= dpi
225
+ end
226
+ @kerning = options.delete(:kerning)
227
+ @kerning = true if @kerning.nil?
228
+
229
+ self.class.register(self)
230
+
231
+ # Get the font path (and verify existence of file)
232
+
233
+ strex = strex(false, true)
234
+ r, rs = SYM[:gdImageStringFTEx].call(nil,
235
+ Array.new(8) { 0 }, 0, @fontname, @ptsize, 0.0, 0, 0, '', strex)
236
+ raise FreeTypeError, r if r
237
+
238
+ strex[:fontpath].free = SYM[:gdFree]
239
+ @fontpath = strex[:fontpath].to_s
240
+ end
241
+
242
+ def inspect #:nodoc:
243
+ result = "#<#{self.class} #{@fontpath.inspect}, #{@ptsize}"
244
+ result += ", :linespacing => #{@linespacing}" if @linespacing
245
+ result += ", :charmap => #{@charmap}" if @charmap
246
+ result += ", :hdpi => #{@hdpi}" if @hdpi
247
+ result += ", :vdpi => #{@vdpi}" if @vdpi
248
+ result += ", :kerning => #{@kerning}" unless @kerning
249
+ result += '>'
250
+ end
251
+
252
+ def draw(image_ptr, x, y, angle, string, fg) #:nodoc:
253
+ brect = Array.new(8) { 0 }
254
+ strex = strex(true)
255
+ r, rs = SYM[:gdImageStringFTEx].call(image_ptr,
256
+ brect, fg, @fontname, @ptsize, angle.to_f, x, y,
257
+ string.gsub('&', '&amp;'), strex)
258
+ raise FreeTypeError, r if r
259
+
260
+ brect = rs[1].to_a('I', 8)
261
+
262
+ if xshow = strex[:xshow]
263
+ xshow.free = SYM[:gdFree]
264
+ xshow = xshow.to_s.split(' ').map { |e| e.to_f }
265
+ else
266
+ xshow = []
267
+ end
268
+
269
+ sum = 0.0
270
+ position = Array.new(xshow.length + 1)
271
+ xshow.each_with_index do |advance, i|
272
+ position[i] = sum
273
+ sum += advance
274
+ end
275
+ position[-1] = sum
276
+
277
+ { :lower_left => [brect[0], brect[1]],
278
+ :lower_right => [brect[2], brect[3]],
279
+ :upper_right => [brect[4], brect[5]],
280
+ :upper_left => [brect[6], brect[7]],
281
+ :position => position
282
+ }
283
+ end
284
+
285
+ def draw_circle(image_ptr, cx, cy, radius, text_radius, fill_portion,
286
+ top, bottom, fgcolor) #:nodoc:
287
+ r, rs = SYM[:gdImageStringFTCircle].call(image_ptr, cx, cy,
288
+ radius.to_f, text_radius.to_f, fill_portion.to_f, @fontname, @ptsize,
289
+ top || '', bottom || '', fgcolor)
290
+ raise FreeTypeError, r if r
291
+ nil
292
+ end
293
+
294
+ # Return a hash describing the rectangle that would enclose the given
295
+ # string rendered in this font at the given angle. The returned hash
296
+ # contains the following keys:
297
+ #
298
+ # - :lower_left => The [x, y] coordinates of the lower left corner.
299
+ # - :lower_right => The [x, y] coordinates of the lower right corner.
300
+ # - :upper_right => The [x, y] coordinates of the upper right corner.
301
+ # - :upper_left => The [x, y] coordinates of the upper left corner.
302
+ # - :position => An array of floating point character position offsets for
303
+ # each character of the +string+, beginning with 0.0. The array also
304
+ # includes a final position indicating where the last character ends.
305
+ #
306
+ # The _upper_, _lower_, _left_, and _right_ references are relative to the
307
+ # text of the +string+, regardless of the +angle+.
308
+ def bounding_rectangle(string, angle = 0.0)
309
+ data = draw(nil, 0, 0, angle, string, 0)
310
+
311
+ if string.length == 1
312
+ # gd annoyingly fails to provide xshow data for strings of length 1
313
+ position = draw(nil, 0, 0, angle, string + ' ', 0)[:position]
314
+ data[:position] = position[0...-1]
315
+ end
316
+
317
+ data
318
+ end
319
+
320
+ private
321
+
322
+ def strex(xshow = false, returnfontpathname = false)
323
+ flags = 0
324
+ flags |= FTEX_LINESPACE if @linespacing
325
+ flags |= FTEX_CHARMAP if @charmap
326
+ flags |= FTEX_RESOLUTION if @hdpi || @vdpi
327
+ flags |= FTEX_DISABLE_KERNING unless @kerning
328
+ flags |= FTEX_XSHOW if xshow
329
+ flags |= FTEX_RETURNFONTPATHNAME if returnfontpathname
330
+
331
+ strex = DL.malloc(DL.sizeof('IDIIISS'))
332
+ strex.struct! 'IDIIISS', :flags, :linespacing, :charmap, :hdpi, :vdpi,
333
+ :xshow, :fontpath
334
+
335
+ strex[:flags] = flags
336
+ strex[:linespacing] = @linespacing || 0.0
337
+ strex[:charmap] = @charmap
338
+ strex[:hdpi] = @hdpi || @vdpi || 0
339
+ strex[:vdpi] = @vdpi || @hdpi || 0
340
+ strex
341
+ end
342
+ end
343
+ end
@@ -0,0 +1,773 @@
1
+ #
2
+ # Ruby/GD2 -- Ruby binding for gd 2 graphics library
3
+ #
4
+ # Copyright © 2005 Robert Leslie
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
+ create = {
97
+ :jpeg => :gdImageCreateFromJpeg,
98
+ :png => :gdImageCreateFromPng,
99
+ :gif => :gdImageCreateFromGif,
100
+ :wbmp => :gdImageCreateFromWBMP,
101
+ :gd2 => :gdImageCreateFromGd2
102
+ }
103
+ args = [src.to_ptr]
104
+ when String
105
+ magic = src
106
+ create = {
107
+ :jpeg => :gdImageCreateFromJpegPtr,
108
+ :png => :gdImageCreateFromPngPtr,
109
+ :gif => :gdImageCreateFromGifPtr,
110
+ :wbmp => :gdImageCreateFromWBMPPtr,
111
+ :gd2 => :gdImageCreateFromGd2Ptr
112
+ }
113
+ args = [src.length, src]
114
+ else
115
+ raise TypeError, 'Unexpected argument type'
116
+ end
117
+
118
+ type = data_type(magic) or
119
+ raise UnrecognizedImageTypeError, 'Image data format is not recognized'
120
+ ptr = SYM[create[type]].call(*args)[0]
121
+ raise LibraryError unless ptr
122
+
123
+ init_image_ptr(ptr)
124
+
125
+ image = (image_true_color?(ptr) ?
126
+ TrueColor : IndexedColor).allocate.init_with_image(ptr)
127
+
128
+ block_given? ? yield(image) : image
129
+ end
130
+
131
+ def self.data_type(str)
132
+ case str
133
+ when /\A\xff\xd8/
134
+ :jpeg
135
+ when /\A\x89PNG/
136
+ :png
137
+ when /\AGIF8/
138
+ :gif
139
+ when /\A\x00/
140
+ :wbmp
141
+ when /\Agd2/
142
+ :gd2
143
+ end
144
+ end
145
+ private_class_method :data_type
146
+
147
+ # Import an image from a file with the given +filename+. The file extension
148
+ # is used to determine the image type (JPEG, PNG, GIF, WBMP, GD, GD2, XBM,
149
+ # or XPM). The resulting image will be either of class Image::TrueColor or
150
+ # Image::IndexedColor.
151
+ #
152
+ # If the file type is GD2, it is optionally possible to extract only a part
153
+ # of the image. Use options :x, :y, :w, and :h to specify the part of the
154
+ # image to import.
155
+ def self.import(filename, options = {})
156
+ md = filename.match /\.([^.]+)\z/
157
+ ext = md ? md[1].downcase : nil
158
+ if ext == 'xpm'
159
+ raise ArgumentError, "Unexpected options #{options.inspect}" unless
160
+ options.empty?
161
+ ptr = SYM[:gdImageCreateFromXpm].call(filename)[0]
162
+ elsif ext == 'gd2' && !options.empty?
163
+ x, y, w, h =
164
+ options.delete(:x) || 0, options.delete(:y) || 0,
165
+ options.delete(:w), options.delete(:h)
166
+ raise ArgumentError, "Unexpected options #{options.inspect}" unless
167
+ options.empty?
168
+ raise ArgumentError, 'Missing required option :w' if w.nil?
169
+ raise ArgumentError, 'Missing required option :h' if h.nil?
170
+ ptr = File.open(filename) do |file|
171
+ SYM[:gdImageCreateFromGd2Part].call(file, x, y, w, h)[0]
172
+ end
173
+ else
174
+ raise ArgumentError, "Unexpected options #{options.inspect}" unless
175
+ options.empty?
176
+ create_sym = {
177
+ 'jpeg' => :gdImageCreateFromJpeg,
178
+ 'jpg' => :gdImageCreateFromJpeg,
179
+ 'png' => :gdImageCreateFromPng,
180
+ 'gif' => :gdImageCreateFromGif,
181
+ 'wbmp' => :gdImageCreateFromWBMP,
182
+ 'gd' => :gdImageCreateFromGd,
183
+ 'gd2' => :gdImageCreateFromGd2,
184
+ 'xbm' => :gdImageCreateFromXbm
185
+ }[ext]
186
+ raise UnrecognizedImageTypeError,
187
+ 'File extension is not recognized' unless create_sym
188
+ ptr = File.open(filename) { |file| SYM[create_sym].call(file)[0] }
189
+ end
190
+ raise LibraryError unless ptr
191
+
192
+ init_image_ptr(ptr)
193
+
194
+ image = (image_true_color?(ptr) ?
195
+ TrueColor : IndexedColor).allocate.init_with_image(ptr)
196
+
197
+ block_given? ? yield(image) : image
198
+ end
199
+
200
+ def self.init_image_ptr(ptr) #:nodoc:
201
+ ptr.size = 7268
202
+ ptr.free = SYM[:gdImageDestroy]
203
+
204
+ c_ary = 'I' * MAX_COLORS
205
+ eval %{
206
+ ptr.struct!("PIII#{c_ary}#{c_ary}#{c_ary}#{c_ary}" \
207
+ "IPIPP#{c_ary}#{c_ary}IIPII#{c_ary}IPII",
208
+ :pixels, :sx, :sy, :colorsTotal,
209
+ } + Array.new(MAX_COLORS) { |i| ":\"red[#{i}]\", " }.join('') +
210
+ Array.new(MAX_COLORS) { |i| ":\"green[#{i}]\", " }.join('') +
211
+ Array.new(MAX_COLORS) { |i| ":\"blue[#{i}]\", " }.join('') +
212
+ Array.new(MAX_COLORS) { |i| ":\"open[#{i}]\", " }.join('') + %{
213
+ :transparent, :polyInts, :polyAllocated, :brush, :tile,
214
+ } + Array.new(MAX_COLORS) { |i| ":\"brushColorMap[#{i}]\", " }.join('') +
215
+ Array.new(MAX_COLORS) { |i| ":\"tileColorMap[#{i}]\", " }.join('') + %{
216
+ :styleLength, :stylePos, :style, :interlace, :thick,
217
+ } + Array.new(MAX_COLORS) { |i| ":\"alpha[#{i}]\", " }.join('') + %{
218
+ :trueColor, :tpixels, :alphaBlendingFlag, :saveAlphaFlag)
219
+ }
220
+ end
221
+
222
+ def self.image_true_color?(ptr)
223
+ not ptr[:trueColor].zero?
224
+ end
225
+ private_class_method :image_true_color?
226
+
227
+ def self.create_image_ptr(sx, sy, alpha_blending = true) #:nodoc:
228
+ ptr = SYM[create_image_sym].call(sx, sy)[0]
229
+ SYM[:gdImageAlphaBlending].call(ptr, alpha_blending ? 1 : 0)
230
+ ptr
231
+ end
232
+
233
+ def init_with_size(sx, sy) #:nodoc:
234
+ init_with_image self.class.create_image_ptr(sx, sy)
235
+ end
236
+
237
+ def init_with_image(ptr) #:nodoc:
238
+ # reentrant
239
+ self.class.init_image_ptr(ptr) unless ptr.size > 0
240
+ @image_ptr = ptr
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
+ SYM[:gdImageCompare].call(image_ptr, other.image_ptr)[0]
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
+ SYM[:gdImageGetPixel].call(@image_ptr, x, y)[0]
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
+ SYM[:gdImageSetPixel].call(@image_ptr, x, y, value)
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
+ # optimize for speed
324
+ get_pixel = SYM[:gdImageGetPixel]
325
+ ptr = image_ptr
326
+ (0...height).each do |y|
327
+ row = (0...width).inject(Array.new(width)) do |row, x|
328
+ row[x] = get_pixel.call(ptr, x, y).at(0)
329
+ row
330
+ end
331
+ yield row
332
+ end
333
+ end
334
+
335
+ # Return a Color object for the given +pixel+ value.
336
+ def pixel2color(pixel)
337
+ Color.new_from_rgba(pixel)
338
+ end
339
+
340
+ # Return a pixel value for the given +color+ object.
341
+ def color2pixel(color)
342
+ color.rgba
343
+ end
344
+
345
+ # Return *true* if this image will be stored in interlaced form when output
346
+ # as PNG or JPEG.
347
+ def interlaced?
348
+ not image_ptr[:interlace].zero?
349
+ end
350
+
351
+ # Set whether this image will be stored in interlaced form when output as
352
+ # PNG or JPEG.
353
+ def interlaced=(bool)
354
+ SYM[:gdImageInterlace].call(image_ptr, bool ? 1 : 0)
355
+ end
356
+
357
+ # Return *true* if colors will be alpha blended into the image when pixels
358
+ # are modified. Returns *false* if colors will be copied verbatim into the
359
+ # image without alpha blending when pixels are modified.
360
+ def alpha_blending?
361
+ not image_ptr[:alphaBlendingFlag].zero?
362
+ end
363
+
364
+ # Set whether colors should be alpha blended with existing colors when
365
+ # pixels are modified. Alpha blending is not available for IndexedColor
366
+ # images.
367
+ def alpha_blending=(bool)
368
+ SYM[:gdImageAlphaBlending].call(image_ptr, bool ? 1 : 0)
369
+ end
370
+
371
+ # Return *true* if this image will be stored with full alpha channel
372
+ # information when output as PNG.
373
+ def save_alpha?
374
+ not image_ptr[:saveAlphaFlag].zero?
375
+ end
376
+
377
+ # Set whether this image will be stored with full alpha channel information
378
+ # when output as PNG.
379
+ def save_alpha=(bool)
380
+ SYM[:gdImageSaveAlpha].call(image_ptr, bool ? 1 : 0)
381
+ end
382
+
383
+ # Return the transparent color for this image, or *nil* if none has been
384
+ # set.
385
+ def transparent
386
+ pixel = image_ptr[:transparent]
387
+ pixel == -1 ? nil : pixel2color(pixel)
388
+ end
389
+
390
+ # Set or unset the transparent color for this image.
391
+ def transparent=(color)
392
+ SYM[:gdImageColorTransparent].call(image_ptr,
393
+ color.nil? ? -1 : color2pixel(color))
394
+ end
395
+
396
+ # Return the current clipping rectangle. Use Image#with_clipping to
397
+ # temporarily modify the clipping rectangle.
398
+ def clipping
399
+ r, rs = SYM[:gdImageGetClip].call(image_ptr, 0, 0, 0, 0)
400
+ rs[1, 4]
401
+ end
402
+
403
+ # Temporarily set the clipping rectangle during the execution of a block.
404
+ # Pixels outside this rectangle will not be modified by drawing or copying
405
+ # operations.
406
+ def with_clipping(x1, y1, x2, y2) #:yields: image
407
+ clip = clipping
408
+ begin
409
+ set_clip = SYM[:gdImageSetClip]
410
+ set_clip.call(image_ptr, x1, y1, x2, y2)
411
+ yield self
412
+ self
413
+ ensure
414
+ set_clip.call(image_ptr, *clip)
415
+ end
416
+ end
417
+
418
+ # Return *true* if the current clipping rectangle excludes the given point.
419
+ def clips?(x, y)
420
+ SYM[:gdImageBoundsSafe].call(image_ptr, x, y)[0].zero?
421
+ end
422
+
423
+ # Provide a drawing environment for a block. See GD2::Canvas.
424
+ def draw #:yields: canvas
425
+ yield Canvas.new(self)
426
+ self
427
+ end
428
+
429
+ # Consolidate duplicate colors in this image, and eliminate all unused
430
+ # palette entries. This only has an effect on IndexedColor images, and
431
+ # is rather expensive. Returns the number of palette entries deallocated.
432
+ def optimize_palette
433
+ # implemented by subclass
434
+ end
435
+
436
+ # Export this image to a file with the given +filename+. The image format
437
+ # is determined by the file extension (JPEG, PNG, GIF, WBMP, GD, or GD2).
438
+ # Returns the size of the written image data. The +options+ are as
439
+ # arguments for the Image#jpeg, Image#png, Image#wbmp, or Image#gd2
440
+ # methods.
441
+ def export(filename, options = {})
442
+ md = filename.match /\.([^.]+)\z/
443
+ case ext = md ? md[1].downcase : nil
444
+ when 'jpeg', 'jpg'
445
+ write_sym = :gdImageJpeg
446
+ args = [nil, options.delete(:quality) || -1]
447
+ when 'png'
448
+ write_sym = :gdImagePngEx
449
+ args = [nil, options.delete(:level) || -1]
450
+ when 'gif'
451
+ write_sym = :gdImageGif
452
+ args = [nil]
453
+ when 'wbmp'
454
+ write_sym = :gdImageWBMP
455
+ fgcolor = options.delete(:fgcolor)
456
+ raise ArgumentError, 'Missing required option :fgcolor' if fgcolor.nil?
457
+ args = [color2pixel(fgcolor), nil]
458
+ when 'gd'
459
+ write_sym = :gdImageGd
460
+ args = [nil]
461
+ when 'gd2'
462
+ write_sym = :gdImageGd2
463
+ args = [nil, options.delete(:chunk_size) || 0,
464
+ options.delete(:fmt) || options.delete(:format) || FMT_COMPRESSED]
465
+ else
466
+ raise UnrecognizedImageTypeError,
467
+ 'File extension is not recognized'
468
+ end
469
+
470
+ raise ArgumentError, "Unrecognized options #{options.inspect}" unless
471
+ options.empty?
472
+
473
+ File.open(filename, 'w') do |file|
474
+ args[args[0].nil? ? 0 : 1] = file
475
+ SYM[write_sym].call(image_ptr, *args)
476
+ file.pos
477
+ end
478
+ end
479
+
480
+ # Encode and return data for this image in JPEG format. The +quality+
481
+ # argument should be in the range 0–95, with higher quality values usually
482
+ # implying both higher quality and larger sizes.
483
+ def jpeg(quality = nil)
484
+ ptr, rs = SYM[:gdImageJpegPtr].call(image_ptr, 0, quality || -1)
485
+ ptr.free = SYM[:gdFree]
486
+ ptr[0, rs[1]]
487
+ end
488
+
489
+ # Encode and return data for this image in PNG format. The +level+
490
+ # argument should be in the range 0–9 indicating the level of lossless
491
+ # compression (0 = none, 1 = minimal but fast, 9 = best but slow).
492
+ def png(level = nil)
493
+ ptr, rs = SYM[:gdImagePngPtrEx].call(image_ptr, 0, level || -1)
494
+ ptr.free = SYM[:gdFree]
495
+ ptr[0, rs[1]]
496
+ end
497
+
498
+ # Encode and return data for this image in GIF format. Note that GIF only
499
+ # supports palette images; TrueColor images will be automatically converted
500
+ # to IndexedColor internally in order to create the GIF. Use
501
+ # Image#to_indexed_color to control this conversion more precisely.
502
+ def gif
503
+ ptr, rs = SYM[:gdImageGifPtr].call(image_ptr, 0)
504
+ ptr.free = SYM[:gdFree]
505
+ ptr[0, rs[1]]
506
+ end
507
+
508
+ # Encode and return data for this image in WBMP format. WBMP currently
509
+ # supports only black and white images; the specified +fgcolor+ will be
510
+ # used as the foreground color (black), and all other colors will be
511
+ # considered “background” (white).
512
+ def wbmp(fgcolor)
513
+ ptr, rs = SYM[:gdImageWBMPPtr].call(image_ptr, 0, color2pixel(fgcolor))
514
+ ptr.free = SYM[:gdFree]
515
+ ptr[0, rs[1]]
516
+ end
517
+
518
+ # Encode and return data for this image in “.gd” format. This is an
519
+ # internal format used by the gd library to quickly read and write images.
520
+ def gd
521
+ ptr, rs = SYM[:gdImageGdPtr].call(image_ptr, 0)
522
+ ptr.free = SYM[:gdFree]
523
+ ptr[0, rs[1]]
524
+ end
525
+
526
+ # Encode and return data for this image in “.gd2” format. This is an
527
+ # internal format used by the gd library to quickly read and write images.
528
+ # The specified +fmt+ may be either GD2::FMT_RAW or GD2::FMT_COMPRESSED.
529
+ def gd2(fmt = FMT_COMPRESSED, chunk_size = 0)
530
+ ptr, rs = SYM[:gdImageGd2Ptr].call(image_ptr, chunk_size, fmt, 0)
531
+ ptr.free = SYM[:gdFree]
532
+ ptr[0, rs[3]]
533
+ end
534
+
535
+ # Copy a portion of another image to this image. If +src_w+ and +src_h+ are
536
+ # specified, the indicated portion of the source image will be resized
537
+ # (and resampled) to fit the indicated dimensions of the destination.
538
+ def copy_from(other, dst_x, dst_y, src_x, src_y,
539
+ dst_w, dst_h, src_w = nil, src_h = nil)
540
+ raise ArgumentError unless src_w.nil? == src_h.nil?
541
+ if src_w
542
+ SYM[:gdImageCopyResampled].call(image_ptr, other.image_ptr,
543
+ dst_x, dst_y, src_x, src_y, dst_w, dst_h, src_w, src_h)
544
+ else
545
+ SYM[:gdImageCopy].call(image_ptr, other.image_ptr,
546
+ dst_x, dst_y, src_x, src_y, dst_w, dst_h)
547
+ end
548
+ self
549
+ end
550
+
551
+ # Copy a portion of another image to this image, rotating the source
552
+ # portion first by the indicated +angle+. The +dst_x+ and +dst_y+ arguments
553
+ # indicate the _center_ of the desired destination, and may be floating
554
+ # point.
555
+ def copy_from_rotated(other, dst_x, dst_y, src_x, src_y, w, h, angle)
556
+ SYM[:gdImageCopyRotated].call(image_ptr, other.image_ptr,
557
+ dst_x.to_f, dst_y.to_f, src_x, src_y, w, h, angle.to_degrees.round)
558
+ self
559
+ end
560
+
561
+ # Merge a portion of another image into this one by the amount specified
562
+ # as +pct+ (a percentage). A percentage of 1.0 is identical to
563
+ # Image#copy_from; a percentage of 0.0 is a no-op. Note that alpha
564
+ # channel information from the source image is ignored.
565
+ def merge_from(other, dst_x, dst_y, src_x, src_y, w, h, pct)
566
+ SYM[:gdImageCopyMerge].call(image_ptr, other.image_ptr,
567
+ dst_x, dst_y, src_x, src_y, w, h, pct.to_percent.round)
568
+ self
569
+ end
570
+
571
+ # Rotate this image by the given +angle+ about the given axis coordinates.
572
+ # Note that some of the edges of the image may be lost.
573
+ def rotate!(angle, axis_x = width / 2.0, axis_y = height / 2.0)
574
+ ptr = self.class.create_image_ptr(width, height, alpha_blending?)
575
+ SYM[:gdImageCopyRotated].call(ptr, image_ptr,
576
+ axis_x.to_f, axis_y.to_f, 0, 0, width, height, angle.to_degrees.round)
577
+ init_with_image(ptr)
578
+ end
579
+
580
+ # Like Image#rotate! except a new image is returned.
581
+ def rotate(angle, axis_x = width / 2.0, axis_y = height / 2.0)
582
+ clone.rotate!(angle, axis_x, axis_y)
583
+ end
584
+
585
+ # Crop this image to the specified dimensions, such that (+x+, +y+) becomes
586
+ # (0, 0).
587
+ def crop!(x, y, w, h)
588
+ ptr = self.class.create_image_ptr(w, h, alpha_blending?)
589
+ SYM[:gdImageCopy].call(ptr, image_ptr, 0, 0, x, y, w, h)
590
+ init_with_image(ptr)
591
+ end
592
+
593
+ # Like Image#crop! except a new image is returned.
594
+ def crop(x, y, w, h)
595
+ clone.crop!(x, y, w, h)
596
+ end
597
+
598
+ # Expand the left, top, right, and bottom borders of this image by the
599
+ # given number of pixels.
600
+ def uncrop!(x1, y1 = x1, x2 = x1, y2 = y1)
601
+ ptr = self.class.create_image_ptr(x1 + width + x2, y1 + height + y2,
602
+ alpha_blending?)
603
+ SYM[:gdImageCopy].call(ptr, image_ptr, x1, y1, 0, 0, width, height)
604
+ init_with_image(ptr)
605
+ end
606
+
607
+ # Like Image#uncrop! except a new image is returned.
608
+ def uncrop(x1, y1 = x1, x2 = x1, y2 = y1)
609
+ clone.uncrop!(x1, y1, x2, y2)
610
+ end
611
+
612
+ # Resize this image to the given dimensions. If +resample+ is *true*,
613
+ # the image pixels will be resampled; otherwise they will be stretched or
614
+ # shrunk as necessary without resampling.
615
+ def resize!(w, h, resample = true)
616
+ ptr = self.class.create_image_ptr(w, h, false)
617
+ SYM[resample ? :gdImageCopyResampled : :gdImageCopyResized].call(
618
+ ptr, image_ptr, 0, 0, 0, 0, w, h, width, height)
619
+ alpha_blending = alpha_blending?
620
+ init_with_image(ptr)
621
+ self.alpha_blending = alpha_blending
622
+ self
623
+ end
624
+
625
+ # Like Image#resize! except a new image is returned.
626
+ def resize(w, h, resample = true)
627
+ clone.resize!(w, h, resample)
628
+ end
629
+
630
+ # Transform this image into a new image of width and height +radius+ × 2,
631
+ # in which the X axis of the original has been remapped to θ (angle) and
632
+ # the Y axis of the original has been remapped to ρ (distance from center).
633
+ # Note that the original image must be square.
634
+ def polar_transform!(radius)
635
+ raise 'Image must be square' unless width == height
636
+ ptr = SYM[:gdImageSquareToCircle].call(image_ptr, radius)[0]
637
+ raise LibraryError unless ptr
638
+ init_with_image(ptr)
639
+ end
640
+
641
+ # Like Image#polar_transform! except a new image is returned.
642
+ def polar_transform(radius)
643
+ clone.polar_transform!(radius)
644
+ end
645
+
646
+ # Sharpen this image by +pct+ (a percentage) which can be greater than 1.0.
647
+ # Transparency/alpha channel are not altered. This has no effect on
648
+ # IndexedColor images.
649
+ def sharpen(pct)
650
+ self
651
+ end
652
+
653
+ # Return this image as a TrueColor image, creating a copy if necessary.
654
+ def to_true_color
655
+ self
656
+ end
657
+
658
+ # Return this image as an IndexedColor image, creating a copy if necessary.
659
+ # +colors+ indicates the maximum number of palette colors to use, and
660
+ # +dither+ controls whether dithering is used.
661
+ def to_indexed_color(colors = MAX_COLORS, dither = true)
662
+ obj = IndexedColor.allocate
663
+ ptr = SYM[:gdImageCreatePaletteFromTrueColor].call(
664
+ to_true_color.image_ptr, dither ? 1 : 0, colors)[0]
665
+ raise LibraryError unless ptr
666
+
667
+ obj.init_with_image(ptr)
668
+
669
+ # fix for gd bug where image->open[] is not properly initialized
670
+ (0...ptr[:colorsTotal]).each do |i|
671
+ ptr[:"open[#{i}]"] = 0
672
+ end
673
+
674
+ obj
675
+ end
676
+ end
677
+
678
+ #
679
+ # = Description
680
+ #
681
+ # IndexedColor images select pixel colors indirectly through a palette of
682
+ # up to 256 colors. Use Image#palette to access the associated Palette
683
+ # object.
684
+ #
685
+ class Image::IndexedColor < Image
686
+ def self.create_image_sym #:nodoc:
687
+ :gdImageCreate
688
+ end
689
+
690
+ def self.palette_class #:nodoc:
691
+ Palette::IndexedColor
692
+ end
693
+
694
+ def pixel2color(pixel) #:nodoc:
695
+ palette[pixel]
696
+ end
697
+
698
+ def color2pixel(color) #:nodoc:
699
+ return color.index if color.from_palette?(palette)
700
+ palette.exact!(color).index
701
+ end
702
+
703
+ def alpha_blending? #:nodoc:
704
+ false
705
+ end
706
+
707
+ def alpha_blending=(bool) #:nodoc:
708
+ raise 'Alpha blending mode not available for indexed color images' if bool
709
+ end
710
+
711
+ def optimize_palette #:nodoc:
712
+ # first map duplicate colors to a single palette index
713
+ map, cache = palette.inject([{}, Array.new(MAX_COLORS)]) do |ary, color|
714
+ ary.at(0)[color.rgba] = color.index
715
+ ary.at(1)[color.index] = color.rgba
716
+ ary
717
+ end
718
+ each_with_index do |row, y|
719
+ row.each_with_index do |pixel, x|
720
+ set_pixel(x, y, map[cache.at(pixel)])
721
+ end
722
+ end
723
+
724
+ # now clean up the palette
725
+ palette.deallocate_unused
726
+ end
727
+
728
+ def to_true_color #:nodoc:
729
+ sz = size
730
+ obj = TrueColor.new(*sz)
731
+ obj.alpha_blending = false
732
+ obj.copy_from(self, 0, 0, 0, 0, *sz)
733
+ obj.alpha_blending = true
734
+ obj
735
+ end
736
+
737
+ def to_indexed_color(colors = MAX_COLORS, dither = true) #:nodoc:
738
+ return self if palette.used <= colors
739
+ super
740
+ end
741
+
742
+ # Like Image#merge_from except an optional final argument can be specified
743
+ # to preserve the hue of the source by converting the destination pixels to
744
+ # grey scale before the merge.
745
+ def merge_from(other, dst_x, dst_y, src_x, src_y, w, h, pct, gray = false)
746
+ return super(other, dst_x, dst_y, src_x, src_y, w, h, pct) unless gray
747
+ SYM[:gdImageCopyMergeGray].call(image_ptr, other.image_ptr,
748
+ dst_x, dst_y, src_x, src_y, w, h, pct.to_percent.round)
749
+ self
750
+ end
751
+ end
752
+
753
+ #
754
+ # = Description
755
+ #
756
+ # TrueColor images represent pixel colors directly and have no palette
757
+ # limitations.
758
+ #
759
+ class Image::TrueColor < Image
760
+ def self.create_image_sym #:nodoc:
761
+ :gdImageCreateTrueColor
762
+ end
763
+
764
+ def self.palette_class #:nodoc:
765
+ Palette::TrueColor
766
+ end
767
+
768
+ def sharpen(pct) #:nodoc:
769
+ SYM[:gdImageSharpen].call(image_ptr, pct.to_percent.round)
770
+ self
771
+ end
772
+ end
773
+ end