/**************************************************************************** ** ** 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 #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]; const QBuffer *memDevice; 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) { my_jpeg_source_mgr* src = (my_jpeg_source_mgr*)cinfo->src; if (src->memDevice) { src->next_input_byte = (const JOCTET *)(src->memDevice->data().constData() + src->memDevice->pos()); src->bytes_in_buffer = (size_t)(src->memDevice->data().size() - src->memDevice->pos()); return true; } src->next_input_byte = src->buffer; int 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) { // Should not happen in case of memDevice 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()) { // read() isn't used for memDevice, so seek past everything that was used if (src->memDevice) src->device->seek(src->device->pos() + (src->memDevice->data().size() - src->memDevice->pos() - src->bytes_in_buffer)); else 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; memDevice = qobject_cast(device); bytes_in_buffer = 0; next_input_byte = buffer; } inline static bool read_jpeg_size(int &w, int &h, j_decompress_ptr cinfo) { (void) jpeg_calc_output_dimensions(cinfo); w = cinfo->output_width; h = cinfo->output_height; return true; } #define HIGH_QUALITY_THRESHOLD 50 inline static bool read_jpeg_format(QImage::Format &format, j_decompress_ptr cinfo) { bool 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; 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(QImage *outImage, QSize scaledSize, QRect scaledClipRect, QRect clipRect, int inQuality, j_decompress_ptr info, struct my_error_mgr* err ) { if (!setjmp(err->setjmp_buffer)) { // -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 ((info->image_width % scaledSize.width()) == 0 && (info->image_height % scaledSize.height()) == 0) { int x = scaledClipRect.x() * info->image_width / scaledSize.width(); int y = scaledClipRect.y() * info->image_height / scaledSize.height(); int width = (scaledClipRect.right() + 1) * info->image_width / scaledSize.width() - x; int height = (scaledClipRect.bottom() + 1) * info->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()) { info->scale_denom = qMin(info->image_width / scaledSize.width(), info->image_height / scaledSize.height()); } else { info->scale_denom = qMin(clipRect.width() / scaledSize.width(), clipRect.height() / scaledSize.height()); } if (info->scale_denom < 2) { info->scale_denom = 1; } else if (info->scale_denom < 4) { info->scale_denom = 2; } else if (info->scale_denom < 8) { info->scale_denom = 4; } else { info->scale_denom = 8; } info->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 (info->scale_denom > 1 && ((clipRect.x() % info->scale_denom) != 0 || (clipRect.y() % info->scale_denom) != 0 || (clipRect.width() % info->scale_denom) != 0 || (clipRect.height() % info->scale_denom) != 0)) { info->scale_denom /= 2; } } } // If high quality not required, use fast decompression if( quality < HIGH_QUALITY_THRESHOLD ) { info->dct_method = JDCT_IFAST; info->do_fancy_upsampling = FALSE; } (void) jpeg_calc_output_dimensions(info); // Determine the clip region to extract. QRect imageRect(0, 0, info->output_width, info->output_height); QRect clip; if (clipRect.isEmpty()) { clip = imageRect; } else if (info->scale_denom == info->scale_num) { 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(info->scale_denom), clipRect.y() / int(info->scale_denom), clipRect.width() / int(info->scale_denom), clipRect.height() / int(info->scale_denom)); clip = clip.intersected(imageRect); } // Allocate memory for the clipped QImage. if (!ensureValidImage(outImage, info, clip.size())) longjmp(err->setjmp_buffer, 1); // Avoid memcpy() overhead if grayscale with no clipping. bool quickGray = (info->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 = (info->mem->alloc_sarray) ((j_common_ptr)info, JPOOL_IMAGE, info->output_width * info->output_components, 1); (void) jpeg_start_decompress(info); while (info->output_scanline < info->output_height) { int y = int(info->output_scanline) - clip.y(); if (y >= clip.height()) break; // We've read the entire clip region, so abort. (void) jpeg_read_scanlines(info, rows, 1); if (y < 0) continue; // Haven't reached the starting line yet. if (info->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 (info->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 (info->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(info); while (info->output_scanline < info->output_height) { uchar *row = outImage->scanLine(info->output_scanline); (void) jpeg_read_scanlines(info, &row, 1); } } if (info->output_scanline == info->output_height) (void) jpeg_finish_decompress(info); if (info->density_unit == 1) { outImage->setDotsPerMeterX(int(100. * info->X_density / 2.54)); outImage->setDotsPerMeterY(int(100. * info->Y_density / 2.54)); } else if (info->density_unit == 2) { outImage->setDotsPerMeterX(int(100. * info->X_density)); outImage->setDotsPerMeterY(int(100. * info->Y_density)); } if (scaledSize.isValid() && scaledSize != clip.size()) { *outImage = outImage->scaled(scaledSize, Qt::IgnoreAspectRatio, quality >= HIGH_QUALITY_THRESHOLD ? Qt::SmoothTransformation : Qt::FastTransformation); } if (!scaledClipRect.isEmpty()) *outImage = outImage->copy(scaledClipRect); return !outImage->isNull(); } else return false; } 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 can_write_format(QImage::Format fmt) { switch (fmt) { case QImage::Format_Mono: case QImage::Format_MonoLSB: case QImage::Format_Indexed8: case QImage::Format_RGB888: case QImage::Format_RGB32: case QImage::Format_ARGB32: case QImage::Format_ARGB32_Premultiplied: return true; break; default: break; } return false; } static bool write_jpeg_image(const QImage &sourceImage, QIODevice *device, int sourceQuality) { bool success = false; const QImage image = can_write_format(sourceImage.format()) ? sourceImage : sourceImage.convertToFormat(QImage::Format_RGB888); 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; idevice()); if(state == ReadHeader) { bool success = read_jpeg_image(image, scaledSize, scaledClipRect, clipRect, quality, &info, &err); state = success ? Ready : Error; return success; } return false; } QJpegHandler::QJpegHandler() : d(new QJpegHandlerPrivate(this)) { } QJpegHandler::~QJpegHandler() { delete d; } bool QJpegHandler::canRead() const { if(d->state == QJpegHandlerPrivate::Ready && !canRead(device())) return false; if (d->state != QJpegHandlerPrivate::Error) { setFormat("jpeg"); return true; } return false; } bool QJpegHandler::canRead(QIODevice *device) { if (!device) { qWarning("QJpegHandler::canRead() called with no device"); return false; } char buffer[2]; if (device->peek(buffer, 2) != 2) return false; return uchar(buffer[0]) == 0xff && uchar(buffer[1]) == 0xd8; } bool QJpegHandler::read(QImage *image) { if (!canRead()) return false; return d->read(image); } bool QJpegHandler::write(const QImage &image) { return write_jpeg_image(image, device(), d->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 { switch(option) { case Quality: return d->quality; case ScaledSize: return d->scaledSize; case ScaledClipRect: return d->scaledClipRect; case ClipRect: return d->clipRect; case Size: d->readJpegHeader(device()); return d->size; case ImageFormat: d->readJpegHeader(device()); return d->format; default: return QVariant(); } } void QJpegHandler::setOption(ImageOption option, const QVariant &value) { switch(option) { case Quality: d->quality = value.toInt(); break; case ScaledSize: d->scaledSize = value.toSize(); break; case ScaledClipRect: d->scaledClipRect = value.toRect(); break; case ClipRect: d->clipRect = value.toRect(); break; default: break; } } QByteArray QJpegHandler::name() const { return "jpeg"; } QT_END_NAMESPACE