]>
Commit | Line | Data |
---|---|---|
3e1be2c1 AMH |
1 | =head1 NAME |
2 | ||
3 | Imager::Engines - Programmable transformation operations | |
4 | ||
5 | =head1 SYNOPSIS | |
6 | ||
7 | use Imager; | |
8 | ||
9 | my %opts; | |
10 | my @imgs; | |
11 | my $img; | |
12 | ... | |
13 | ||
14 | my $newimg = $img->transform( | |
15 | xexpr=>'x', | |
16 | yexpr=>'y+10*sin((x+y)/10)') | |
17 | or die $img->errstr; | |
18 | ||
19 | my $newimg = Imager::transform2(\%opts, @imgs) | |
20 | or die "transform2 failed: $Imager::ERRSTR"; | |
21 | ||
22 | my $newimg = $img->matrix_transform( | |
23 | matrix=>[ -1, 0, $img->getwidth-1, | |
24 | 0, 1, 0, | |
25 | 0, 0, 1 ]); | |
26 | ||
27 | ||
28 | =head1 DESCRIPTION | |
29 | ||
5715f7c3 | 30 | =head2 transform() |
3e1be2c1 AMH |
31 | |
32 | The C<transform()> function can be used to generate spatial warps and | |
33 | rotations and such effects. It only operates on a single image and | |
34 | its only function is to displace pixels. | |
35 | ||
36 | It can be given the operations in postfix notation or the module | |
37 | Affix::Infix2Postfix can be used to generate postfix code from infix | |
38 | code. Look in the test case t/t55trans.t for an example. | |
39 | ||
40 | C<transform()> needs expressions (or opcodes) that determine the | |
41 | source pixel for each target pixel. Source expressions are infix | |
42 | expressions using any of the +, -, *, / or ** binary operators, the - | |
5715f7c3 | 43 | unary operator, ( and ) for grouping and the C<sin()> and C<cos()> |
3e1be2c1 AMH |
44 | functions. The target pixel is input as the variables x and y. |
45 | ||
5715f7c3 | 46 | You specify the x and y expressions as C<xexpr> and C<yexpr> respectively. |
3e1be2c1 AMH |
47 | You can also specify opcodes directly, but that's magic deep enough |
48 | that you can look at the source code. | |
49 | ||
50 | Note: You can still use the transform() function, but the transform2() | |
51 | function is just as fast and is more likely to be enhanced and | |
52 | maintained. | |
53 | ||
77c06476 | 54 | $new_img=$img->transform(xexpr=>'x',yexpr=>'y+10*sin((x+y)/10)') |
3e1be2c1 | 55 | |
77c06476 TC |
56 | $new_img=$img->transform(xexpr=>'x+0.1*y+5*sin(y/10.0+1.57)', |
57 | yexpr=>'y+10*sin((x+y-0.785)/10)') | |
3e1be2c1 | 58 | |
5715f7c3 | 59 | =head2 transform2() |
3e1be2c1 AMH |
60 | |
61 | Imager also supports a C<transform2()> class method which allows you | |
62 | perform a more general set of operations, rather than just specifying | |
63 | a spatial transformation as with the transform() method, you can also | |
5715f7c3 | 64 | perform color transformations, image synthesis and image |
3e1be2c1 AMH |
65 | combinations from multiple source images. |
66 | ||
67 | C<transform2()> takes an reference to an options hash, and a list of | |
68 | images to operate one (this list may be empty): | |
69 | ||
70 | my %opts; | |
71 | my @imgs; | |
72 | ... | |
73 | my $img = Imager::transform2(\%opts, @imgs) | |
74 | or die "transform2 failed: $Imager::ERRSTR"; | |
75 | ||
76 | The options hash may define a transformation function, and optionally: | |
77 | ||
78 | =over | |
79 | ||
80 | =item * | |
81 | ||
82 | width - the width of the image in pixels. If this isn't supplied the | |
83 | width of the first input image is used. If there are no input images | |
84 | an error occurs. | |
85 | ||
86 | =item * | |
87 | ||
88 | height - the height of the image in pixels. If this isn't supplied | |
89 | the height of the first input image is used. If there are no input | |
90 | images an error occurs. | |
91 | ||
92 | =item * | |
93 | ||
94 | constants - a reference to hash of constants to define for the | |
95 | expression engine. Some extra constants are defined by Imager | |
96 | ||
e5744e01 TC |
97 | =item * |
98 | ||
99 | channels - the number of channels in the output image. If this isn't | |
100 | supplied a 3 channel image will be created. | |
101 | ||
3e1be2c1 AMH |
102 | =back |
103 | ||
5715f7c3 TC |
104 | The transformation function is specified using either the C<expr> or |
105 | C<rpnexpr> member of the options. | |
3e1be2c1 | 106 | |
77c06476 | 107 | =head3 Infix expressions |
3e1be2c1 | 108 | |
5715f7c3 | 109 | You can supply infix expressions to transform 2 with the C<expr> keyword. |
3e1be2c1 | 110 | |
77c06476 | 111 | $opts{expr} = 'return getp1(w-x, h-y)' |
3e1be2c1 AMH |
112 | |
113 | The 'expression' supplied follows this general grammar: | |
114 | ||
115 | ( identifier '=' expr ';' )* 'return' expr | |
116 | ||
117 | This allows you to simplify your expressions using variables. | |
118 | ||
119 | A more complex example might be: | |
120 | ||
77c06476 | 121 | $opts{expr} = 'pix = getp1(x,y); return if(value(pix)>0.8,pix*0.8,pix)' |
3e1be2c1 | 122 | |
77c06476 | 123 | Currently to use infix expressions you must have the L<Parse::RecDescent> |
3e1be2c1 AMH |
124 | module installed (available from CPAN). There is also what might be a |
125 | significant delay the first time you run the infix expression parser | |
126 | due to the compilation of the expression grammar. | |
127 | ||
77c06476 | 128 | =head3 Postfix expressions |
3e1be2c1 AMH |
129 | |
130 | You can supply postfix or reverse-polish notation expressions to | |
5715f7c3 | 131 | transform2() through the C<rpnexpr> keyword. |
3e1be2c1 | 132 | |
5715f7c3 | 133 | The parser for C<rpnexpr> emulates a stack machine, so operators will |
3e1be2c1 AMH |
134 | expect to see their parameters on top of the stack. A stack machine |
135 | isn't actually used during the image transformation itself. | |
136 | ||
137 | You can store the value at the top of the stack in a variable called | |
5715f7c3 | 138 | C<foo> using C<!foo> and retrieve that value again using @foo. The !foo |
3e1be2c1 AMH |
139 | notation will pop the value from the stack. |
140 | ||
141 | An example equivalent to the infix expression above: | |
142 | ||
143 | $opts{rpnexpr} = 'x y getp1 !pix @pix value 0.8 gt @pix 0.8 * @pix ifp' | |
144 | ||
333d7485 TC |
145 | At the end of the expression there should be a single pixel value left |
146 | on the stack, which is used as the output pixel. | |
147 | ||
77c06476 | 148 | =head3 Operators |
3e1be2c1 AMH |
149 | |
150 | transform2() has a fairly rich range of operators. | |
151 | ||
5715f7c3 | 152 | Each entry below includes the usage with C<rpnexpr>, formatted as: |
333d7485 TC |
153 | |
154 | =over | |
155 | ||
156 | I<operand> I<operand> ... B<I<operator>> -- I<result> | |
157 | ||
158 | =back | |
159 | ||
160 | If the operand or result begins with "N" it is a numeric value, if it | |
161 | begins with "C" it is a color or pixel value. | |
162 | ||
3e1be2c1 AMH |
163 | =over |
164 | ||
165 | =item +, *, -, /, %, ** | |
166 | ||
167 | multiplication, addition, subtraction, division, remainder and | |
168 | exponentiation. Multiplication, addition and subtraction can be used | |
5715f7c3 TC |
169 | on color values too - though you need to be careful - adding 2 white |
170 | values together and multiplying by 0.5 will give you gray, not white. | |
3e1be2c1 AMH |
171 | |
172 | Division by zero (or a small number) just results in a large number. | |
3799c4d1 TC |
173 | Modulo zero (or a small number) results in zero. % is implemented |
174 | using fmod() so you can use this to take a value mod a floating point | |
175 | value. | |
3e1be2c1 | 176 | |
5715f7c3 TC |
177 | =for stopwords N1 N2 N uminus |
178 | ||
179 | C<rpnexpr> usage: | |
333d7485 TC |
180 | |
181 | =over | |
182 | ||
183 | I<N1> I<N2> B<+> -- I<N> | |
184 | ||
185 | I<N1> I<N2> B<*> -- I<N> | |
186 | ||
187 | I<N1> I<N2> B<-> -- I<N> | |
188 | ||
189 | I<N1> I<N2> B</> -- I<N> | |
190 | ||
191 | I<N1> I<N2> B<**> -- I<N> | |
192 | ||
193 | I<N1> B<uminus> -- I<N> | |
194 | ||
195 | =back | |
196 | ||
3e1be2c1 AMH |
197 | =item sin(N), cos(N), atan2(y,x) |
198 | ||
199 | Some basic trig functions. They work in radians, so you can't just | |
200 | use the hue values. | |
201 | ||
5715f7c3 TC |
202 | =for stopwords Ny Nx atan2 |
203 | ||
204 | C<rpnexpr> usage: | |
333d7485 TC |
205 | |
206 | =over | |
207 | ||
208 | I<N> B<sin> -- I<N> | |
209 | ||
210 | I<N> B<cos> -- I<N> | |
211 | ||
212 | I<Ny> I<Nx> B<atan2> -- I<N> | |
213 | ||
214 | =back | |
215 | ||
3e1be2c1 AMH |
216 | =item distance(x1, y1, x2, y2) |
217 | ||
218 | Find the distance between two points. This is handy (along with | |
219 | atan2()) for producing circular effects. | |
220 | ||
5715f7c3 TC |
221 | =for stopwords Nx1 Ny1 Nx2 Ny2 |
222 | ||
223 | C<rpnexpr> usage: | |
333d7485 TC |
224 | |
225 | =over | |
226 | ||
227 | I<Nx1> I<Ny1> I<Nx2> I<Ny2> B<distance> -- I<N> | |
228 | ||
229 | =back | |
230 | ||
3e1be2c1 AMH |
231 | =item sqrt(n) |
232 | ||
233 | Find the square root. I haven't had much use for this since adding | |
234 | the distance() function. | |
235 | ||
5715f7c3 | 236 | C<rpnexpr> usage: |
333d7485 TC |
237 | |
238 | =over | |
239 | ||
240 | I<N> B<sqrt> -- I<N> | |
241 | ||
242 | =back | |
243 | ||
3e1be2c1 AMH |
244 | =item abs(n) |
245 | ||
246 | Find the absolute value. | |
247 | ||
5715f7c3 | 248 | C<rpnexpr> usage: |
333d7485 TC |
249 | |
250 | =over | |
251 | ||
252 | I<N> B<abs> -- I<N> | |
253 | ||
254 | =back | |
255 | ||
3e1be2c1 AMH |
256 | =item getp1(x,y), getp2(x,y), getp3(x, y) |
257 | ||
258 | Get the pixel at position (x,y) from the first, second or third image | |
259 | respectively. I may add a getpn() function at some point, but this | |
260 | prevents static checking of the instructions against the number of | |
261 | images actually passed in. | |
262 | ||
5715f7c3 TC |
263 | =for stopwords getp1 getp2 getp3 |
264 | ||
265 | C<rpnexpr> usage: | |
333d7485 TC |
266 | |
267 | =over | |
268 | ||
269 | I<Nx> I<Ny> B<getp1> -- I<C> | |
270 | ||
271 | I<Nx> I<Ny> B<getp2> -- I<C> | |
272 | ||
273 | I<Nx> I<Ny> B<getp3> -- I<C> | |
274 | ||
275 | =back | |
276 | ||
e5744e01 | 277 | =item value(c), hue(c), sat(c), hsv(h,s,v), hsva(h,s,v,alpha) |
3e1be2c1 | 278 | |
5715f7c3 | 279 | Separates a color value into it's value (brightness), hue (color) |
3e1be2c1 | 280 | and saturation elements. Use hsv() to put them back together (after |
5715f7c3 | 281 | suitable manipulation), or hsva() to include a transparency value. |
3e1be2c1 | 282 | |
5715f7c3 TC |
283 | =for stopwords Nh Ns Nv hsv hsva Nr Ng Nb rgb rgba |
284 | ||
285 | C<rpnexpr> usage: | |
333d7485 TC |
286 | |
287 | =over | |
288 | ||
289 | I<C> B<value> -- I<N> | |
290 | ||
291 | I<C> B<hue> -- I<N> | |
292 | ||
293 | I<C> B<sat> -- I<N> | |
294 | ||
295 | I<Nh> I<Ns> I<Nv> B<hsv> -- I<C> | |
296 | ||
297 | I<Nh> I<Ns> I<Nv> I<Na> B<hsva> -- I<C> | |
298 | ||
299 | =back | |
300 | ||
301 | =item red(c), green(c), blue(c), rgb(r,g,b), rgba(r,g,b,a) | |
3e1be2c1 | 302 | |
5715f7c3 | 303 | Separates a color value into it's red, green and blue colors. Use |
e5744e01 TC |
304 | rgb(r,g,b) to put it back together, or rgba() to include a |
305 | transparency value. | |
306 | ||
5715f7c3 | 307 | C<rpnexpr> usage: |
333d7485 TC |
308 | |
309 | =over | |
310 | ||
311 | I<C> B<red> -- I<N> | |
312 | ||
313 | I<C> B<green> -- I<N> | |
314 | ||
315 | I<C> B<blue> -- I<N> | |
316 | ||
317 | I<Nr> I<Ng> I<Nb> B<rgb> -- I<C> | |
318 | ||
319 | I<Nr> I<Ng> I<Nb> I<Na> B<rgba> -- I<C> | |
320 | ||
321 | =back | |
322 | ||
e5744e01 TC |
323 | =item alpha(c) |
324 | ||
5715f7c3 | 325 | Retrieve the alpha value from a color. |
3e1be2c1 | 326 | |
5715f7c3 | 327 | C<rpnexpr> usage: |
333d7485 TC |
328 | |
329 | =over | |
330 | ||
331 | I<C> B<alpha> -- I<N> | |
332 | ||
333 | =back | |
334 | ||
3e1be2c1 AMH |
335 | =item int(n) |
336 | ||
337 | Convert a value to an integer. Uses a C int cast, so it may break on | |
338 | large values. | |
339 | ||
5715f7c3 | 340 | C<rpnexpr> usage: |
333d7485 TC |
341 | |
342 | =over | |
343 | ||
344 | I<N> B<int> -- I<N> | |
345 | ||
346 | =back | |
347 | ||
3e1be2c1 AMH |
348 | =item if(cond,ntrue,nfalse), if(cond,ctrue,cfalse) |
349 | ||
350 | A simple (and inefficient) if function. | |
351 | ||
5715f7c3 TC |
352 | =for stopwords Ncond ifp |
353 | ||
354 | C<rpnexpr> usage: | |
333d7485 TC |
355 | |
356 | =over | |
357 | ||
358 | I<Ncond> I<N-true-result> I<N-false-result> B<if> -- I<N> | |
359 | ||
360 | I<Ncond> I<C-true-result> I<C-false-result> B<if> -- I<C> | |
361 | ||
362 | I<Ncond> I<C-true-result> I<C-false-result> B<ifp> -- I<C> | |
363 | ||
364 | =back | |
365 | ||
3e1be2c1 AMH |
366 | =item <=,<,==,>=,>,!= |
367 | ||
368 | Relational operators (typically used with if()). Since we're working | |
369 | with floating point values the equalities are 'near equalities' - an | |
370 | epsilon value is used. | |
371 | ||
333d7485 TC |
372 | =over |
373 | ||
374 | I<N1> I<N2> B<< <= >> -- I<N> | |
375 | ||
376 | I<N1> I<N2> B<< < >> -- I<N> | |
377 | ||
378 | I<N1> I<N2> B<< >= >> -- I<N> | |
379 | ||
380 | I<N1> I<N2> B<< > >> -- I<N> | |
381 | ||
382 | I<N1> I<N2> B<< == >> -- I<N> | |
383 | ||
384 | I<N1> I<N2> B<< != >> -- I<N> | |
385 | ||
386 | =back | |
387 | ||
3e1be2c1 AMH |
388 | =item &&, ||, not(n) |
389 | ||
390 | Basic logical operators. | |
391 | ||
5715f7c3 | 392 | C<rpnexpr> usage: |
333d7485 TC |
393 | |
394 | =over | |
395 | ||
396 | I<N1> I<N2> B<and> -- I<N> | |
397 | ||
398 | I<N1> I<N2> B<or> -- I<N> | |
399 | ||
400 | I<N> B<not> -- I<N> | |
401 | ||
402 | =back | |
403 | ||
042cdaea TC |
404 | =item log(n), exp(n) |
405 | ||
406 | Natural logarithm and exponential. | |
407 | ||
5715f7c3 | 408 | C<rpnexpr> usage: |
333d7485 TC |
409 | |
410 | =over | |
411 | ||
412 | I<N> B<log> -- I<N> | |
413 | ||
414 | I<N> B<exp> -- I<N> | |
415 | ||
416 | =back | |
417 | ||
3309187a TC |
418 | =item det(a, b, c, d) |
419 | ||
420 | Calculate the determinant of the 2 x 2 matrix; | |
421 | ||
422 | a b | |
423 | c d | |
424 | ||
5715f7c3 TC |
425 | =for stopwords Na Nv Nc Nd det |
426 | ||
427 | C<rpnexpr> usage: | |
3309187a TC |
428 | |
429 | =over | |
430 | ||
431 | I<Na> I<Nb> I<Nc> I<Nd> B<det> -- I<N> | |
432 | ||
433 | =back | |
434 | ||
042cdaea TC |
435 | =back |
436 | ||
dbb1064f | 437 | =head3 Constants |
042cdaea TC |
438 | |
439 | transform2() defines the following constants: | |
440 | ||
441 | =over | |
442 | ||
5715f7c3 | 443 | =item C<pi> |
042cdaea TC |
444 | |
445 | The classical constant. | |
446 | ||
5715f7c3 | 447 | =item C<w> |
042cdaea | 448 | |
5715f7c3 | 449 | =item C<h> |
042cdaea TC |
450 | |
451 | The width and height of the output image. | |
452 | ||
5715f7c3 | 453 | =item C<cx> |
042cdaea | 454 | |
5715f7c3 | 455 | =item C<cy> |
042cdaea TC |
456 | |
457 | The center of the output image. | |
458 | ||
5715f7c3 | 459 | =item C<w>I<image number> |
042cdaea | 460 | |
5715f7c3 | 461 | =item C<h>I<image number> |
042cdaea TC |
462 | |
463 | The width and height of each of the input images, C<w1> is the width | |
464 | of the first input image and so on. | |
465 | ||
5715f7c3 | 466 | =item C<cx>I<image number> |
042cdaea | 467 | |
5715f7c3 | 468 | =item C<cy>I<image number> |
042cdaea TC |
469 | |
470 | The center of each of the input images, (C<cx1>, C<cy1>) is the center | |
471 | of the first input image and so on. | |
472 | ||
3e1be2c1 AMH |
473 | =back |
474 | ||
475 | A few examples: | |
476 | ||
477 | =over | |
478 | ||
5715f7c3 | 479 | rpnexpr=>'x 25 % 15 * y 35 % 10 * getp1 !pat x y getp1 !pix @pix sat 0.7 gt @pat @pix ifp' |
3e1be2c1 AMH |
480 | |
481 | tiles a smaller version of the input image over itself where the | |
5715f7c3 | 482 | color has a saturation over 0.7. |
3e1be2c1 | 483 | |
5715f7c3 | 484 | rpnexpr=>'x 25 % 15 * y 35 % 10 * getp1 !pat y 360 / !rat x y getp1 1 @rat - pmult @pat @rat pmult padd' |
3e1be2c1 AMH |
485 | |
486 | tiles the input image over itself so that at the top of the image the | |
487 | full-size image is at full strength and at the bottom the tiling is | |
488 | most visible. | |
489 | ||
5715f7c3 | 490 | rpnexpr=>'x y getp1 !pix @pix value 0.96 gt @pix sat 0.1 lt and 128 128 255 rgb @pix ifp' |
3e1be2c1 AMH |
491 | |
492 | replace pixels that are white or almost white with a palish blue | |
493 | ||
5715f7c3 | 494 | rpnexpr=>'x 35 % 10 * y 45 % 8 * getp1 !pat x y getp1 !pix @pix sat 0.2 lt @pix value 0.9 gt and @pix @pat @pix value 2 / 0.5 + pmult ifp' |
3e1be2c1 | 495 | |
5715f7c3 | 496 | Tiles the input image over it self where the image isn't white or almost |
3e1be2c1 AMH |
497 | white. |
498 | ||
5715f7c3 | 499 | rpnexpr=>'x y 160 180 distance !d y 180 - x 160 - atan2 !a @d 10 / @a + 3.1416 2 * % !a2 @a2 180 * 3.1416 / 1 @a2 sin 1 + 2 / hsv' |
3e1be2c1 AMH |
500 | |
501 | Produces a spiral. | |
502 | ||
5715f7c3 | 503 | rpnexpr=>'x y 160 180 distance !d y 180 - x 160 - atan2 !a @d 10 / @a + 3.1416 2 * % !a2 @a 180 * 3.1416 / 1 @a2 sin 1 + 2 / hsv' |
3e1be2c1 | 504 | |
5715f7c3 | 505 | A spiral built on top of a color wheel. |
3e1be2c1 AMH |
506 | |
507 | =back | |
508 | ||
509 | For details on expression parsing see L<Imager::Expr>. For details on | |
510 | the virtual machine used to transform the images, see | |
67d441b2 | 511 | L<Imager::regmach>. |
3e1be2c1 | 512 | |
3799c4d1 TC |
513 | # generate a colorful spiral |
514 | # requires that Parse::RecDescent be installed | |
515 | my $newimg = Imager::transform2({ | |
516 | width => 160, height=>160, | |
517 | expr => <<EOS | |
518 | dist = distance(x, y, w/2, h/2); | |
519 | angle = atan2(y-h/2, x-w/2); | |
520 | angle2 = (dist / 10 + angle) % ( 2 * pi ); | |
521 | return hsv(angle*180/pi, 1, (sin(angle2)+1)/2); | |
522 | EOS | |
523 | }); | |
3e1be2c1 | 524 | |
e5744e01 TC |
525 | # replace green portions of an image with another image |
526 | my $newimg = Imager::transform2({ | |
527 | rpnexpr => <<EOS | |
528 | x y getp2 !pat # used to replace green portions | |
529 | x y getp1 !pix # source with "green screen" | |
530 | @pix red 10 lt @pix blue 10 lt && # low blue and red | |
531 | @pix green 254 gt && # and high green | |
532 | @pat @pix ifp | |
533 | EOS | |
534 | }, $source, $background); | |
535 | ||
3e1be2c1 AMH |
536 | =head2 Matrix Transformations |
537 | ||
58a9ba58 TC |
538 | =over |
539 | ||
67d441b2 | 540 | =item matrix_transform() |
58a9ba58 | 541 | |
3e1be2c1 AMH |
542 | Rather than having to write code in a little language, you can use a |
543 | matrix to perform affine transformations, using the matrix_transform() | |
544 | method: | |
545 | ||
546 | my $newimg = $img->matrix_transform(matrix=>[ -1, 0, $img->getwidth-1, | |
547 | 0, 1, 0, | |
548 | 0, 0, 1 ]); | |
549 | ||
550 | By default the output image will be the same size as the input image, | |
5715f7c3 TC |
551 | but you can supply the C<xsize> and C<ysize> parameters to change the |
552 | size. | |
3e1be2c1 AMH |
553 | |
554 | Rather than building matrices by hand you can use the Imager::Matrix2d | |
555 | module to build the matrices. This class has methods to allow you to | |
556 | scale, shear, rotate, translate and reflect, and you can combine these | |
557 | with an overloaded multiplication operator. | |
558 | ||
559 | WARNING: the matrix you provide in the matrix operator transforms the | |
560 | co-ordinates within the B<destination> image to the co-ordinates | |
561 | within the I<source> image. This can be confusing. | |
562 | ||
0d3b936e TC |
563 | You can also supply a C<back> argument which acts as a background |
564 | color for the areas of the image with no samples available (outside | |
565 | the rectangle of the source image.) This can be either an | |
566 | Imager::Color or Imager::Color::Float object. This is B<not> mixed | |
567 | transparent pixels in the middle of the source image, it is B<only> | |
568 | used for pixels where there is no corresponding pixel in the source | |
569 | image. | |
3e1be2c1 | 570 | |
58a9ba58 TC |
571 | =back |
572 | ||
8ba1b8a6 TC |
573 | =head1 AUTHOR |
574 | ||
575 | Tony Cook <tonyc@cpan.org>, Arnar M. Hrafnkelsson | |
576 | ||
3e1be2c1 | 577 | =cut |