/****************************************************************************
**
** Copyright (C) 1992-2006 Trolltech AS. All rights reserved.
**
** This file is part of the plugins of the Qt Toolkit.
**
** This file may be used under the terms of the GNU General Public
** License version 2.0 as published by the Free Software Foundation
** and appearing in the file LICENSE.GPL included in the packaging of
** this file.  Please review the following information to ensure GNU
** General Public Licensing requirements will be met:
** http://www.trolltech.com/products/qt/opensource.html
**
** If you are unsure which license is appropriate for your use, please
** review the following information:
** http://www.trolltech.com/products/qt/licensing.html or contact the
** sales department at sales@trolltech.com.
**
** This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE
** WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
**
****************************************************************************/

#include "qjpeghandler.h"

#include <qimage.h>
#include <qvariant.h>
#include <qvector.h>

#include <stdio.h>      // jpeglib needs this to be pre-included
#include <setjmp.h>

#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 <jpeglib.h>
#ifdef const
#  undef const          // remove crazy C hackery in jconfig.h
#endif
}


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)
{
}

#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
void scaleSize(int &reqW, int &reqH, int imgW, int imgH, Qt::AspectRatioMode mode)
{
    if (mode == Qt::IgnoreAspectRatio)
        return;
    int t1 = imgW * reqH;
    int t2 = reqW * imgH;
    if ((mode == Qt::KeepAspectRatio && (t1 > t2)) || (mode == Qt::KeepAspectRatioByExpanding && (t1 < t2)))
        reqH = t2 / imgW;
    else
        reqW = t1 / imgH;
}


