/* NSBitmapImageRep+JPEG.m Methods for reading jpeg images Copyright (C) 2003 Free Software Foundation, Inc. Written by: Stefan Kleine Stegemann Date: Nov 2003 This file is part of the GNUstep GUI Library. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "config.h" #include "NSBitmapImageRep+JPEG.h" #if HAVE_LIBJPEG #include #include #include #include #include "AppKit/NSGraphics.h" #include #if defined(__MINGW32__) /* Hack so that INT32 is not redefined in jmorecfg.h. MingW defines this as well in basetsd.h */ #ifndef XMD_H #define XMD_H #endif #endif #include #include /* ----------------------------------------------------------- The following functions are for interacting with the jpeg library ----------------------------------------------------------- */ /* A custom error manager for the jpeg library * that 'inherits' from libjpeg's standard * error manager. */ struct gs_jpeg_error_mgr { struct jpeg_error_mgr parent; /* marks where to return after an error (instead of simply exiting) */ jmp_buf setjmpBuffer; /* a pointer to the last error message, nil if no error occured. if present, string is autoreleased. */ NSString *error; }; typedef struct gs_jpeg_error_mgr *gs_jpeg_error_mgr_ptr; /* Print the last jpeg library error and returns * the control to the caller of the libary. * libjpegs default error handling would exit * after printing the error. */ static void gs_jpeg_error_exit(j_common_ptr cinfo) { gs_jpeg_error_mgr_ptr myerr = (gs_jpeg_error_mgr_ptr)cinfo->err; (*cinfo->err->output_message)(cinfo); /* jump back to the caller of the library */ longjmp(myerr->setjmpBuffer, 1); } /* Save the error message in error. */ static void gs_jpeg_output_message(j_common_ptr cinfo) { char msgBuffer[JMSG_LENGTH_MAX]; gs_jpeg_error_mgr_ptr myerr = (gs_jpeg_error_mgr_ptr)cinfo->err; (*cinfo->err->format_message)(cinfo, msgBuffer); myerr->error = [NSString stringWithCString: msgBuffer]; } /* Initialize our error manager */ static void gs_jpeg_error_mgr_init(gs_jpeg_error_mgr_ptr errMgr) { errMgr->error = nil; } /* ------------------------------------------------------------------*/ /* A custom data source manager that allows the jpeg library * to read it's data from memory. It 'inherits' from the * default jpeg source manager. */ typedef struct { struct jpeg_source_mgr parent; /* the data to be passed to the library functions */ const unsigned char *data; unsigned int length; } gs_jpeg_source_mgr; typedef gs_jpeg_source_mgr *gs_jpeg_source_ptr; static void gs_init_source(j_decompress_ptr cinfo) { /* nothing to do here (so far) */ } static boolean gs_fill_input_buffer(j_decompress_ptr cinfo) { gs_jpeg_source_ptr src = (gs_jpeg_source_ptr)cinfo->src; /* we make all data available at once */ if (src->length == 0) { ERREXIT(cinfo, JERR_INPUT_EMPTY); } src->parent.next_input_byte = src->data; src->parent.bytes_in_buffer = src->length; return TRUE; } static void gs_skip_input_data(j_decompress_ptr cinfo, long numBytes) { gs_jpeg_source_ptr src = (gs_jpeg_source_ptr)cinfo->src; if (numBytes > 0) { src->parent.next_input_byte += numBytes; src->parent.bytes_in_buffer -= numBytes; } } static void gs_term_source(j_decompress_ptr cinfo) { /* nothing to do here */ } /* Prepare a decompression object for input from memory. The * caller is responsible for providing and releasing the * data. After decompression is done, gs_jpeg_memory_src_destroy * has to be called. */ static void gs_jpeg_memory_src_create(j_decompress_ptr cinfo, NSData *data) { gs_jpeg_source_ptr src; cinfo->src = (struct jpeg_source_mgr *)malloc(sizeof(gs_jpeg_source_mgr)); src = (gs_jpeg_source_ptr)cinfo->src; src = (gs_jpeg_source_ptr) cinfo->src; src->parent.init_source = gs_init_source; src->parent.fill_input_buffer = gs_fill_input_buffer; src->parent.skip_input_data = gs_skip_input_data; src->parent.resync_to_restart = jpeg_resync_to_restart; /* use default */ src->parent.term_source = gs_term_source; src->parent.bytes_in_buffer = 0; /* forces fill_input_buffer on first read */ src->parent.next_input_byte = NULL; /* until buffer loaded */ src->data = (const unsigned char *)[data bytes]; src->length = [data length]; } /* Destroy the source manager of the jpeg decompression object. */ static void gs_jpeg_memory_src_destroy(j_decompress_ptr cinfo) { gs_jpeg_source_ptr src = (gs_jpeg_source_ptr)cinfo->src; free(src); // does not free the data cinfo->src = NULL; } /* ------------------------------------------------------------------*/ /* * A custom destination manager. */ typedef struct { struct jpeg_destination_mgr pub; // public fields unsigned char* buffer; unsigned char* data; NSData** finalData; int length; } gs_jpeg_destination_mgr; typedef gs_jpeg_destination_mgr * gs_jpeg_dest_ptr; /* Initialize destination. This is called by jpeg_start_compress() before any data is actually written. It must initialize next_output_byte and free_in_buffer. free_in_buffer must be initialized to a positive value. */ static void gs_init_destination (j_compress_ptr cinfo) { //NSLog (@"gs_init_destination"); gs_jpeg_dest_ptr dest = (gs_jpeg_dest_ptr) cinfo->dest; // allocate the output image int imageSize = cinfo->image_width * cinfo->image_height; dest->buffer = (void*) calloc ((imageSize * cinfo->input_components), sizeof(unsigned char)); dest->data = (void*) calloc ((imageSize * cinfo->input_components), sizeof(unsigned char)); dest->pub.next_output_byte = dest->buffer; dest->pub.free_in_buffer = imageSize * cinfo->input_components; dest->length = 0; } /* This is called whenever the buffer has filled (free_in_buffer reaches zero). In typical applications, it should write out the *entire* buffer (use the saved start address and buffer length; ignore the current state of next_output_byte and free_in_buffer). Then reset the pointer & count to the start of the buffer, and return TRUE indicating that the buffer has been dumped. free_in_buffer must be set to a positive value when TRUE is returned. A FALSE return should only be used when I/O suspension is desired (this operating mode is discussed in the next section). */ static boolean gs_empty_output_buffer (j_compress_ptr cinfo) { //NSLog (@"gs_empty_output_buffer..."); gs_jpeg_dest_ptr dest = (gs_jpeg_dest_ptr) cinfo->dest; //NSLog (@"length added (%d)", dest->length); int imageSize = cinfo->image_width * cinfo->image_height; int bufSize = imageSize * cinfo->input_components; int i; for (i = 0; i < bufSize; i++) { dest->data [dest->length + i] = dest->buffer [i]; } dest->length = dest->length + bufSize; dest->pub.next_output_byte = dest->buffer; dest->pub.free_in_buffer = imageSize * cinfo->input_components; return TRUE; } /* Terminate destination --- called by jpeg_finish_compress() after all data has been written. In most applications, this must flush any data remaining in the buffer. Use either next_output_byte or free_in_buffer to determine how much data is in the buffer. */ static void gs_term_destination (j_compress_ptr cinfo) { //NSLog (@"gs_term_destination"); gs_jpeg_dest_ptr dest = (gs_jpeg_dest_ptr) cinfo->dest; int imageSize = cinfo->image_width * cinfo->image_height; int bufSize = imageSize * cinfo->input_components; int i; for (i = 0; i < bufSize; i++) { dest->data [dest->length + i] = dest->buffer [i]; } dest->length = dest->length + bufSize; *dest->finalData = [[NSData alloc] initWithBytes: dest->data length: (dest->length) - dest->pub.free_in_buffer]; } static void gs_jpeg_memory_dest_create (j_compress_ptr cinfo, NSData** data) { gs_jpeg_dest_ptr dest; cinfo->dest = (struct jpeg_destination_mgr*) malloc (sizeof (gs_jpeg_destination_mgr)); dest = (gs_jpeg_dest_ptr) cinfo->dest; dest->pub.init_destination = gs_init_destination; dest->pub.empty_output_buffer = gs_empty_output_buffer; dest->pub.term_destination = gs_term_destination; dest->finalData = data; } static void gs_jpeg_memory_dest_destroy (j_compress_ptr cinfo) { gs_jpeg_dest_ptr dest = (gs_jpeg_dest_ptr) cinfo->dest; free (dest->buffer); free (dest->data); free (dest); cinfo->dest = NULL; } /* ----------------------------------------------------------- The jpeg loading part of NSBitmapImageRep ----------------------------------------------------------- */ @implementation NSBitmapImageRep (JPEGReading) /* Return YES if this looks like a JPEG. */ + (BOOL) _bitmapIsJPEG: (NSData *)imageData { struct jpeg_decompress_struct cinfo; struct gs_jpeg_error_mgr jerrMgr; /* Be sure imageData contains data */ if (![imageData length]) { return NO; } /* Establish the our custom error handler */ gs_jpeg_error_mgr_init(&jerrMgr); cinfo.err = jpeg_std_error(&jerrMgr.parent); jerrMgr.parent.error_exit = gs_jpeg_error_exit; jerrMgr.parent.output_message = gs_jpeg_output_message; // establish return context for error handling if (setjmp(jerrMgr.setjmpBuffer)) { gs_jpeg_memory_src_destroy(&cinfo); jpeg_destroy_decompress(&cinfo); return NO; } /* read the header to see if we have a jpeg */ jpeg_create_decompress(&cinfo); /* Establish our own data source manager */ gs_jpeg_memory_src_create(&cinfo, imageData); jpeg_read_header(&cinfo, TRUE); gs_jpeg_memory_src_destroy(&cinfo); jpeg_destroy_decompress(&cinfo); return YES; } /* Read the jpeg image. Assume it is from a jpeg file and imageData * is not nil. */ - (id) _initBitmapFromJPEG: (NSData *)imageData errorMessage: (NSString **)errorMsg { struct jpeg_decompress_struct cinfo; struct gs_jpeg_error_mgr jerrMgr; JDIMENSION sclcount, samplesPerRow, i, j, rowSize; JSAMPARRAY sclbuffer = NULL; unsigned char *imgbuffer = NULL; if (!(self = [super init])) return nil; /* Establish the our custom error handler */ gs_jpeg_error_mgr_init(&jerrMgr); cinfo.err = jpeg_std_error(&jerrMgr.parent); jerrMgr.parent.error_exit = gs_jpeg_error_exit; jerrMgr.parent.output_message = gs_jpeg_output_message; // establish return context for error handling if (setjmp(jerrMgr.setjmpBuffer)) { /* assign the description of possible occured error to errorMsg */ if (errorMsg) *errorMsg = (jerrMgr.error ? (id)jerrMgr.error : (id)nil); gs_jpeg_memory_src_destroy(&cinfo); jpeg_destroy_decompress(&cinfo); if (imgbuffer) { free(imgbuffer); } RELEASE(self); return nil; } /* jpeg-decompression */ jpeg_create_decompress(&cinfo); /* Establish our own data source manager */ gs_jpeg_memory_src_create(&cinfo, imageData); jpeg_read_header(&cinfo, TRUE); /* we use RGB as target color space */ cinfo.out_color_space = JCS_RGB; /* decompress */ jpeg_start_decompress(&cinfo); /* process the decompressed data */ samplesPerRow = cinfo.output_width * cinfo.output_components; rowSize = samplesPerRow * sizeof(unsigned char); NSAssert(sizeof(JSAMPLE) == sizeof(unsigned char), @"unexpected sample size"); sclbuffer = cinfo.mem->alloc_sarray((j_common_ptr)&cinfo, JPOOL_IMAGE, samplesPerRow, cinfo.rec_outbuf_height); /* sclbuffer is freed when cinfo is destroyed */ imgbuffer = NSZoneMalloc([self zone], cinfo.output_height * rowSize); if (!imgbuffer) { NSLog(@"NSBitmapImageRep+JPEG: failed to allocated image buffer"); RELEASE(self); return nil; } i = 0; while (cinfo.output_scanline < cinfo.output_height) { sclcount = jpeg_read_scanlines(&cinfo, sclbuffer, cinfo.rec_outbuf_height); for (j = 0; j < sclcount; j++) { // copy a row to the image buffer memcpy((imgbuffer + (i * rowSize)), *(sclbuffer + (j * rowSize)), rowSize); i++; } } /* done */ jpeg_finish_decompress(&cinfo); gs_jpeg_memory_src_destroy(&cinfo); jpeg_destroy_decompress(&cinfo); if (jerrMgr.parent.num_warnings) { NSLog(@"NSBitmapImageRep+JPEG: %d warnings during jpeg decompression, " @"image may be corrupted", jerrMgr.parent.num_warnings); } // create the imagerep //BITS_IN_JSAMPLE is defined by libjpeg [self initWithBitmapDataPlanes: &imgbuffer pixelsWide: cinfo.output_width pixelsHigh: cinfo.output_height bitsPerSample: BITS_IN_JSAMPLE samplesPerPixel: cinfo.output_components hasAlpha: (cinfo.output_components == 3 ? NO : YES) isPlanar: NO colorSpaceName: NSCalibratedRGBColorSpace bytesPerRow: rowSize bitsPerPixel: BITS_IN_JSAMPLE * cinfo.output_components]; _imageData = [[NSData alloc] initWithBytesNoCopy: imgbuffer length: (rowSize * cinfo.output_height)]; return self; } @end /* ----------------------------------------------------------- The jpeg writing part of NSBitmapImageRep ----------------------------------------------------------- */ @implementation NSBitmapImageRep (JPEGWriting) - (NSData*) representationUsingType: (NSBitmapImageFileType) storageType properties: (NSDictionary*) properties { NSData *ret; unsigned char *imageSource = [self bitmapData]; int sPP = [self samplesPerPixel]; int width = [self size].width; int height = [self size].height; int row_stride = width * sPP; int quality = 90; NSNumber *qualityNumber = nil; NSString *colorSpace = nil; struct jpeg_compress_struct cinfo; struct jpeg_error_mgr jerr; JSAMPROW row_pointer[1]; // pointer to a single row cinfo.err = jpeg_std_error (&jerr); jpeg_create_compress (&cinfo); // TODO: handles planar images if ([self isPlanar]) { NSLog (@"Planar Image, not handled yet !"); return nil; } // specify the destination for the compressed data.. gs_jpeg_memory_dest_create (&cinfo, &ret); // set parameters cinfo.image_width = width; cinfo.image_height = height; cinfo.input_components = sPP; // TODO: use the image infos to choose the proper color space //cinfo.in_color_space = JCS_GRAYSCALE; colorSpace = [self colorSpaceName]; if ([colorSpace isEqualToString: @"NSCalibratedRGBColorSpace"] || [colorSpace isEqualToString: @"NSDeviceRGBColorSpace"]) { cinfo.in_color_space = JCS_RGB; } else { NSLog (@"Image Color Space: %@ not handled yet !", colorSpace); return nil; } // set quality qualityNumber = [properties objectForKey: @"NSImageCompressionFactor"]; if (qualityNumber != nil) { quality = (int) ([qualityNumber floatValue] * 100); } jpeg_set_defaults (&cinfo); // compress the image jpeg_set_quality (&cinfo, quality, TRUE); jpeg_start_compress (&cinfo, TRUE); while (cinfo.next_scanline < cinfo.image_height) { int index = cinfo.next_scanline * row_stride; row_pointer[0] = &imageSource[index]; jpeg_write_scanlines (&cinfo, row_pointer, 1); } jpeg_finish_compress(&cinfo); gs_jpeg_memory_dest_destroy (&cinfo); jpeg_destroy_compress(&cinfo); return AUTORELEASE(ret); } @end #else /* !HAVE_LIBJPEG */ @implementation NSBitmapImageRep (JPEGReading) + (BOOL) _bitmapIsJPEG: (NSData *)imageData { return NO; } - (id) _initBitmapFromJPEG: (NSData *)imageData errorMessage: (NSString **)errorMsg { RELEASE(self); return nil; } @end #endif /* !HAVE_LIBJPEG */