/* ============================================================
 *
 * This file is a part of digiKam project
 * https://www.digikam.org
 *
 * Date        : 2004-09-29
 * Description : helper methods for JPEG image format.
 *
 * SPDX-FileCopyrightText: 2004-2005 by Renchi Raju <renchi dot raju at gmail dot com>
 * SPDX-FileCopyrightText: 2006-2026 by Gilles Caulier <caulier dot gilles at gmail dot com>
 * SPDX-FileCopyrightText: 2006-2012 by Marcel Wiesweg <marcel dot wiesweg at gmx dot de>
 *
 * Parts of the loading code is taken from qjpeghandler.cpp
 * SPDX-FileCopyrightText: 2011 Nokia Corporation and/or its subsidiary(-ies).
 * All rights reserved.
 * Contact: Nokia Corporation (qt-info@nokia.com)
 *
 * SPDX-License-Identifier: GPL-2.0-or-later
 *
 * ============================================================ */

#define XMD_H

#include "jpegutils.h"

// C++ includes

#include <cstdio>
#include <cstdlib>

// C ANSI includes

extern "C"
{
#include <sys/types.h>
#include <jpeglib.h>
}

// Pragma directives to reduce warnings from libjpeg transupp header file.
#if !defined(Q_OS_DARWIN) && defined(Q_CC_GNU)
#   pragma GCC diagnostic push
#   pragma GCC diagnostic ignored "-Wunused-parameter"
#endif

#if defined(Q_OS_DARWIN) && defined(Q_CC_CLANG)
#   pragma clang diagnostic push
#   pragma clang diagnostic ignored "-Wunused-parameter"
#endif

extern "C"
{
#include "transupp.h"
}

// Restore warnings
#if !defined(Q_OS_DARWIN) && defined(Q_CC_GNU)
#   pragma GCC diagnostic pop
#endif

#if defined(Q_OS_DARWIN) && defined(Q_CC_CLANG)
#   pragma clang diagnostic pop
#endif

// Qt includes

#include <QFile>
#include <QFileInfo>
#include <QByteArray>
#include <QImageReader>
#include <QScopedPointer>
#include <qplatformdefs.h>

// Local includes

#include "dimg.h"
#include "digikam_debug.h"
#include "dfileoperations.h"
#include "metaenginesettings.h"
#include "filereadwritelock.h"

#ifdef Q_OS_WIN
#   include "windows.h"
#   include "jpegwin.h"
#endif