static bool read_jpeg_image(QIODevice *device, QImage *outImage, const QByteArray &parameters)
{
    QImage image;

    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_start_decompress(&cinfo);

        QString params = parameters;
        params.simplified();
        int sWidth = 0, sHeight = 0;
        char sModeStr[1024] = "";
        Qt::AspectRatioMode sMode;

        if (params.contains("GetHeaderInformation")) {

            // Create QImage but don't read the pixels
            if (cinfo.output_components == 3 || cinfo.output_components == 4) {
                image = QImage(cinfo.output_width, cinfo.output_height, QImage::Format_RGB32);
            } else if (cinfo.output_components == 1) {
                image = QImage(cinfo.output_width, cinfo.output_height, QImage::Format_Indexed8);
            } else {
                // Unsupported format
            }


        } else if (params.contains("Scale")) {
#if defined(_MSC_VER) && _MSC_VER >= 1400
            sscanf_s(params.toLatin1().data(), "Scale(%i, %i, %1023s)",
                   &sWidth, &sHeight, sModeStr, sizeof(sModeStr));
#else
            sscanf(params.toLatin1().data(), "Scale(%i, %i, %1023s)",
                   &sWidth, &sHeight, sModeStr);
#endif

            QString sModeQStr(sModeStr);
            if (sModeQStr == "IgnoreAspectRatio") {
                sMode = Qt::IgnoreAspectRatio;
            } else if (sModeQStr == "KeepAspectRatio") {
                sMode = Qt::KeepAspectRatio;
            } else if (sModeQStr == "KeepAspectRatioByExpanding") {
                sMode = Qt::KeepAspectRatioByExpanding;
            } else {
                qDebug("read_jpeg_image: invalid aspect ratio mode \"%s\", see QImage::AspectRatioMode documentation", sModeStr);
                sMode = Qt::KeepAspectRatio;
            }

//            qDebug("Parameters ask to scale the image to %i x %i AspectRatioMode: %s", sWidth, sHeight, sModeStr);
            scaleSize(sWidth, sHeight, cinfo.output_width, cinfo.output_height, sMode);
//            qDebug("Scaling the jpeg to %i x %i", sWidth, sHeight, sModeStr);

            if (cinfo.output_components == 3 || cinfo.output_components == 4) {
                image = QImage(sWidth, sHeight, QImage::Format_RGB32);
            } else if (cinfo.output_components == 1) {
                image = QImage(sWidth, sHeight, QImage::Format_Indexed8);
                image.setNumColors(256);
                for (int i=0; i<256; i++)
                    image.setColor(i, qRgb(i,i,i));
            } else {
                // Unsupported format
            }
            if (image.isNull())
                return false;

            if (!image.isNull()) {
                QImage tmpImage(cinfo.output_width, 1, QImage::Format_RGB32);
                uchar* inData = tmpImage.bits();
                uchar* outData = image.bits();
                int out_bpl = image.bytesPerLine();
                while (cinfo.output_scanline < cinfo.output_height) {
                    int outputLine = sHeight * cinfo.output_scanline / cinfo.output_height;
                    (void) jpeg_read_scanlines(&cinfo, &inData, 1);
                    if (cinfo.output_components == 3) {
                        uchar *in = inData;
                        QRgb *out = (QRgb*)outData + outputLine * out_bpl;
                        for (uint i=0; i<cinfo.output_width; i++) {
// ### Only scaling down an image works, I don't think scaling up will work at the moment
// ### An idea I have to make this a smooth scale is to progressively add the pixel values up
// When scaling down, multiple values are being over drawn in to the output buffer.
// Instead, a weighting based on the distance the line or pixel is from the output pixel determines
// the weight of it when added to the output buffer. At present it is a non-smooth scale which is
// inefficently implemented, it still uncompresses all the jpeg, an optimization for progressive
// jpegs could be made if scaling by say 50% or some other special cases
                            out[sWidth * i / cinfo.output_width] = qRgb(in[0], in[1], in[2]);
                            in += 3;
                        }
                    } else {
// ### Need to test the case where the jpeg is grayscale, need some black and white jpegs to test
// this code. (also only scales down and probably won't scale to a larger size)
                        uchar *in = inData;
                        uchar *out = outData + outputLine*out_bpl;
                        for (uint i=0; i<cinfo.output_width; i++) {
                            out[sWidth * i / cinfo.output_width] = in[i];
                        }
                    }
                }
                (void) jpeg_finish_decompress(&cinfo);
            }

        } else {

            if (cinfo.output_components == 3 || cinfo.output_components == 4) {
                image = QImage(cinfo.output_width, cinfo.output_height, QImage::Format_RGB32);
            } else if (cinfo.output_components == 1) {
                image = QImage(cinfo.output_width, cinfo.output_height, QImage::Format_Indexed8);
                image.setNumColors(256);
                for (int i=0; i<256; i++)
                    image.setColor(i, qRgb(i,i,i));
            } else {
                // Unsupported format
            }
            if (image.isNull())
                return false;

            if (!image.isNull()) {
                uchar* data = image.bits();
                int bpl = image.bytesPerLine();
                while (cinfo.output_scanline < cinfo.output_height) {
                    uchar *d = data + cinfo.output_scanline * bpl;
                    (void) jpeg_read_scanlines(&cinfo,
                                               &d,
                                               1);
                }
                (void) jpeg_finish_decompress(&cinfo);

                if (cinfo.output_components == 3) {
                    // Expand 24->32 bpp.
                    for (uint j=0; j<cinfo.output_height; j++) {
                        uchar *in = image.scanLine(j) + cinfo.output_width * 3;
                        QRgb *out = (QRgb*)image.scanLine(j);

                        for (uint i=cinfo.output_width; i--;) {
                            in-=3;
                            out[i] = qRgb(in[0], in[1], in[2]);
                        }
                    }
                } else if (cinfo.out_color_space == JCS_CMYK) {
                    for (uint j = 0; j < cinfo.output_height; ++j) {
                        uchar *in = image.scanLine(j) + cinfo.output_width * 4;
                        QRgb *out = (QRgb*)image.scanLine(j);

                        for (uint i = cinfo.output_width; i--; ) {
                            in-=4;
                            int k = in[3];
                            out[i] = qRgb(k * in[0] / 255, k * in[1] / 255, k * in[2] / 255);
                        }
                    }
                }
                if (cinfo.density_unit == 1) {
                    image.setDotsPerMeterX(int(100. * cinfo.X_density / 2.54));
                    image.setDotsPerMeterY(int(100. * cinfo.Y_density / 2.54));
                } else if (cinfo.density_unit == 2) {
                    image.setDotsPerMeterX(int(100. * cinfo.X_density));
                    image.setDotsPerMeterY(int(100. * cinfo.Y_density));
                }
            }
        }

        *outImage = image;
    }

    jpeg_destroy_decompress(&cinfo);
    delete iod_src;
    return !image.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;
    QImage image = sourceImage;

    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)) {
        jpeg_create_compress(&cinfo);

        cinfo.dest = iod_dest;

        cinfo.image_width = image.width();
        cinfo.image_height = image.height();

        QVector<QRgb> cmap = image.colorTable();
        bool gray=false;
        switch (image.depth()) {
        case 1:
        case 8:
            gray = true;
            for (int i = image.numColors(); 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;
        case 32:
            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.depth()) {
            case 1:
                if (gray) {
                    uchar* data = image.scanLine(cinfo.next_scanline);
                    if (image.format() == QImage::Format_MonoLSB) {
                        for (int i=0; i<w; i++) {
                            bool bit = !!(*(data + (i >> 3)) & (1 << (i & 7)));
                            row[i] = qRed(cmap[bit]);
                        }
                    } else {
                        for (int i=0; i<w; i++) {
                            bool bit = !!(*(data + (i >> 3)) & (1 << (7 -(i & 7))));
                            row[i] = qRed(cmap[bit]);
                        }
                    }
                } else {
                    uchar* data = image.scanLine(cinfo.next_scanline);
                    if (image.format() == QImage::Format_MonoLSB) {
                        for (int i=0; i<w; i++) {
                            bool bit = !!(*(data + (i >> 3)) & (1 << (i & 7)));
                            *row++ = qRed(cmap[bit]);
                            *row++ = qGreen(cmap[bit]);
                            *row++ = qBlue(cmap[bit]);
                        }
                    } else {
                        for (int i=0; i<w; i++) {
                            bool bit = !!(*(data + (i >> 3)) & (1 << (7 -(i & 7))));
                            *row++ = qRed(cmap[bit]);
                            *row++ = qGreen(cmap[bit]);
                            *row++ = qBlue(cmap[bit]);
                        }
                    }
                }
                break;
            case 8:
                if (gray) {
                    uchar* pix = image.scanLine(cinfo.next_scanline);
                    for (int i=0; i<w; i++) {
                        *row = qRed(cmap[*pix]);
                        ++row; ++pix;
                    }
                } else {
                    uchar* pix = image.scanLine(cinfo.next_scanline);
                    for (int i=0; i<w; i++) {
                        *row++ = qRed(cmap[*pix]);
                        *row++ = qGreen(cmap[*pix]);
                        *row++ = qBlue(cmap[*pix]);
                        ++pix;
                    }
                }
                break;
            case 32: {
                QRgb* rgb = (QRgb*)image.scanLine(cinfo.next_scanline);
                for (int i=0; i<w; i++) {
                    *row++ = qRed(*rgb);
                    *row++ = qGreen(*rgb);
                    *row++ = qBlue(*rgb);
                    ++rgb;
                }
            }
            }
            jpeg_write_scanlines(&cinfo, row_pointer, 1);
        }

        jpeg_finish_compress(&cinfo);
        jpeg_destroy_compress(&cinfo);
        success = true;
    } else {
        jpeg_destroy_compress(&cinfo);
        success = false;
    }

    delete iod_dest;
    delete [] row_pointer[0];
    return success;
}

