gd2 1.0

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