/**************************************************************************** ** ** Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies). ** All rights reserved. ** Contact: Nokia Corporation (qt-info@nokia.com) ** ** This file is part of the plugins of the Qt Toolkit. ** ** $QT_BEGIN_LICENSE:LGPL$ ** No Commercial Usage ** This file contains pre-release code and may not be distributed. ** You may use this file in accordance with the terms and conditions ** contained in the Technology Preview License Agreement accompanying ** this package. ** ** GNU Lesser General Public License Usage ** Alternatively, this file may be used under the terms of the GNU Lesser ** General Public License version 2.1 as published by the Free Software ** Foundation and appearing in the file LICENSE.LGPL included in the ** packaging of this file. Please review the following information to ** ensure the GNU Lesser General Public License version 2.1 requirements ** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. ** ** In addition, as a special exception, Nokia gives you certain additional ** rights. These rights are described in the Nokia Qt LGPL Exception ** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. ** ** If you have questions regarding the use of this file, please contact ** Nokia at qt-info@nokia.com. ** ** ** ** ** ** ** ** ** $QT_END_LICENSE$ ** ****************************************************************************/ #include "qjpeghandler.h" #include #include #include #include // jpeglib needs this to be pre-included #include #ifdef FAR #undef FAR #endif // including jpeglib.h seems to be a little messy extern "C" { // mingw includes rpcndr.h but does not define boolean #if defined(Q_OS_WIN) && defined(Q_CC_GNU) # if defined(__RPCNDR_H__) && !defined(boolean) typedef unsigned char boolean; # define HAVE_BOOLEAN # endif #endif #define XMD_H // shut JPEGlib up #if defined(Q_OS_UNIXWARE) # define HAVE_BOOLEAN // libjpeg under Unixware seems to need this #endif #include #ifdef const # undef const // remove crazy C hackery in jconfig.h #endif } QT_BEGIN_NAMESPACE struct my_error_mgr : public jpeg_error_mgr { jmp_buf setjmp_buffer; }; #if defined(Q_C_CALLBACKS) extern "C" { #endif static void my_error_exit (j_common_ptr cinfo) { my_error_mgr* myerr = (my_error_mgr*) cinfo->err; char buffer[JMSG_LENGTH_MAX]; (*cinfo->err->format_message)(cinfo, buffer); qWarning("%s", buffer); longjmp(myerr->setjmp_buffer, 1); } #if defined(Q_C_CALLBACKS) } #endif static const int max_buf = 4096; struct my_jpeg_source_mgr : public jpeg_source_mgr { // Nothing dynamic - cannot rely on destruction over longjump QIODevice *device; JOCTET buffer[max_buf]; public: my_jpeg_source_mgr(QIODevice *device); }; #if defined(Q_C_CALLBACKS) extern "C" { #endif static void qt_init_source(j_decompress_ptr) { } static boolean qt_fill_input_buffer(j_decompress_ptr cinfo) { int num_read; my_jpeg_source_mgr* src = (my_jpeg_source_mgr*)cinfo->src; src->next_input_byte = src->buffer; num_read = src->device->read((char*)src->buffer, max_buf); if (num_read <= 0) { // Insert a fake EOI marker - as per jpeglib recommendation src->buffer[0] = (JOCTET) 0xFF; src->buffer[1] = (JOCTET) JPEG_EOI; src->bytes_in_buffer = 2; } else { src->bytes_in_buffer = num_read; } #if defined(Q_OS_UNIXWARE) return B_TRUE; #else return true; #endif } static void qt_skip_input_data(j_decompress_ptr cinfo, long num_bytes) { my_jpeg_source_mgr* src = (my_jpeg_source_mgr*)cinfo->src; // `dumb' implementation from jpeglib /* Just a dumb implementation for now. Could use fseek() except * it doesn't work on pipes. Not clear that being smart is worth * any trouble anyway --- large skips are infrequent. */ if (num_bytes > 0) { while (num_bytes > (long) src->bytes_in_buffer) { num_bytes -= (long) src->bytes_in_buffer; (void) qt_fill_input_buffer(cinfo); /* note we assume that qt_fill_input_buffer will never return false, * so suspension need not be handled. */ } src->next_input_byte += (size_t) num_bytes; src->bytes_in_buffer -= (size_t) num_bytes; } } static void qt_term_source(j_decompress_ptr cinfo) { my_jpeg_source_mgr* src = (my_jpeg_source_mgr*)cinfo->src; if (!src->device->isSequential()) src->device->seek(src->device->pos() - src->bytes_in_buffer); } #if defined(Q_C_CALLBACKS) } #endif inline my_jpeg_source_mgr::my_jpeg_source_mgr(QIODevice *device) { jpeg_source_mgr::init_source = qt_init_source; jpeg_source_mgr::fill_input_buffer = qt_fill_input_buffer; jpeg_source_mgr::skip_input_data = qt_skip_input_data; jpeg_source_mgr::resync_to_restart = jpeg_resync_to_restart; jpeg_source_mgr::term_source = qt_term_source; this->device = device; bytes_in_buffer = 0; next_input_byte = buffer; } static bool read_jpeg_size(QIODevice *device, int &w, int &h) { bool rt = false; struct jpeg_decompress_struct cinfo; struct my_jpeg_source_mgr *iod_src = new my_jpeg_source_mgr(device); struct my_error_mgr jerr; jpeg_create_decompress(&cinfo); cinfo.src = iod_src; cinfo.err = jpeg_std_error(&jerr); jerr.error_exit = my_error_exit; if (!setjmp(jerr.setjmp_buffer)) { #if defined(Q_OS_UNIXWARE) (void) jpeg_read_header(&cinfo, B_TRUE); #else (void) jpeg_read_header(&cinfo, true); #endif (void) jpeg_calc_output_dimensions(&cinfo); w = cinfo.output_width; h = cinfo.output_height; rt = true; } jpeg_destroy_decompress(&cinfo); delete iod_src; return rt; } #define HIGH_QUALITY_THRESHOLD 50 static bool read_jpeg_format(QIODevice *device, QImage::Format &format) { bool result = false; struct jpeg_decompress_struct cinfo; struct my_jpeg_source_mgr *iod_src = new my_jpeg_source_mgr(device); struct my_error_mgr jerr; jpeg_create_decompress(&cinfo); cinfo.src = iod_src; cinfo.err = jpeg_std_error(&jerr); jerr.error_exit = my_error_exit; if (!setjmp(jerr.setjmp_buffer)) { #if defined(Q_OS_UNIXWARE) (void) jpeg_read_header(&cinfo, B_TRUE); #else (void) jpeg_read_header(&cinfo, true); #endif // This does not allocate memory for the whole image // or such, so we are safe. (void) jpeg_start_decompress(&cinfo); result = true; switch (cinfo.output_components) { case 1: format = QImage::Format_Indexed8; break; case 3: case 4: format = QImage::Format_RGB32; break; default: result = false; break; } cinfo.output_scanline = cinfo.output_height; (void) jpeg_finish_decompress(&cinfo); } jpeg_destroy_decompress(&cinfo); delete iod_src; return result; } static bool ensureValidImage(QImage *dest, struct jpeg_decompress_struct *info, const QSize& size) { QImage::Format format; switch (info->output_components) { case 1: format = QImage::Format_Indexed8; break; case 3: case 4: format = QImage::Format_RGB32; break; default: return false; // unsupported format } if (dest->size() != size || dest->format() != format) { *dest = QImage(size, format); if (format == QImage::Format_Indexed8) { dest->setColorCount(256); for (int i = 0; i < 256; i++) dest->setColor(i, qRgb(i,i,i)); } } return !dest->isNull(); } static bool read_jpeg_image(QIODevice *device, QImage *outImage, QSize scaledSize, QRect scaledClipRect, QRect clipRect, int inQuality ) { struct jpeg_decompress_struct cinfo; struct my_jpeg_source_mgr *iod_src = new my_jpeg_source_mgr(device); struct my_error_mgr jerr; jpeg_create_decompress(&cinfo); cinfo.src = iod_src; cinfo.err = jpeg_std_error(&jerr); jerr.error_exit = my_error_exit; if (!setjmp(jerr.setjmp_buffer)) { #if defined(Q_OS_UNIXWARE) (void) jpeg_read_header(&cinfo, B_TRUE); #else (void) jpeg_read_header(&cinfo, true); #endif // -1 means default quality. int quality = inQuality; if (quality < 0) quality = 75; // If possible, merge the scaledClipRect into either scaledSize // or clipRect to avoid doing a separate scaled clipping pass. // Best results are achieved by clipping before scaling, not after. if (!scaledClipRect.isEmpty()) { if (scaledSize.isEmpty() && clipRect.isEmpty()) { // No clipping or scaling before final clip. clipRect = scaledClipRect; scaledClipRect = QRect(); } else if (scaledSize.isEmpty()) { // Clipping, but no scaling: combine the clip regions. scaledClipRect.translate(clipRect.topLeft()); clipRect = scaledClipRect.intersected(clipRect); scaledClipRect = QRect(); } else if (clipRect.isEmpty()) { // No clipping, but scaling: if we can map back to an // integer pixel boundary, then clip before scaling. if ((cinfo.image_width % scaledSize.width()) == 0 && (cinfo.image_height % scaledSize.height()) == 0) { int x = scaledClipRect.x() * cinfo.image_width / scaledSize.width(); int y = scaledClipRect.y() * cinfo.image_height / scaledSize.height(); int width = (scaledClipRect.right() + 1) * cinfo.image_width / scaledSize.width() - x; int height = (scaledClipRect.bottom() + 1) * cinfo.image_height / scaledSize.height() - y; clipRect = QRect(x, y, width, height); scaledSize = scaledClipRect.size(); scaledClipRect = QRect(); } } else { // Clipping and scaling: too difficult to figure out, // and not a likely use case, so do it the long way. } } // Determine the scale factor to pass to libjpeg for quick downscaling. if (!scaledSize.isEmpty()) { if (clipRect.isEmpty()) { cinfo.scale_denom = qMin(cinfo.image_width / scaledSize.width(), cinfo.image_height / scaledSize.height()); } else { cinfo.scale_denom = qMin(clipRect.width() / scaledSize.width(), clipRect.height() / scaledSize.height()); } if (cinfo.scale_denom < 2) { cinfo.scale_denom = 1; } else if (cinfo.scale_denom < 4) { cinfo.scale_denom = 2; } else if (cinfo.scale_denom < 8) { cinfo.scale_denom = 4; } else { cinfo.scale_denom = 8; } cinfo.scale_num = 1; if (!clipRect.isEmpty()) { // Correct the scale factor so that we clip accurately. // It is recommended that the clip rectangle be aligned // on an 8-pixel boundary for best performance. while (cinfo.scale_denom > 1 && ((clipRect.x() % cinfo.scale_denom) != 0 || (clipRect.y() % cinfo.scale_denom) != 0 || (clipRect.width() % cinfo.scale_denom) != 0 || (clipRect.height() % cinfo.scale_denom) != 0)) { cinfo.scale_denom /= 2; } } } // If high quality not required, use fast decompression if( quality < HIGH_QUALITY_THRESHOLD ) { cinfo.dct_method = JDCT_IFAST; cinfo.do_fancy_upsampling = FALSE; } (void) jpeg_calc_output_dimensions(&cinfo); // Determine the clip region to extract. QRect imageRect(0, 0, cinfo.output_width, cinfo.output_height); QRect clip; if (clipRect.isEmpty()) { clip = imageRect; } else if (cinfo.scale_denom == 1) { clip = clipRect.intersected(imageRect); } else { // The scale factor was corrected above to ensure that // we don't miss pixels when we scale the clip rectangle. clip = QRect(clipRect.x() / int(cinfo.scale_denom), clipRect.y() / int(cinfo.scale_denom), clipRect.width() / int(cinfo.scale_denom), clipRect.height() / int(cinfo.scale_denom)); clip = clip.intersected(imageRect); } // Allocate memory for the clipped QImage. if (!ensureValidImage(outImage, &cinfo, clip.size())) longjmp(jerr.setjmp_buffer, 1); // Avoid memcpy() overhead if grayscale with no clipping. bool quickGray = (cinfo.output_components == 1 && clip == imageRect); if (!quickGray) { // Ask the jpeg library to allocate a temporary row. // The library will automatically delete it for us later. // The libjpeg docs say we should do this before calling // jpeg_start_decompress(). We can't use "new" here // because we are inside the setjmp() block and an error // in the jpeg input stream would cause a memory leak. JSAMPARRAY rows = (cinfo.mem->alloc_sarray) ((j_common_ptr)&cinfo, JPOOL_IMAGE, cinfo.output_width * cinfo.output_components, 1); (void) jpeg_start_decompress(&cinfo); while (cinfo.output_scanline < cinfo.output_height) { int y = int(cinfo.output_scanline) - clip.y(); if (y >= clip.height()) break; // We've read the entire clip region, so abort. (void) jpeg_read_scanlines(&cinfo, rows, 1); if (y < 0) continue; // Haven't reached the starting line yet. if (cinfo.output_components == 3) { // Expand 24->32 bpp. uchar *in = rows[0] + clip.x() * 3; QRgb *out = (QRgb*)outImage->scanLine(y); for (int i = 0; i < clip.width(); ++i) { *out++ = qRgb(in[0], in[1], in[2]); in += 3; } } else if (cinfo.out_color_space == JCS_CMYK) { // Convert CMYK->RGB. uchar *in = rows[0] + clip.x() * 4; QRgb *out = (QRgb*)outImage->scanLine(y); for (int i = 0; i < clip.width(); ++i) { int k = in[3]; *out++ = qRgb(k * in[0] / 255, k * in[1] / 255, k * in[2] / 255); in += 4; } } else if (cinfo.output_components == 1) { // Grayscale. memcpy(outImage->scanLine(y), rows[0] + clip.x(), clip.width()); } } } else { // Load unclipped grayscale data directly into the QImage. (void) jpeg_start_decompress(&cinfo); while (cinfo.output_scanline < cinfo.output_height) { uchar *row = outImage->scanLine(cinfo.output_scanline); (void) jpeg_read_scanlines(&cinfo, &row, 1); } } if (cinfo.output_scanline == cinfo.output_height) (void) jpeg_finish_decompress(&cinfo); if (cinfo.density_unit == 1) { outImage->setDotsPerMeterX(int(100. * cinfo.X_density / 2.54)); outImage->setDotsPerMeterY(int(100. * cinfo.Y_density / 2.54)); } else if (cinfo.density_unit == 2) { outImage->setDotsPerMeterX(int(100. * cinfo.X_density)); outImage->setDotsPerMeterY(int(100. * cinfo.Y_density)); } if (scaledSize.isValid() && scaledSize != clip.size()) { *outImage = outImage->scaled(scaledSize, Qt::IgnoreAspectRatio, quality >= HIGH_QUALITY_THRESHOLD ? Qt::SmoothTransformation : Qt::FastTransformation); } } jpeg_destroy_decompress(&cinfo); delete iod_src; if (!scaledClipRect.isEmpty()) *outImage = outImage->copy(scaledClipRect); return !outImage->isNull(); } struct my_jpeg_destination_mgr : public jpeg_destination_mgr { // Nothing dynamic - cannot rely on destruction over longjump QIODevice *device; JOCTET buffer[max_buf]; public: my_jpeg_destination_mgr(QIODevice *); }; #if defined(Q_C_CALLBACKS) extern "C" { #endif static void qt_init_destination(j_compress_ptr) { } static boolean qt_empty_output_buffer(j_compress_ptr cinfo) { my_jpeg_destination_mgr* dest = (my_jpeg_destination_mgr*)cinfo->dest; int written = dest->device->write((char*)dest->buffer, max_buf); if (written == -1) (*cinfo->err->error_exit)((j_common_ptr)cinfo); dest->next_output_byte = dest->buffer; dest->free_in_buffer = max_buf; #if defined(Q_OS_UNIXWARE) return B_TRUE; #else return true; #endif } static void qt_term_destination(j_compress_ptr cinfo) { my_jpeg_destination_mgr* dest = (my_jpeg_destination_mgr*)cinfo->dest; qint64 n = max_buf - dest->free_in_buffer; qint64 written = dest->device->write((char*)dest->buffer, n); if (written == -1) (*cinfo->err->error_exit)((j_common_ptr)cinfo); } #if defined(Q_C_CALLBACKS) } #endif inline my_jpeg_destination_mgr::my_jpeg_destination_mgr(QIODevice *device) { jpeg_destination_mgr::init_destination = qt_init_destination; jpeg_destination_mgr::empty_output_buffer = qt_empty_output_buffer; jpeg_destination_mgr::term_destination = qt_term_destination; this->device = device; next_output_byte = buffer; free_in_buffer = max_buf; } static bool write_jpeg_image(const QImage &sourceImage, QIODevice *device, int sourceQuality) { bool success = false; const QImage image = sourceImage; const QVector cmap = image.colorTable(); struct jpeg_compress_struct cinfo; JSAMPROW row_pointer[1]; row_pointer[0] = 0; struct my_jpeg_destination_mgr *iod_dest = new my_jpeg_destination_mgr(device); struct my_error_mgr jerr; cinfo.err = jpeg_std_error(&jerr); jerr.error_exit = my_error_exit; if (!setjmp(jerr.setjmp_buffer)) { // WARNING: // this if loop is inside a setjmp/longjmp branch // do not create C++ temporaries here because the destructor may never be called // if you allocate memory, make sure that you can free it (row_pointer[0]) jpeg_create_compress(&cinfo); cinfo.dest = iod_dest; cinfo.image_width = image.width(); cinfo.image_height = image.height(); bool gray=false; switch (image.format()) { case QImage::Format_Mono: case QImage::Format_MonoLSB: case QImage::Format_Indexed8: gray = true; for (int i = image.colorCount(); gray && i--;) { gray = gray & (qRed(cmap[i]) == qGreen(cmap[i]) && qRed(cmap[i]) == qBlue(cmap[i])); } cinfo.input_components = gray ? 1 : 3; cinfo.in_color_space = gray ? JCS_GRAYSCALE : JCS_RGB; break; default: cinfo.input_components = 3; cinfo.in_color_space = JCS_RGB; } jpeg_set_defaults(&cinfo); qreal diffInch = qAbs(image.dotsPerMeterX()*2.54/100. - qRound(image.dotsPerMeterX()*2.54/100.)) + qAbs(image.dotsPerMeterY()*2.54/100. - qRound(image.dotsPerMeterY()*2.54/100.)); qreal diffCm = (qAbs(image.dotsPerMeterX()/100. - qRound(image.dotsPerMeterX()/100.)) + qAbs(image.dotsPerMeterY()/100. - qRound(image.dotsPerMeterY()/100.)))*2.54; if (diffInch < diffCm) { cinfo.density_unit = 1; // dots/inch cinfo.X_density = qRound(image.dotsPerMeterX()*2.54/100.); cinfo.Y_density = qRound(image.dotsPerMeterY()*2.54/100.); } else { cinfo.density_unit = 2; // dots/cm cinfo.X_density = (image.dotsPerMeterX()+50) / 100; cinfo.Y_density = (image.dotsPerMeterY()+50) / 100; } int quality = sourceQuality >= 0 ? qMin(sourceQuality,100) : 75; #if defined(Q_OS_UNIXWARE) jpeg_set_quality(&cinfo, quality, B_TRUE /* limit to baseline-JPEG values */); jpeg_start_compress(&cinfo, B_TRUE); #else jpeg_set_quality(&cinfo, quality, true /* limit to baseline-JPEG values */); jpeg_start_compress(&cinfo, true); #endif row_pointer[0] = new uchar[cinfo.image_width*cinfo.input_components]; int w = cinfo.image_width; while (cinfo.next_scanline < cinfo.image_height) { uchar *row = row_pointer[0]; switch (image.format()) { case QImage::Format_Mono: case QImage::Format_MonoLSB: if (gray) { const uchar* data = image.scanLine(cinfo.next_scanline); if (image.format() == QImage::Format_MonoLSB) { for (int i=0; i> 3)) & (1 << (i & 7))); row[i] = qRed(cmap[bit]); } } else { for (int i=0; i> 3)) & (1 << (7 -(i & 7)))); row[i] = qRed(cmap[bit]); } } } else { const uchar* data = image.scanLine(cinfo.next_scanline); if (image.format() == QImage::Format_MonoLSB) { for (int i=0; i> 3)) & (1 << (i & 7))); *row++ = qRed(cmap[bit]); *row++ = qGreen(cmap[bit]); *row++ = qBlue(cmap[bit]); } } else { for (int i=0; i> 3)) & (1 << (7 -(i & 7)))); *row++ = qRed(cmap[bit]); *row++ = qGreen(cmap[bit]); *row++ = qBlue(cmap[bit]); } } } break; case QImage::Format_Indexed8: if (gray) { const uchar* pix = image.scanLine(cinfo.next_scanline); for (int i=0; ipeek(2) == "\xFF\xD8"; } bool QJpegHandler::read(QImage *image) { if (!canRead()) return false; return read_jpeg_image(device(), image, scaledSize, scaledClipRect, clipRect, quality); } bool QJpegHandler::write(const QImage &image) { return write_jpeg_image(image, device(), quality); } bool QJpegHandler::supportsOption(ImageOption option) const { return option == Quality || option == ScaledSize || option == ScaledClipRect || option == ClipRect || option == Size || option == ImageFormat; } QVariant QJpegHandler::option(ImageOption option) const { if (option == Quality) { return quality; } else if (option == ScaledSize) { return scaledSize; } else if (option == ScaledClipRect) { return scaledClipRect; } else if (option == ClipRect) { return clipRect; } else if (option == Size) { if (canRead() && !device()->isSequential()) { qint64 pos = device()->pos(); int width = 0; int height = 0; read_jpeg_size(device(), width, height); device()->seek(pos); return QSize(width, height); } } else if (option == ImageFormat) { if (canRead() && !device()->isSequential()) { qint64 pos = device()->pos(); QImage::Format format = QImage::Format_Invalid; read_jpeg_format(device(), format); device()->seek(pos); return format; } return QImage::Format_Invalid; } return QVariant(); } void QJpegHandler::setOption(ImageOption option, const QVariant &value) { if (option == Quality) quality = value.toInt(); else if ( option == ScaledSize ) scaledSize = value.toSize(); else if ( option == ScaledClipRect ) scaledClipRect = value.toRect(); else if ( option == ClipRect ) clipRect = value.toRect(); } QByteArray QJpegHandler::name() const { return "jpeg"; } QT_END_NAMESPACE