QJpegHandler::QJpegHandler()
{
    quality = 75;
}

bool QJpegHandler::canRead() const
{
    if (canRead(device())) {
        setFormat("jpeg");
        return true;
    }
    return false;
}

bool QJpegHandler::canRead(QIODevice *device)
{
    if (!device) {
        qWarning("QJpegHandler::canRead() called with no device");
        return false;
    }

    return device->peek(2) == "\xFF\xD8";
}

bool QJpegHandler::read(QImage *image)
{
    if (!canRead())
        return false;
    return read_jpeg_image(device(), image, parameters);
}

bool QJpegHandler::write(const QImage &image)
{
    return write_jpeg_image(image, device(), quality);
}

bool QJpegHandler::supportsOption(ImageOption option) const
{
    return option == Quality
        || option == Size;// || option == Parameters;
}

QVariant QJpegHandler::option(ImageOption option) const
{
    if (option == Quality) {
        return quality;
    } else if (option == Size) {
        if (canRead() && !device()->isSequential()) {
            qint64 pos = device()->pos();
            QImage image;
            read_jpeg_image(device(), &image, "GetHeaderInformation");
            device()->seek(pos);
            return image.size();
        }
    }
//    else if (option == Parameters)
//        return parameters;
    return QVariant();
}

void QJpegHandler::setOption(ImageOption option, const QVariant &value)
{
    if (option == Quality)
        quality = value.toInt();
//    else if (option == Parameters)
//        parameters = value.toByteArray();
}

QByteArray QJpegHandler::name() const
{
    return "jpeg";
}