namespace Digikam
{

namespace JPEGUtils
{

// To manage Errors/Warnings handling provide by libjpeg

static void jpegutils_jpeg_error_exit(j_common_ptr cinfo)
{
    char buffer[JMSG_LENGTH_MAX];
    (*cinfo->err->format_message)(cinfo, buffer);

    qCWarning(DIGIKAM_GENERAL_LOG) << "Jpegutils error:" << buffer;

    throw std::runtime_error(buffer);
}

static void jpegutils_jpeg_emit_message(j_common_ptr cinfo, int msg_level)
{
    Q_UNUSED(msg_level)
    char buffer[JMSG_LENGTH_MAX];
    (*cinfo->err->format_message)(cinfo, buffer);

    // TODO this was behind the ifdef guard for dimg imageloaders, should this class be moved to dimg?

    //qCDebug(DIGIKAM_GENERAL_LOG) << buffer << " (" << msg_level << ")";
}

static void jpegutils_jpeg_output_message(j_common_ptr cinfo)
{
    char buffer[JMSG_LENGTH_MAX];
    (*cinfo->err->format_message)(cinfo, buffer);

    // TODO this was behind the ifdef guard for dimg imageloaders, should this class be moved to dimg?

    //qCDebug(DIGIKAM_GENERAL_LOG) << buffer;
}

bool loadJPEGScaled(QImage& image, const QString& path, int maximumSize)
{
    FileReadLocker lock(path);

    if (!isJpegImage(path))
    {
        return false;
    }

#ifdef Q_OS_WIN

    FILE* const inFile = _wfopen((const wchar_t*)path.utf16(), L"rb");

#else

    FILE* const inFile = fopen(path.toUtf8().constData(), "rb");

#endif

    if (!inFile)
    {
        return false;
    }

    struct jpeg_decompress_struct cinfo;
    struct jpeg_error_mgr         jerr;

    // JPEG error handling - thanks to Marcus Meissner

    cinfo.err                 = jpeg_std_error(&jerr);
    cinfo.err->error_exit     = jpegutils_jpeg_error_exit;
    cinfo.err->emit_message   = jpegutils_jpeg_emit_message;
    cinfo.err->output_message = jpegutils_jpeg_output_message;

    try
    {
        jpeg_create_decompress(&cinfo);
        jpeg_stdio_src(&cinfo, inFile);
        jpeg_read_header(&cinfo, true);

        int imgSize = qMax(cinfo.image_width, cinfo.image_height);
        int scale   = 1;

        // libjpeg supports 1/1, 1/2, 1/4, 1/8

        while (maximumSize*scale*2 <= imgSize)
        {
            scale *= 2;
        }

        if (scale > 8)
        {
            scale = 8;
        }
/*
        cinfo.scale_num = 1;
        cinfo.scale_denom = scale;
*/
        cinfo.scale_denom *= scale;

        switch (cinfo.jpeg_color_space)
        {
            case JCS_UNKNOWN:
            {
                break;
            }

            case JCS_GRAYSCALE:
            case JCS_RGB:
            case JCS_YCbCr:
            {
                cinfo.out_color_space = JCS_RGB;
                break;
            }

            case JCS_CMYK:
            case JCS_YCCK:
            {
                cinfo.out_color_space = JCS_CMYK;
                break;
            }

            default:
            {
                break;
            }
        }

        jpeg_start_decompress(&cinfo);

        QImage img;

        // We only take RGB with 1 or 3 components, or CMYK with 4 components

        if (!(
               (
                 (cinfo.out_color_space    == JCS_RGB)  &&
                 ((cinfo.output_components == 3) || (cinfo.output_components == 1))
               ) ||
               (
                 (cinfo.out_color_space    == JCS_CMYK) &&
                 (cinfo.output_components  == 4)
               )
             )
           )
        {
            jpeg_destroy_decompress(&cinfo);
            fclose(inFile);

            return false;
        }

        switch (cinfo.output_components)
        {
            case 3:
            case 4:
            {
                img = QImage(cinfo.output_width, cinfo.output_height, QImage::Format_RGB32);
                break;
            }

            case 1: // B&W image
            {
                img = QImage(cinfo.output_width, cinfo.output_height, QImage::Format_Indexed8);
                img.setColorCount(256);

                for (int i = 0 ; i < 256 ; ++i)
                {
                    img.setColor(i, qRgb(i, i, i));
                }

                break;
            }
        }

        uchar* const data = img.bits();
        int bpl           = img.bytesPerLine();

        while (cinfo.output_scanline < cinfo.output_height)
        {
            uchar* d = data + cinfo.output_scanline * bpl;
            jpeg_read_scanlines(&cinfo, &d, 1);
        }

        jpeg_finish_decompress(&cinfo);

        if (cinfo.output_components == 3)
        {
            // Expand 24->32 bpp.

            for (uint j = 0 ; j < cinfo.output_height ; ++j)
            {
                uchar* in       = img.scanLine(j) + cinfo.output_width * 3;
                QRgb* const out = reinterpret_cast<QRgb*>(img.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       = img.scanLine(j) + cinfo.output_width * 4;
                QRgb* const out = reinterpret_cast<QRgb*>(img.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)
        {
            img.setDotsPerMeterX(int(100. * cinfo.X_density / 2.54));
            img.setDotsPerMeterY(int(100. * cinfo.Y_density / 2.54));
        }
        else if (cinfo.density_unit == 2)
        {
            img.setDotsPerMeterX(int(100. * cinfo.X_density));
            img.setDotsPerMeterY(int(100. * cinfo.Y_density));
        }
/*
        int newMax = qMax(cinfo.output_width, cinfo.output_height);
        int newx   = maximumSize*cinfo.output_width  / newMax;
        int newy   = maximumSize*cinfo.output_height / newMax;
*/
        jpeg_destroy_decompress(&cinfo);
        fclose(inFile);

        image = img;

        return true;
    }
    catch (std::runtime_error&)
    {
        jpeg_destroy_decompress(&cinfo);
        fclose(inFile);

        return false;
    }
}

JpegRotator::JpegRotator(const QString& file)
    : m_file    (file),
      m_destFile(file)
{
    m_metadata     = new DMetadata;
    m_metadata->load(file);
    m_orientation  = m_metadata->getItemOrientation();
    QFileInfo info(file);
    m_documentName = info.fileName();
}

JpegRotator::~JpegRotator()
{
    delete m_metadata;
}

// -----------------------------------------------------------------------------

void JpegRotator::setCurrentOrientation(MetaEngine::ImageOrientation orientation)
{
    m_orientation = orientation;
}

void JpegRotator::setDocumentName(const QString& documentName)
{
    m_documentName = documentName;
}

void JpegRotator::setDestinationFile(const QString& dest)
{
    m_destFile = dest;
}

bool JpegRotator::autoExifTransform()
{
    return exifTransform(MetaEngineRotation::NoTransformation);
}

bool JpegRotator::exifTransform(TransformAction action)
{
    MetaEngineRotation matrix;
    matrix *= m_orientation;
    matrix *= action;

    return exifTransform(matrix);
}

bool JpegRotator::exifTransform(const MetaEngineRotation& matrix)
{
    FileWriteLocker lock(m_destFile);

    QFileInfo fi(m_file);

    if (!fi.exists())
    {
        qCDebug(DIGIKAM_GENERAL_LOG) << "ExifRotate: file does not exist: " << m_file;

        return false;
    }

    if (!isJpegImage(m_file))
    {
        // Not a jpeg image.

        qCDebug(DIGIKAM_GENERAL_LOG) << "ExifRotate: not a JPEG file: " << m_file;

        return false;
    }

    QList<TransformAction> actions = matrix.transformations();

    if (actions.isEmpty())
    {
        if (m_file != m_destFile)
        {
            return DFileOperations::copyFile(m_file, m_destFile);
        }

        return true;
    }

    bool        ret  = true;
    QString     dest = m_destFile;
    QString     src  = m_file;
    QString     dir  = fi.path();
    QStringList removeLater;

    MetaEngineSettingsContainer::RotationBehaviorFlags behavior;
    behavior = MetaEngineSettings::instance()->settings().rotationBehavior;

    for (int i = 0 ; i < actions.size() ; ++i)
    {
        SafeTemporaryFile* const temp = new SafeTemporaryFile(dir + QLatin1String("/JpegRotator-XXXXXX"
                                                                                  ".digikamtempfile.jpg"));
        temp->setAutoRemove(false);
        temp->open();
        QString tempFile = temp->safeFilePath();

        // Crash fix: a QTemporaryFile is not properly closed until its destructor is called.

        delete temp;

        bool canLosslessTransform = true;

#if (JPEG_LIB_VERSION < 80)

        PhotoInfoContainer photoInfo = m_metadata->getPhotographInformation();
        QStringList unsupportedModels({ QLatin1String("Redmi Note 6 Pro") });

        for (const QString& model : std::as_const(unsupportedModels))
        {
            if (model == photoInfo.model)
            {
                qCDebug(DIGIKAM_GENERAL_LOG) << "Unsupported camera model for lossless transform:"
                                             << photoInfo.model;
                canLosslessTransform = false;
                break;
            }
        }

#endif

        if (!canLosslessTransform || !performJpegTransform(actions[i], src, tempFile))
        {
            qCDebug(DIGIKAM_GENERAL_LOG) << "JPEG lossless transform failed for" << src;

            // See bug 320107 : if lossless transform cannot be achieve,
            // do lossy transform if enabled in the settings.

            if (!(behavior & MetaEngineSettingsContainer::RotateByLossyRotation))
            {
                removeLater << tempFile;
                ret = false;

                break;
            }

            DImg srcImg;

            qCDebug(DIGIKAM_GENERAL_LOG) << "Trying lossy transform for" << src;

            if (!srcImg.load(src))
            {
                removeLater << tempFile;
                ret = false;

                break;
            }

            if (actions[i] != MetaEngineRotation::NoTransformation)
            {
                srcImg.transform(actions[i]);
            }

            srcImg.setAttribute(QLatin1String("quality"), getJpegQuality(src));

            if (!srcImg.save(tempFile, DImg::JPEG))
            {
                qCDebug(DIGIKAM_GENERAL_LOG) << "Lossy transform failed for" << src;

                removeLater << tempFile;
                ret = false;

                break;
            }

            qCDebug(DIGIKAM_GENERAL_LOG) << "Lossy transform done for" << src;
        }

        if (i + 1 != actions.size())
        {
            // another round

            src = tempFile;
            removeLater << tempFile;

            continue;
        }

        // finalize

        updateMetadata(tempFile, matrix);

        // atomic rename

        if (DMetadata::hasSidecar(tempFile))
        {
            QString sidecarTemp = DMetadata::sidecarPath(tempFile);
            QString sidecarDest = DMetadata::sidecarPath(dest);

            if ((sidecarTemp != sidecarDest) &&
                QFile::exists(sidecarTemp)   &&
                QFile::exists(sidecarDest))
            {
                QFile::remove(sidecarDest);
            }

            if (!QFile::rename(sidecarTemp, sidecarDest))
            {
                qCDebug(DIGIKAM_GENERAL_LOG) << "Renaming sidecar file" << sidecarTemp
                                             << "to" << sidecarDest << "failed";

                removeLater << sidecarTemp;
                ret = false;
                break;
            }
        }

        if (
            (tempFile != dest)      &&
            QFile::exists(tempFile) &&
            QFile::exists(dest)
           )
        {
            QFile::remove(dest);
        }

        if (!QFile::rename(tempFile, dest))
        {
            qCDebug(DIGIKAM_GENERAL_LOG) << "Renaming" << tempFile
                                         << "to" << dest << "failed";

            removeLater << tempFile;
            ret = false;

            break;
        }
    }

    for (const QString& temp : std::as_const(removeLater))
    {
        QFile::remove(temp);
    }

    return ret;
}

void JpegRotator::updateMetadata(const QString& fileName, const MetaEngineRotation &matrix)
{
    QTransform qmatrix = matrix.toTransform();
    QRect r(QPoint(0, 0), m_originalSize);
    QSize newSize      = qmatrix.mapRect(r).size();

    // Get the new image dimension of the temp image. Using a dummy QImage object here
    // has a sense because the Exif dimension information can be missing from original image.
    // Get new dimensions with QImage will always work...

    m_metadata->setItemDimensions(newSize);

    // Update the image thumbnail.

    QImage exifThumb = m_metadata->getExifThumbnail(false);

    if (!exifThumb.isNull())
    {
        m_metadata->setExifThumbnail(exifThumb.transformed(qmatrix));
    }

    QImage imagePreview;

    if (m_metadata->getItemPreview(imagePreview))
    {
        m_metadata->setItemPreview(imagePreview.transformed(qmatrix));
    }

    // Reset the Exif orientation tag of the temp image to normal

    m_metadata->setItemOrientation(DMetadata::ORIENTATION_NORMAL);

    // We update all new metadata now...

    m_metadata->save(fileName, true);

    // File properties restoration.

    // See bug #329608: Restore file modification time from original file
    // only if updateFileTimeStamp for Setup/Metadata is turned off.

    if (!MetaEngineSettings::instance()->settings().updateFileTimeStamp)
    {
        DFileOperations::copyModificationTime(m_file, fileName);
    }

    // Restore permissions in all cases

    QFile::Permissions permissions = QFile::permissions(m_file);
    QFile::setPermissions(fileName, permissions);
}

bool JpegRotator::performJpegTransform(TransformAction action, const QString& src, const QString& dest)
{
    JCOPY_OPTION copyoption         = JCOPYOPT_ALL;
    jpeg_transform_info transformoption;

    transformoption.force_grayscale = false;
    transformoption.trim            = false;

#if (JPEG_LIB_VERSION >= 80)

    // we need to initialize a few more parameters, see bug 274947

    transformoption.perfect         = true;   // See bug 320107 : we need perfect transform here.
    transformoption.crop            = false;

#endif // (JPEG_LIB_VERSION >= 80)

    // NOTE : Cast is fine here. See metaengine_rotation.h for details.

    transformoption.transform       = (JXFORM_CODE)action;

    if (transformoption.transform == JXFORM_NONE)
    {
        return true;
    }

    // A transformation must be done.

#ifdef Q_OS_WIN

    FILE* const input_file = _wfopen((const wchar_t*)src.utf16(), L"rb");

#else

    FILE* const input_file = fopen(src.toUtf8().constData(), "rb");

#endif

    if (!input_file)
    {
        qCWarning(DIGIKAM_GENERAL_LOG) << "ExifRotate: Error in opening input file: " << src;

        return false;
    }

#ifdef Q_OS_WIN

    FILE* const output_file = _wfopen((const wchar_t*)dest.utf16(), L"wb");

#else

    FILE* const output_file = fopen(dest.toUtf8().constData(), "wb");

#endif

    if (!output_file)
    {
        fclose(input_file);
        qCWarning(DIGIKAM_GENERAL_LOG) << "ExifRotate: Error in opening output file: " << dest;

        return false;
    }

    struct jpeg_decompress_struct srcinfo;
    struct jpeg_compress_struct   dstinfo;
    struct jpeg_error_mgr         jsrcerr, jdsterr;
    jvirt_barray_ptr* src_coef_arrays = nullptr;
    jvirt_barray_ptr* dst_coef_arrays = nullptr;

    // Initialize the JPEG decompression object with default error handling

    srcinfo.err                       = jpeg_std_error(&jsrcerr);
    srcinfo.err->error_exit           = jpegutils_jpeg_error_exit;
    srcinfo.err->emit_message         = jpegutils_jpeg_emit_message;
    srcinfo.err->output_message       = jpegutils_jpeg_output_message;

    // Initialize the JPEG compression object with default error handling

    dstinfo.err                       = jpeg_std_error(&jdsterr);
    dstinfo.err->error_exit           = jpegutils_jpeg_error_exit;
    dstinfo.err->emit_message         = jpegutils_jpeg_emit_message;
    dstinfo.err->output_message       = jpegutils_jpeg_output_message;

    try
    {
        jpeg_create_decompress(&srcinfo);
        jpeg_create_compress(&dstinfo);

        jpeg_stdio_src(&srcinfo, input_file);
        jcopy_markers_setup(&srcinfo, copyoption);

        (void) jpeg_read_header(&srcinfo, true);

        // Read original size initially

        if (!m_originalSize.isValid())
        {
            m_originalSize = QSize(srcinfo.image_width, srcinfo.image_height);
        }

#if (JPEG_LIB_VERSION >= 80)

        if (!jtransform_request_workspace(&srcinfo, &transformoption))
        {
            qCDebug(DIGIKAM_GENERAL_LOG) << "ExifRotate: Transformation is not perfect";
            jpeg_destroy_decompress(&srcinfo);
            jpeg_destroy_compress(&dstinfo);
            fclose(input_file);
            fclose(output_file);

            return false;
        }

#else

        if (((srcinfo.image_width % 8) != 0) || ((srcinfo.image_height % 8) != 0))
        {
            qCDebug(DIGIKAM_GENERAL_LOG) << "ExifRotate: Transformation is not perfect";
            jpeg_destroy_decompress(&srcinfo);
            jpeg_destroy_compress(&dstinfo);
            fclose(input_file);
            fclose(output_file);

            return false;
        }

        jtransform_request_workspace(&srcinfo, &transformoption);

#endif

        // Read source file as DCT coefficients

        src_coef_arrays         = jpeg_read_coefficients(&srcinfo);

        // Initialize destination compression parameters from source values

        jpeg_copy_critical_parameters(&srcinfo, &dstinfo);
        dst_coef_arrays         = jtransform_adjust_parameters(&srcinfo, &dstinfo, src_coef_arrays, &transformoption);

        // Specify data destination for compression

        jpeg_stdio_dest(&dstinfo, output_file);

        // Start compressor (note no image data is actually written here)

        dstinfo.optimize_coding = true;
        jpeg_write_coefficients(&dstinfo, dst_coef_arrays);

        // Copy to the output file any extra markers that we want to preserve

        jcopy_markers_execute(&srcinfo, &dstinfo, copyoption);
        jtransform_execute_transformation(&srcinfo, &dstinfo, src_coef_arrays, &transformoption);

        // Finish compression and release memory

        jpeg_finish_compress(&dstinfo);
        jpeg_destroy_compress(&dstinfo);
        (void) jpeg_finish_decompress(&srcinfo);
        jpeg_destroy_decompress(&srcinfo);

        fclose(input_file);
        fclose(output_file);

        return true;
    }
    catch (std::runtime_error&)
    {
        jpeg_destroy_decompress(&srcinfo);
        jpeg_destroy_compress(&dstinfo);
        fclose(input_file);
        fclose(output_file);

        return false;
    }
}

bool jpegConvert(const QString& src, const QString& dest, const QString& documentName, const QString& format)
{
    qCDebug(DIGIKAM_GENERAL_LOG) << "Converting " << src
                                 << " to " << dest
                                 << " format: " << format
                                 << " documentName: " << documentName;
    QFileInfo fi(src);

    if (!fi.exists())
    {
        qCDebug(DIGIKAM_GENERAL_LOG) << "JpegConvert: file do not exist: " << src;

        return false;
    }

    if (isJpegImage(src))
    {
        DImg image(src);

        // Get image Exif/IPTC data.

        QScopedPointer<DMetadata> meta(new DMetadata(image.getMetadata()));

        // Update IPTC preview.

        QImage preview = image.smoothScale(1280, 1024, Qt::KeepAspectRatio).copyQImage();

        // TODO: see bug #130525. a JPEG segment is limited to 64K. If the IPTC byte array is
        // bigger than 64K duing of image preview tag size, the target JPEG image will be
        // broken. Note that IPTC image preview tag is limited to 256K!!!
        // Temp. solution to disable IPTC preview record in JPEG file until a right solution
        // will be found into Exiv2.
        // Note : There is no limitation with TIFF and PNG about IPTC byte array size.

        if (
            (format.toUpper() != QLatin1String("JPG"))  &&
            (format.toUpper() != QLatin1String("JPEG")) &&
            (format.toUpper() != QLatin1String("JPE"))
           )
        {
            meta->setItemPreview(preview);
        }

        // Update Exif thumbnail.

        QImage thumb = preview.scaled(160, 120, Qt::KeepAspectRatio, Qt::SmoothTransformation);
        meta->setExifThumbnail(thumb);

        // Update Exif Document Name tag (the original file name from camera for example).

        meta->setExifTagString("Exif.Image.DocumentName", documentName);

        // Store new Exif/IPTC data into image.

        image.setMetadata(meta->data());

        // And now save the image to a new file format.

        if (format.toUpper() == QLatin1String("PNG"))
        {
            image.setAttribute(QLatin1String("quality"), 9);
        }

        if ((format.toUpper() == QLatin1String("TIFF")) || (format.toUpper() == QLatin1String("TIF")))
        {
            image.setAttribute(QLatin1String("compress"), true);
        }

        if (
            (format.toUpper() == QLatin1String("JP2")) || (format.toUpper() == QLatin1String("JPX")) ||
            (format.toUpper() == QLatin1String("JPC")) || (format.toUpper() == QLatin1String("PGX")) ||
            (format.toUpper() == QLatin1String("J2K"))
           )
        {
            image.setAttribute(QLatin1String("quality"), 100);    // LossLess
        }

        if (format.toUpper() == QLatin1String("JXL"))
        {
            image.setAttribute(QLatin1String("quality"), 100);    // LossLess
        }

        if (format.toUpper() == QLatin1String("WEBP"))
        {
            image.setAttribute(QLatin1String("quality"), 100);    // LossLess
        }

        if (format.toUpper() == QLatin1String("AVIF"))
        {
            image.setAttribute(QLatin1String("quality"), 100);    // LossLess
        }

        if (format.toUpper() == QLatin1String("PGF"))
        {
            image.setAttribute(QLatin1String("quality"), 0);      // LossLess
        }

        if ((format.toUpper() == QLatin1String("HEIF")) || (format.toUpper() == QLatin1String("HEIC")) ||
            (format.toUpper() == QLatin1String("HIF")))
        {
            image.setAttribute(QLatin1String("quality"), 0);      // LossLess
        }

        return (image.save(dest, format));
    }

    return false;
}

bool isJpegImage(const QString& file)
{
    QFileInfo fileInfo(file);

    // Check if the file is an JPEG image

    QString format = QString::fromUtf8(QImageReader::imageFormat(file)).toUpper();

    // Check if its not MPO format (See bug #307277).

    QString ext    = fileInfo.suffix().toUpper();

    qCDebug(DIGIKAM_GENERAL_LOG) << "mimetype = " << format << " ext = " << ext;

    if ((format != QLatin1String("JPEG")) || (ext == QLatin1String("MPO")))
    {
        return false;
    }

    return true;
}

int getJpegQuality(const QString& file)
{
    // Set a good default quality

    volatile int quality = 90;

    if (!isJpegImage(file))
    {
        return quality;
    }

#ifdef Q_OS_WIN

    FILE* const inFile = _wfopen((const wchar_t*)file.utf16(), L"rb");

#else

    FILE* const inFile = fopen(file.toUtf8().constData(), "rb");

#endif

    if (!inFile)
    {
        return quality;
    }

    struct jpeg_decompress_struct jpeg_info;
    struct jpeg_error_mgr         jerr;

    // Initialize the JPEG decompression object with default error handling

    jpeg_info.err                 = jpeg_std_error(&jerr);
    jpeg_info.err->error_exit     = jpegutils_jpeg_error_exit;
    jpeg_info.err->emit_message   = jpegutils_jpeg_emit_message;
    jpeg_info.err->output_message = jpegutils_jpeg_output_message;

    try
    {
        jpeg_create_decompress(&jpeg_info);
        jpeg_stdio_src(&jpeg_info, inFile);
        jpeg_read_header(&jpeg_info, true);
        jpeg_start_decompress(&jpeg_info);

        // https://subversion.imagemagick.org/subversion/ImageMagick/trunk/coders/jpeg.c
        // Determine the JPEG compression quality from the quantization tables.

        long value;
        long i, j;
        long sum = 0;

        for (i = 0 ; i < NUM_QUANT_TBLS ; ++i)
        {
            if (jpeg_info.quant_tbl_ptrs[i] != nullptr)
            {
                for (j = 0 ; j < DCTSIZE2 ; ++j)
                {
                    sum += jpeg_info.quant_tbl_ptrs[i]->quantval[j];
                }
            }
        }

        if (
            (jpeg_info.quant_tbl_ptrs[0] != nullptr) &&
            (jpeg_info.quant_tbl_ptrs[1] != nullptr)
           )
        {
            const long hash[101] =
                {
                    1020, 1015,  932,  848,  780,  735,  702,  679,  660,  645,
                     632,  623,  613,  607,  600,  594,  589,  585,  581,  571,
                     555,  542,  529,  514,  494,  474,  457,  439,  424,  410,
                     397,  386,  373,  364,  351,  341,  334,  324,  317,  309,
                     299,  294,  287,  279,  274,  267,  262,  257,  251,  247,
                     243,  237,  232,  227,  222,  217,  213,  207,  202,  198,
                     192,  188,  183,  177,  173,  168,  163,  157,  153,  148,
                     143,  139,  132,  128,  125,  119,  115,  108,  104,   99,
                      94,   90,   84,   79,   74,   70,   64,   59,   55,   49,
                      45,   40,   34,   30,   25,   20,   15,   11,    6,    4,
                       0
                },
                sums[101] =
                {
                    32640, 32635, 32266, 31495, 30665, 29804, 29146, 28599, 28104,
                    27670, 27225, 26725, 26210, 25716, 25240, 24789, 24373, 23946,
                    23572, 22846, 21801, 20842, 19949, 19121, 18386, 17651, 16998,
                    16349, 15800, 15247, 14783, 14321, 13859, 13535, 13081, 12702,
                    12423, 12056, 11779, 11513, 11135, 10955, 10676, 10392, 10208,
                     9928,  9747,  9564,  9369,  9193,  9017,  8822,  8639,  8458,
                     8270,  8084,  7896,  7710,  7527,  7347,  7156,  6977,  6788,
                     6607,  6422,  6236,  6054,  5867,  5684,  5495,  5305,  5128,
                     4945,  4751,  4638,  4442,  4248,  4065,  3888,  3698,  3509,
                     3326,  3139,  2957,  2775,  2586,  2405,  2216,  2037,  1846,
                     1666,  1483,  1297,  1109,   927,   735,   554,   375,   201,
                      128,     0
                };

            value = (long)(jpeg_info.quant_tbl_ptrs[0]->quantval[2]  +
                           jpeg_info.quant_tbl_ptrs[0]->quantval[53] +
                           jpeg_info.quant_tbl_ptrs[1]->quantval[0]  +
                           jpeg_info.quant_tbl_ptrs[1]->quantval[DCTSIZE2 - 1]);

            for (i = 0 ; i < 100 ; ++i)
            {
                if ((value < hash[i]) && (sum < sums[i]))
                {
                    continue;
                }

                if (((value <= hash[i]) && (sum <= sums[i])) || (i >= 50))
                {
                    quality = i + 1;
                }

                break;
            }
        }
        else if (jpeg_info.quant_tbl_ptrs[0] != nullptr)
        {
            long hash[101] =
                {
                    510,  505,  422,  380,  355,  338,  326,  318,  311,  305,
                    300,  297,  293,  291,  288,  286,  284,  283,  281,  280,
                    279,  278,  277,  273,  262,  251,  243,  233,  225,  218,
                    211,  205,  198,  193,  186,  181,  177,  172,  168,  164,
                    158,  156,  152,  148,  145,  142,  139,  136,  133,  131,
                    129,  126,  123,  120,  118,  115,  113,  110,  107,  105,
                    102,  100,   97,   94,   92,   89,   87,   83,   81,   79,
                     76,   74,   70,   68,   66,   63,   61,   57,   55,   52,
                     50,   48,   44,   42,   39,   37,   34,   31,   29,   26,
                     24,   21,   18,   16,   13,   11,    8,    6,    3,    2,
                      0
                },
                sums[101] =
                {
                    16320, 16315, 15946, 15277, 14655, 14073, 13623, 13230, 12859,
                    12560, 12240, 11861, 11456, 11081, 10714, 10360, 10027,  9679,
                    9368,  9056,  8680,  8331,  7995,  7668,  7376,  7084,  6823,
                    6562,  6345,  6125,  5939,  5756,  5571,  5421,  5240,  5086,
                    4976,  4829,  4719,  4616,  4463,  4393,  4280,  4166,  4092,
                    3980,  3909,  3835,  3755,  3688,  3621,  3541,  3467,  3396,
                    3323,  3247,  3170,  3096,  3021,  2952,  2874,  2804,  2727,
                    2657,  2583,  2509,  2437,  2362,  2290,  2211,  2136,  2068,
                    1996,  1915,  1858,  1773,  1692,  1620,  1552,  1477,  1398,
                    1326,  1251,  1179,  1109,  1031,   961,   884,   814,   736,
                     667,   592,   518,   441,   369,   292,   221,   151,    86,
                      64,     0
                };

            value = (long)(jpeg_info.quant_tbl_ptrs[0]->quantval[2] +
                        jpeg_info.quant_tbl_ptrs[0]->quantval[53]);

            for (i = 0 ; i < 100 ; ++i)
            {
                if ((value < hash[i]) && (sum < sums[i]))
                {
                    continue;
                }

                if (((value <= hash[i]) && (sum <= sums[i])) || (i >= 50))
                {
                    quality = i + 1;
                }

                break;
            }
        }

        jpeg_destroy_decompress(&jpeg_info);
        fclose(inFile);

        qCDebug(DIGIKAM_GENERAL_LOG) << "JPEG Quality: " << quality << " File: " << file;

        return quality;
    }
    catch (std::runtime_error&)
    {
        jpeg_destroy_decompress(&jpeg_info);
        fclose(inFile);

        return quality;
    }
}

} // namespace JPEGUtils

} // namespace Digikam
