]> git.imager.perl.org - imager.git/blame_incremental - jpeg.c
cast 2 signed/unsigned char pointer conversions to prevent warnings
[imager.git] / jpeg.c
... / ...
CommitLineData
1/*
2=head1 NAME
3
4jpeg.c - implement saving and loading JPEG images
5
6=head1 SYNOPSIS
7
8 io_glue *ig;
9 if (!i_writejpeg_wiol(im, ig, quality)) {
10 .. error ..
11 }
12 im = i_readjpeg_wiol(ig, length, iptc_text, itlength);
13
14=head1 DESCRIPTION
15
16Reads and writes JPEG images
17
18=over
19
20=cut
21*/
22
23#include <stdio.h>
24#include <sys/stat.h>
25#ifndef _MSC_VER
26#include <unistd.h>
27#endif
28#include <setjmp.h>
29
30#include "iolayer.h"
31#include "imageri.h"
32#include "jpeglib.h"
33#include "jerror.h"
34#include <errno.h>
35#ifdef IMEXIF_ENABLE
36#include "imexif.h"
37#endif
38
39#define JPEG_APP13 0xED /* APP13 marker code */
40#define JPEG_APP1 (JPEG_APP0 + 1)
41#define JPGS 16384
42
43static unsigned char fake_eoi[]={(JOCTET) 0xFF,(JOCTET) JPEG_EOI};
44
45/* Bad design right here */
46
47static int tlength=0;
48static char **iptc_text=NULL;
49
50
51/* Source and Destination managers */
52
53
54typedef struct {
55 struct jpeg_source_mgr pub; /* public fields */
56 io_glue *data;
57 JOCTET *buffer; /* start of buffer */
58 int length; /* Do I need this? */
59 boolean start_of_file; /* have we gotten any data yet? */
60} wiol_source_mgr;
61
62typedef struct {
63 struct jpeg_destination_mgr pub; /* public fields */
64 io_glue *data;
65 JOCTET *buffer; /* start of buffer */
66 boolean start_of_file; /* have we gotten any data yet? */
67} wiol_destination_mgr;
68
69typedef wiol_source_mgr *wiol_src_ptr;
70typedef wiol_destination_mgr *wiol_dest_ptr;
71
72
73/*
74 * Methods for io manager objects
75 *
76 * Methods for source managers:
77 *
78 * init_source (j_decompress_ptr cinfo);
79 * skip_input_data (j_decompress_ptr cinfo, long num_bytes);
80 * fill_input_buffer (j_decompress_ptr cinfo);
81 * term_source (j_decompress_ptr cinfo);
82 */
83
84
85
86static void
87wiol_init_source (j_decompress_ptr cinfo) {
88 wiol_src_ptr src = (wiol_src_ptr) cinfo->src;
89
90 /* We reset the empty-input-file flag for each image, but we don't clear
91 * the input buffer. This is correct behavior for reading a series of
92 * images from one source.
93 */
94 src->start_of_file = TRUE;
95}
96
97
98
99static boolean
100wiol_fill_input_buffer(j_decompress_ptr cinfo) {
101 wiol_src_ptr src = (wiol_src_ptr) cinfo->src;
102 ssize_t nbytes; /* We assume that reads are "small" */
103
104 mm_log((1,"wiol_fill_input_buffer(cinfo 0x%p)\n", cinfo));
105
106 nbytes = src->data->readcb(src->data, src->buffer, JPGS);
107
108 if (nbytes <= 0) { /* Insert a fake EOI marker */
109 src->pub.next_input_byte = fake_eoi;
110 src->pub.bytes_in_buffer = 2;
111 } else {
112 src->pub.next_input_byte = src->buffer;
113 src->pub.bytes_in_buffer = nbytes;
114 }
115 src->start_of_file = FALSE;
116 return TRUE;
117}
118
119
120static void
121wiol_skip_input_data (j_decompress_ptr cinfo, long num_bytes) {
122 wiol_src_ptr src = (wiol_src_ptr) cinfo->src;
123
124 /* Just a dumb implementation for now. Could use fseek() except
125 * it doesn't work on pipes. Not clear that being smart is worth
126 * any trouble anyway --- large skips are infrequent.
127 */
128
129 if (num_bytes > 0) {
130 while (num_bytes > (long) src->pub.bytes_in_buffer) {
131 num_bytes -= (long) src->pub.bytes_in_buffer;
132 (void) wiol_fill_input_buffer(cinfo);
133 /* note we assume that fill_input_buffer will never return FALSE,
134 * so suspension need not be handled.
135 */
136 }
137 src->pub.next_input_byte += (size_t) num_bytes;
138 src->pub.bytes_in_buffer -= (size_t) num_bytes;
139 }
140}
141
142static void
143wiol_term_source (j_decompress_ptr cinfo) {
144 /* no work necessary here */
145 wiol_src_ptr src;
146 if (cinfo->src != NULL) {
147 src = (wiol_src_ptr) cinfo->src;
148 myfree(src->buffer);
149 }
150}
151
152
153/* Source manager Constructor */
154
155static void
156jpeg_wiol_src(j_decompress_ptr cinfo, io_glue *ig, int length) {
157 wiol_src_ptr src;
158
159 if (cinfo->src == NULL) { /* first time for this JPEG object? */
160 cinfo->src = (struct jpeg_source_mgr *) (*cinfo->mem->alloc_small)
161 ((j_common_ptr) cinfo, JPOOL_PERMANENT,sizeof(wiol_source_mgr));
162 src = (wiol_src_ptr) cinfo->src;
163 }
164
165 /* put the request method call in here later */
166 io_glue_commit_types(ig);
167
168 src = (wiol_src_ptr) cinfo->src;
169 src->data = ig;
170 src->buffer = mymalloc( JPGS );
171 src->length = length;
172
173 src->pub.init_source = wiol_init_source;
174 src->pub.fill_input_buffer = wiol_fill_input_buffer;
175 src->pub.skip_input_data = wiol_skip_input_data;
176 src->pub.resync_to_restart = jpeg_resync_to_restart; /* use default method */
177 src->pub.term_source = wiol_term_source;
178 src->pub.bytes_in_buffer = 0; /* forces fill_input_buffer on first read */
179 src->pub.next_input_byte = NULL; /* until buffer loaded */
180}
181
182
183
184
185/*
186 * Methods for destination managers:
187 *
188 * init_destination (j_compress_ptr cinfo);
189 * empty_output_buffer (j_compress_ptr cinfo);
190 * term_destination (j_compress_ptr cinfo);
191 *
192 */
193
194static void
195wiol_init_destination (j_compress_ptr cinfo) {
196 wiol_dest_ptr dest = (wiol_dest_ptr) cinfo->dest;
197
198 /* We reset the empty-input-file flag for each image, but we don't clear
199 * the input buffer. This is correct behavior for reading a series of
200 * images from one source.
201 */
202 dest->start_of_file = TRUE;
203}
204
205static boolean
206wiol_empty_output_buffer(j_compress_ptr cinfo) {
207 wiol_dest_ptr dest = (wiol_dest_ptr) cinfo->dest;
208 ssize_t rc;
209 /*
210 Previously this code was checking free_in_buffer to see how much
211 needed to be written. This does not follow the documentation:
212
213 "In typical applications, it should write out the
214 *entire* buffer (use the saved start address and buffer length;
215 ignore the current state of next_output_byte and free_in_buffer)."
216
217 ssize_t nbytes = JPGS - dest->pub.free_in_buffer;
218 */
219
220 mm_log((1,"wiol_empty_output_buffer(cinfo 0x%p)\n", cinfo));
221 rc = dest->data->writecb(dest->data, dest->buffer, JPGS);
222
223 if (rc != JPGS) { /* XXX: Should raise some jpeg error */
224 myfree(dest->buffer);
225 mm_log((1, "wiol_empty_output_buffer: Error: nbytes = %d != rc = %d\n", JPGS, rc));
226 ERREXIT(cinfo, JERR_FILE_WRITE);
227 }
228 dest->pub.free_in_buffer = JPGS;
229 dest->pub.next_output_byte = dest->buffer;
230 return TRUE;
231}
232
233static void
234wiol_term_destination (j_compress_ptr cinfo) {
235 wiol_dest_ptr dest = (wiol_dest_ptr) cinfo->dest;
236 size_t nbytes = JPGS - dest->pub.free_in_buffer;
237 /* yes, this needs to flush the buffer */
238 /* needs error handling */
239
240 if (dest->data->writecb(dest->data, dest->buffer, nbytes) != nbytes) {
241 myfree(dest->buffer);
242 ERREXIT(cinfo, JERR_FILE_WRITE);
243 }
244
245 if (dest != NULL) myfree(dest->buffer);
246}
247
248
249/* Destination manager Constructor */
250
251static void
252jpeg_wiol_dest(j_compress_ptr cinfo, io_glue *ig) {
253 wiol_dest_ptr dest;
254
255 if (cinfo->dest == NULL) { /* first time for this JPEG object? */
256 cinfo->dest =
257 (struct jpeg_destination_mgr *)
258 (*cinfo->mem->alloc_small)
259 ((j_common_ptr) cinfo, JPOOL_PERMANENT, sizeof(wiol_destination_mgr));
260 }
261
262 dest = (wiol_dest_ptr) cinfo->dest;
263 dest->data = ig;
264 dest->buffer = mymalloc( JPGS );
265
266 dest->pub.init_destination = wiol_init_destination;
267 dest->pub.empty_output_buffer = wiol_empty_output_buffer;
268 dest->pub.term_destination = wiol_term_destination;
269 dest->pub.free_in_buffer = JPGS;
270 dest->pub.next_output_byte = dest->buffer;
271}
272
273LOCAL(unsigned int)
274jpeg_getc (j_decompress_ptr cinfo)
275/* Read next byte */
276{
277 struct jpeg_source_mgr * datasrc = cinfo->src;
278
279 if (datasrc->bytes_in_buffer == 0) {
280 if (! (*datasrc->fill_input_buffer) (cinfo))
281 { fprintf(stderr,"Jpeglib: cant suspend.\n"); exit(3); }
282 /* ERREXIT(cinfo, JERR_CANT_SUSPEND);*/
283 }
284 datasrc->bytes_in_buffer--;
285 return GETJOCTET(*datasrc->next_input_byte++);
286}
287
288METHODDEF(boolean)
289APP13_handler (j_decompress_ptr cinfo) {
290 INT32 length;
291 unsigned int cnt=0;
292
293 length = jpeg_getc(cinfo) << 8;
294 length += jpeg_getc(cinfo);
295 length -= 2; /* discount the length word itself */
296
297 tlength=length;
298
299 if ( ((*iptc_text)=mymalloc(length)) == NULL ) return FALSE;
300 while (--length >= 0) (*iptc_text)[cnt++] = jpeg_getc(cinfo);
301
302 return TRUE;
303}
304
305METHODDEF(void)
306my_output_message (j_common_ptr cinfo) {
307 char buffer[JMSG_LENGTH_MAX];
308
309 /* Create the message */
310 (*cinfo->err->format_message) (cinfo, buffer);
311
312 i_push_error(0, buffer);
313
314 /* Send it to stderr, adding a newline */
315 mm_log((1, "%s\n", buffer));
316}
317
318struct my_error_mgr {
319 struct jpeg_error_mgr pub; /* "public" fields */
320 jmp_buf setjmp_buffer; /* for return to caller */
321};
322
323typedef struct my_error_mgr * my_error_ptr;
324
325/* Here's the routine that will replace the standard error_exit method */
326
327METHODDEF(void)
328my_error_exit (j_common_ptr cinfo) {
329 /* cinfo->err really points to a my_error_mgr struct, so coerce pointer */
330 my_error_ptr myerr = (my_error_ptr) cinfo->err;
331
332 /* Always display the message. */
333 /* We could postpone this until after returning, if we chose. */
334 (*cinfo->err->output_message) (cinfo);
335
336 /* Return control to the setjmp point */
337 longjmp(myerr->setjmp_buffer, 1);
338}
339
340static void
341transfer_cmyk_inverted(i_color *out, JSAMPARRAY in, int width) {
342 JSAMPROW inrow = *in;
343 while (width--) {
344 /* extract and convert to real CMYK */
345 /* horribly enough this is correct given cmyk values are inverted */
346 int c = *inrow++;
347 int m = *inrow++;
348 int y = *inrow++;
349 int k = *inrow++;
350 out->rgba.r = (c * k) / MAXJSAMPLE;
351 out->rgba.g = (m * k) / MAXJSAMPLE;
352 out->rgba.b = (y * k) / MAXJSAMPLE;
353 ++out;
354 }
355}
356
357static void
358transfer_rgb(i_color *out, JSAMPARRAY in, int width) {
359 JSAMPROW inrow = *in;
360 while (width--) {
361 out->rgba.r = *inrow++;
362 out->rgba.g = *inrow++;
363 out->rgba.b = *inrow++;
364 ++out;
365 }
366}
367
368static void
369transfer_gray(i_color *out, JSAMPARRAY in, int width) {
370 JSAMPROW inrow = *in;
371 while (width--) {
372 out->gray.gray_color = *inrow++;
373 ++out;
374 }
375}
376
377typedef void (*transfer_function_t)(i_color *out, JSAMPARRAY in, int width);
378
379/*
380=item i_readjpeg_wiol(data, length, iptc_itext, itlength)
381
382=cut
383*/
384i_img*
385i_readjpeg_wiol(io_glue *data, int length, char** iptc_itext, int *itlength) {
386 i_img *im;
387#ifdef IMEXIF_ENABLE
388 int seen_exif = 0;
389#endif
390 i_color *line_buffer = NULL;
391 struct jpeg_decompress_struct cinfo;
392 struct my_error_mgr jerr;
393 JSAMPARRAY buffer; /* Output row buffer */
394 int row_stride; /* physical row width in output buffer */
395 jpeg_saved_marker_ptr markerp;
396 transfer_function_t transfer_f;
397 int channels;
398
399 mm_log((1,"i_readjpeg_wiol(data 0x%p, length %d,iptc_itext 0x%p)\n", data, length, iptc_itext));
400
401 i_clear_error();
402
403 iptc_text = iptc_itext;
404 cinfo.err = jpeg_std_error(&jerr.pub);
405 jerr.pub.error_exit = my_error_exit;
406 jerr.pub.output_message = my_output_message;
407
408 /* Set error handler */
409 if (setjmp(jerr.setjmp_buffer)) {
410 jpeg_destroy_decompress(&cinfo);
411 *iptc_itext=NULL;
412 *itlength=0;
413 if (line_buffer)
414 myfree(line_buffer);
415 return NULL;
416 }
417
418 jpeg_create_decompress(&cinfo);
419 jpeg_set_marker_processor(&cinfo, JPEG_APP13, APP13_handler);
420 jpeg_save_markers(&cinfo, JPEG_APP1, 0xFFFF);
421 jpeg_save_markers(&cinfo, JPEG_COM, 0xFFFF);
422 jpeg_wiol_src(&cinfo, data, length);
423
424 (void) jpeg_read_header(&cinfo, TRUE);
425 (void) jpeg_start_decompress(&cinfo);
426
427 channels = cinfo.output_components;
428 switch (cinfo.out_color_space) {
429 case JCS_GRAYSCALE:
430 transfer_f = transfer_gray;
431 break;
432
433 case JCS_RGB:
434 transfer_f = transfer_rgb;
435 break;
436
437 case JCS_CMYK:
438 if (cinfo.output_components == 4) {
439 /* we treat the CMYK values as inverted, because that's what that
440 buggy photoshop does, and everyone has to follow the gorilla.
441
442 Is there any app that still produces correct CMYK JPEGs?
443 */
444 transfer_f = transfer_cmyk_inverted;
445 channels = 3;
446 }
447 else {
448 mm_log((1, "i_readjpeg: cmyk image with %d channels\n", cinfo.output_components));
449 i_push_errorf(0, "CMYK image with invalid components %d", cinfo.output_components);
450 wiol_term_source(&cinfo);
451 jpeg_destroy_decompress(&cinfo);
452 return NULL;
453 }
454 break;
455
456 default:
457 mm_log((1, "i_readjpeg: unknown color space %d\n", cinfo.out_color_space));
458 i_push_errorf(0, "Unknown color space %d", cinfo.out_color_space);
459 wiol_term_source(&cinfo);
460 jpeg_destroy_decompress(&cinfo);
461 return NULL;
462 }
463
464 if (!i_int_check_image_file_limits(cinfo.output_width, cinfo.output_height,
465 channels, sizeof(i_sample_t))) {
466 mm_log((1, "i_readjpeg: image size exceeds limits\n"));
467 wiol_term_source(&cinfo);
468 jpeg_destroy_decompress(&cinfo);
469 return NULL;
470 }
471
472 im=i_img_empty_ch(NULL, cinfo.output_width, cinfo.output_height, channels);
473 if (!im) {
474 wiol_term_source(&cinfo);
475 jpeg_destroy_decompress(&cinfo);
476 return NULL;
477 }
478 row_stride = cinfo.output_width * cinfo.output_components;
479 buffer = (*cinfo.mem->alloc_sarray) ((j_common_ptr) &cinfo, JPOOL_IMAGE, row_stride, 1);
480 line_buffer = mymalloc(sizeof(i_color) * cinfo.output_width);
481 while (cinfo.output_scanline < cinfo.output_height) {
482 (void) jpeg_read_scanlines(&cinfo, buffer, 1);
483 transfer_f(line_buffer, buffer, cinfo.output_width);
484 i_plin(im, 0, cinfo.output_width, cinfo.output_scanline-1, line_buffer);
485 }
486 myfree(line_buffer);
487
488 /* check for APP1 marker and save */
489 markerp = cinfo.marker_list;
490 while (markerp != NULL) {
491 if (markerp->marker == JPEG_COM) {
492 i_tags_add(&im->tags, "jpeg_comment", 0, (const char *)markerp->data,
493 markerp->data_length, 0);
494 }
495#ifdef IMEXIF_ENABLE
496 else if (markerp->marker == JPEG_APP1 && !seen_exif) {
497 seen_exif = i_int_decode_exif(im, markerp->data, markerp->data_length);
498 }
499#endif
500
501 markerp = markerp->next;
502 }
503
504 i_tags_addn(&im->tags, "jpeg_out_color_space", 0, cinfo.out_color_space);
505 i_tags_addn(&im->tags, "jpeg_color_space", 0, cinfo.jpeg_color_space);
506
507 if (cinfo.saw_JFIF_marker) {
508 double xres = cinfo.X_density;
509 double yres = cinfo.Y_density;
510
511 i_tags_addn(&im->tags, "jpeg_density_unit", 0, cinfo.density_unit);
512 switch (cinfo.density_unit) {
513 case 0: /* values are just the aspect ratio */
514 i_tags_addn(&im->tags, "i_aspect_only", 0, 1);
515 i_tags_add(&im->tags, "jpeg_density_unit_name", 0, "none", -1, 0);
516 break;
517
518 case 1: /* per inch */
519 i_tags_add(&im->tags, "jpeg_density_unit_name", 0, "inch", -1, 0);
520 break;
521
522 case 2: /* per cm */
523 i_tags_add(&im->tags, "jpeg_density_unit_name", 0, "centimeter", -1, 0);
524 xres *= 2.54;
525 yres *= 2.54;
526 break;
527 }
528 i_tags_set_float2(&im->tags, "i_xres", 0, xres, 6);
529 i_tags_set_float2(&im->tags, "i_yres", 0, yres, 6);
530 }
531
532 (void) jpeg_finish_decompress(&cinfo);
533 jpeg_destroy_decompress(&cinfo);
534 *itlength=tlength;
535
536 i_tags_add(&im->tags, "i_format", 0, "jpeg", 4, 0);
537
538 mm_log((1,"i_readjpeg_wiol -> (0x%x)\n",im));
539 return im;
540}
541
542/*
543=item i_writejpeg_wiol(im, ig, qfactor)
544
545=cut
546*/
547
548undef_int
549i_writejpeg_wiol(i_img *im, io_glue *ig, int qfactor) {
550 JSAMPLE *image_buffer;
551 int quality;
552 int got_xres, got_yres, aspect_only, resunit;
553 double xres, yres;
554 int comment_entry;
555
556 struct jpeg_compress_struct cinfo;
557 struct my_error_mgr jerr;
558
559 JSAMPROW row_pointer[1]; /* pointer to JSAMPLE row[s] */
560 int row_stride; /* physical row width in image buffer */
561 unsigned char * data = NULL;
562
563 mm_log((1,"i_writejpeg(im %p, ig %p, qfactor %d)\n", im, ig, qfactor));
564
565 i_clear_error();
566 io_glue_commit_types(ig);
567
568 if (!(im->channels==1 || im->channels==3)) {
569 i_push_error(0, "only 1 or 3 channels images can be saved as JPEG");
570 return 0;
571 }
572 quality = qfactor;
573
574 cinfo.err = jpeg_std_error(&jerr.pub);
575 jerr.pub.error_exit = my_error_exit;
576 jerr.pub.output_message = my_output_message;
577
578 jpeg_create_compress(&cinfo);
579
580 if (setjmp(jerr.setjmp_buffer)) {
581 jpeg_destroy_compress(&cinfo);
582 if (data)
583 myfree(data);
584 return 0;
585 }
586
587 jpeg_wiol_dest(&cinfo, ig);
588
589 cinfo.image_width = im -> xsize; /* image width and height, in pixels */
590 cinfo.image_height = im -> ysize;
591
592 if (im->channels==3) {
593 cinfo.input_components = 3; /* # of color components per pixel */
594 cinfo.in_color_space = JCS_RGB; /* colorspace of input image */
595 }
596
597 if (im->channels==1) {
598 cinfo.input_components = 1; /* # of color components per pixel */
599 cinfo.in_color_space = JCS_GRAYSCALE; /* colorspace of input image */
600 }
601
602 jpeg_set_defaults(&cinfo);
603 jpeg_set_quality(&cinfo, quality, TRUE); /* limit to baseline-JPEG values */
604
605 got_xres = i_tags_get_float(&im->tags, "i_xres", 0, &xres);
606 got_yres = i_tags_get_float(&im->tags, "i_yres", 0, &yres);
607 if (!i_tags_get_int(&im->tags, "i_aspect_only", 0,&aspect_only))
608 aspect_only = 0;
609 if (!i_tags_get_int(&im->tags, "jpeg_density_unit", 0, &resunit))
610 resunit = 1; /* per inch */
611 if (resunit < 0 || resunit > 2) /* default to inch if invalid */
612 resunit = 1;
613 if (got_xres || got_yres) {
614 if (!got_xres)
615 xres = yres;
616 else if (!got_yres)
617 yres = xres;
618 if (aspect_only)
619 resunit = 0; /* standard tags override format tags */
620 if (resunit == 2) {
621 /* convert to per cm */
622 xres /= 2.54;
623 yres /= 2.54;
624 }
625 cinfo.density_unit = resunit;
626 cinfo.X_density = (int)(xres + 0.5);
627 cinfo.Y_density = (int)(yres + 0.5);
628 }
629
630 jpeg_start_compress(&cinfo, TRUE);
631
632 if (i_tags_find(&im->tags, "jpeg_comment", 0, &comment_entry)) {
633 jpeg_write_marker(&cinfo, JPEG_COM,
634 (const JOCTET *)im->tags.tags[comment_entry].data,
635 im->tags.tags[comment_entry].size);
636 }
637
638 row_stride = im->xsize * im->channels; /* JSAMPLEs per row in image_buffer */
639
640 if (!im->virtual && im->type == i_direct_type && im->bits == i_8_bits) {
641 image_buffer=im->idata;
642
643 while (cinfo.next_scanline < cinfo.image_height) {
644 /* jpeg_write_scanlines expects an array of pointers to scanlines.
645 * Here the array is only one element long, but you could pass
646 * more than one scanline at a time if that's more convenient.
647 */
648 row_pointer[0] = & image_buffer[cinfo.next_scanline * row_stride];
649 (void) jpeg_write_scanlines(&cinfo, row_pointer, 1);
650 }
651 }
652 else {
653 data = mymalloc(im->xsize * im->channels);
654 if (data) {
655 while (cinfo.next_scanline < cinfo.image_height) {
656 /* jpeg_write_scanlines expects an array of pointers to scanlines.
657 * Here the array is only one element long, but you could pass
658 * more than one scanline at a time if that's more convenient.
659 */
660 i_gsamp(im, 0, im->xsize, cinfo.next_scanline, data,
661 NULL, im->channels);
662 row_pointer[0] = data;
663 (void) jpeg_write_scanlines(&cinfo, row_pointer, 1);
664 }
665 myfree(data);
666 }
667 else {
668 jpeg_destroy_compress(&cinfo);
669 i_push_error(0, "out of memory");
670 return 0; /* out of memory? */
671 }
672 }
673
674 /* Step 6: Finish compression */
675
676 jpeg_finish_compress(&cinfo);
677
678 jpeg_destroy_compress(&cinfo);
679
680 ig->closecb(ig);
681
682 return(1);
683}
684
685/*
686=back
687
688=head1 AUTHOR
689
690Arnar M. Hrafnkelsson, addi@umich.edu
691
692=head1 SEE ALSO
693
694Imager(3)
695
696=cut
697*/