/* ============================================================
 *
 * This file is a part of digiKam project
 * https://www.digikam.org
 *
 * Date        : 2004-09-29
 * Description : helper methods for JPEG image format.
 *
 * Copyright (C) 2004-2005 by Renchi Raju <renchi dot raju at gmail dot com>
 * Copyright (C) 2006-2022 by Gilles Caulier <caulier dot gilles at gmail dot com>
 * Copyright (C) 2006-2012 by Marcel Wiesweg <marcel dot wiesweg at gmx dot de>
 *
 * Parts of the loading code is taken from qjpeghandler.cpp
 * Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies).
 * All rights reserved.
 * Contact: Nokia Corporation (qt-info@nokia.com)
 *
 * This program is free software; you can redistribute it
 * and/or modify it under the terms of the GNU General
 * Public License as published by the Free Software Foundation;
 * either version 2, or (at your option)
 * any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * ============================================================ */

#define XMD_H

#include "jpegutils.h"

// C++ includes

#include <cstdio>
#include <cstdlib>

// C ANSI includes

extern "C"
{
#include <sys/types.h>
#include <setjmp.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 <QImageReader>
#include <QByteArray>
#include <QUuid>
#include <QFile>
#include <QFileInfo>
#include <QScopedPointer>
#include <qplatformdefs.h>

// Local includes

#include "dimg.h"
#include "digikam_debug.h"
#include "digikam_config.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

struct jpegutils_jpeg_error_mgr : public jpeg_error_mgr
{
    jmp_buf setjmp_buffer;
};

static void jpegutils_jpeg_error_exit(j_common_ptr cinfo)
{
    jpegutils_jpeg_error_mgr* myerr = static_cast<jpegutils_jpeg_error_mgr*>(cinfo->err);

    char buffer[JMSG_LENGTH_MAX];
    (*cinfo->err->format_message)(cinfo, buffer);

    qCDebug(DIGIKAM_GENERAL_LOG) << "Jpegutils error, aborting operation:" << buffer;

#ifdef __MINGW32__  // krazy:exclude=cpp

    __builtin_longjmp(myerr->setjmp_buffer, 1);

#else

    longjmp(myerr->setjmp_buffer, 1);

#endif

}

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 jpegutils_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;

#ifdef __MINGW32__  // krazy:exclude=cpp

    if (__builtin_setjmp(jerr.setjmp_buffer))

#else

    if (setjmp(jerr.setjmp_buffer))

#endif

    {
        jpeg_destroy_decompress(&cinfo);
        fclose(inFile);
        return false;
    }

    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;
}

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 QFile::copy(m_file, m_destFile);
        }

        return true;
    }

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

    for (int i = 0 ; i < actions.size() ; ++i)
    {
        QString random                = QUuid::createUuid().toString().mid(1, 8);
        SafeTemporaryFile* const temp = new SafeTemporaryFile(dir + QLatin1String("/JpegRotator-XXXXXX-") +
                                                              random + QLatin1String(".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;

        if (!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.

            DImg srcImg;

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

            if (!srcImg.load(src))
            {
                QFile::remove(tempFile);
                return false;
            }

            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;

                QFile::remove(tempFile);
                return false;
            }

            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;
                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;
            break;
        }
    }

    foreach (const QString& tempFile, removeLater)
    {
        QFile::remove(tempFile);
    }

    return true;
}

void JpegRotator::updateMetadata(const QString& fileName, const MetaEngineRotation &matrix)
{
    QMatrix qmatrix = matrix.toMatrix();
    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(true);

    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.

    struct jpeg_decompress_struct srcinfo;
    struct jpeg_compress_struct   dstinfo;
    struct jpegutils_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;

#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;
    }

    if (setjmp(jsrcerr.setjmp_buffer) || setjmp(jdsterr.setjmp_buffer))
    {
        jpeg_destroy_decompress(&srcinfo);
        jpeg_destroy_compress(&dstinfo);
        fclose(input_file);
        fclose(output_file);
        return false;
    }

    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

    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;
}

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("PGF"))
        {
            image.setAttribute(QLatin1String("quality"), 0);      // LossLess
        }

        if ((format.toUpper() == QLatin1String("HEIF")) || (format.toUpper() == QLatin1String("HEIC")))
        {
            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 jpegutils_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;

    if (setjmp(jerr.setjmp_buffer))
    {
        jpeg_destroy_decompress(&jpeg_info);
        fclose(inFile);
        return quality;
    }

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

} // namespace JPEGUtils

} // namespace Digikam
