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.
- data/COPYING +340 -0
- data/COPYRIGHT +18 -0
- data/README +13 -0
- data/Rakefile +32 -0
- data/lib/gd2.rb +184 -0
- data/lib/gd2/canvas.rb +281 -0
- data/lib/gd2/color.rb +236 -0
- data/lib/gd2/font.rb +343 -0
- data/lib/gd2/image.rb +773 -0
- data/lib/gd2/palette.rb +253 -0
- data/test/dl.rb +11 -0
- data/test/image.rb +22 -0
- metadata +57 -0
data/lib/gd2/font.rb
ADDED
@@ -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('&', '&'), 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
|
data/lib/gd2/image.rb
ADDED
@@ -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
|